itd/infinitime/dfu.go

223 lines
4.8 KiB
Go

package infinitime
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"io/fs"
"tinygo.org/x/bluetooth"
)
const (
dfuSegmentSize = 20 // Size of each firmware packet
dfuPktRecvInterval = 10 // Amount of packets to send before checking for receipt
)
var (
dfuCmdStart = []byte{0x01, 0x04}
dfuCmdRecvInitPkt = []byte{0x02, 0x00}
dfuCmdInitPktComplete = []byte{0x02, 0x01}
dfuCmdPktReceiptInterval = []byte{0x08}
dfuCmdRecvFirmware = []byte{0x03}
dfuCmdValidate = []byte{0x04}
dfuCmdActivateReset = []byte{0x05}
dfuResponseStart = []byte{0x10, 0x01, 0x01}
dfuResponseInitParams = []byte{0x10, 0x02, 0x01}
dfuResponseRecvFwImgSuccess = []byte{0x10, 0x03, 0x01}
dfuResponseValidate = []byte{0x10, 0x04, 0x01}
)
// DFUOptions contains options for [UpgradeFirmware]
type DFUOptions struct {
InitPacket fs.File
FirmwareImage fs.File
ProgressFunc func(sent, received, total uint32)
SegmentSize int
ReceiveInterval uint8
}
// UpgradeFirmware upgrades the firmware running on the PineTime.
func (d *Device) UpgradeFirmware(opts DFUOptions) error {
if opts.SegmentSize <= 0 {
opts.SegmentSize = dfuSegmentSize
}
if opts.ReceiveInterval <= 0 {
opts.ReceiveInterval = dfuPktRecvInterval
}
ctrlPoint, err := d.getChar(dfuCtrlPointChar)
if err != nil {
return err
}
packet, err := d.getChar(dfuPacketChar)
if err != nil {
return err
}
d.deviceMtx.Lock()
defer d.deviceMtx.Unlock()
d.updating.Store(true)
defer d.updating.Store(false)
_, err = ctrlPoint.WriteWithoutResponse(dfuCmdStart)
if err != nil {
return err
}
fi, err := opts.FirmwareImage.Stat()
if err != nil {
return err
}
size := uint32(fi.Size())
sizePacket := make([]byte, 8, 12)
sizePacket = binary.LittleEndian.AppendUint32(sizePacket, size)
_, err = packet.WriteWithoutResponse(sizePacket)
if err != nil {
return err
}
_, err = awaitDFUResponse(ctrlPoint, dfuResponseStart)
if err != nil {
return err
}
err = writeDFUInitPacket(ctrlPoint, packet, opts.InitPacket)
if err != nil {
return err
}
err = setRecvInterval(ctrlPoint, opts.ReceiveInterval)
if err != nil {
return err
}
err = sendFirmware(ctrlPoint, packet, opts, size)
if err != nil {
return err
}
return finalize(ctrlPoint)
}
func finalize(ctrlPoint *bluetooth.DeviceCharacteristic) error {
_, err := ctrlPoint.WriteWithoutResponse(dfuCmdValidate)
if err != nil {
return err
}
_, err = awaitDFUResponse(ctrlPoint, dfuResponseValidate)
if err != nil {
return err
}
_, _ = ctrlPoint.WriteWithoutResponse(dfuCmdActivateReset)
return nil
}
func sendFirmware(ctrlPoint, packet *bluetooth.DeviceCharacteristic, opts DFUOptions, totalSize uint32) error {
_, err := ctrlPoint.WriteWithoutResponse(dfuCmdRecvFirmware)
if err != nil {
return err
}
var (
chunksSinceReceipt uint8
bytesSent uint32
)
chunk := make([]byte, opts.SegmentSize)
for {
n, err := opts.FirmwareImage.Read(chunk)
if err != nil && !errors.Is(err, io.EOF) {
return err
} else if n == 0 {
break
}
bytesSent += uint32(n)
_, err = packet.WriteWithoutResponse(chunk[:n])
if err != nil {
return err
}
if errors.Is(err, io.EOF) {
break
}
chunksSinceReceipt += 1
if chunksSinceReceipt == opts.ReceiveInterval {
sizeData, err := awaitDFUResponse(ctrlPoint, []byte{0x11})
if err != nil {
return err
}
size := binary.LittleEndian.Uint32(sizeData)
if size != bytesSent {
return fmt.Errorf("size mismatch: expected %d, got %d", bytesSent, size)
}
if opts.ProgressFunc != nil {
opts.ProgressFunc(bytesSent, size, totalSize)
}
chunksSinceReceipt = 0
}
}
return nil
}
func writeDFUInitPacket(ctrlPoint, packet *bluetooth.DeviceCharacteristic, initPkt fs.File) error {
_, err := ctrlPoint.WriteWithoutResponse(dfuCmdRecvInitPkt)
if err != nil {
return err
}
initData, err := io.ReadAll(initPkt)
if err != nil {
return err
}
_, err = packet.WriteWithoutResponse(initData)
if err != nil {
return err
}
_, err = ctrlPoint.WriteWithoutResponse(dfuCmdInitPktComplete)
if err != nil {
return err
}
_, err = awaitDFUResponse(ctrlPoint, dfuResponseInitParams)
return err
}
func setRecvInterval(ctrlPoint *bluetooth.DeviceCharacteristic, interval uint8) error {
_, err := ctrlPoint.WriteWithoutResponse(append(dfuCmdPktReceiptInterval, interval))
return err
}
func awaitDFUResponse(ctrlPoint *bluetooth.DeviceCharacteristic, expect []byte) ([]byte, error) {
respCh := make(chan []byte, 1)
err := ctrlPoint.EnableNotifications(func(buf []byte) {
respCh <- buf
})
if err != nil {
return nil, err
}
data := <-respCh
ctrlPoint.EnableNotifications(nil)
if !bytes.HasPrefix(data, expect) {
return nil, fmt.Errorf("unexpected dfu response %x (expected %x)", data, expect)
}
return bytes.TrimPrefix(data, expect), nil
}