223 lines
4.8 KiB
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
|
|
}
|