This repository has been archived on 2024-04-14. You can view files and clone it, but cannot push or open issues or pull requests.
infinitime/dfu.go
2021-08-19 17:41:09 -07:00

364 lines
8.3 KiB
Go

package infinitime
import (
"archive/zip"
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"io"
"io/fs"
"io/ioutil"
"os"
"time"
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
)
const (
DFUCtrlPointChar = "00001531-1212-efde-1523-785feabcd123" // UUID of Control Point characteristic
DFUPacketChar = "00001532-1212-efde-1523-785feabcd123" // UUID of Packet characteristic
)
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, 0x0A}
DFUCmdRecvFirmware = []byte{0x03}
DFUCmdValidate = []byte{0x04}
DFUCmdActivateReset = []byte{0x05}
)
var (
DFUResponseStart = []byte{0x10, 0x01, 0x01}
DFUResponseInitParams = []byte{0x10, 0x02, 0x01}
DFUResponseRecvFwImgSuccess = []byte{0x10, 0x03, 0x01}
DFUResponseValidate = []byte{0x10, 0x04, 0x01}
)
var DFUNotifPktRecvd = []byte{0x11}
var (
ErrDFUInvalidInput = errors.New("input file invalid, must be a .bin file")
ErrDFUTimeout = errors.New("timed out waiting for response")
ErrDFUNoFilesLoaded = errors.New("no files are loaded")
ErrDFUInvalidResponse = errors.New("invalid response returned")
ErrDFUSizeMismatch = errors.New("amount of bytes sent does not match amount received")
)
var btOptsCmd = map[string]interface{}{"type": "command"}
// DFU stores everything required for doing firmware upgrades
type DFU struct {
initPacket fs.File
fwImage fs.File
ctrlRespCh <-chan *bluez.PropertyChanged
fwSize int64
bytesSent int
bytesRecvd int
fwSendDone bool
ctrlPointChar *gatt.GattCharacteristic1
packetChar *gatt.GattCharacteristic1
}
// LoadFiles loads an init packet (.dat) and firmware image (.bin)
func (dfu *DFU) LoadFiles(initPath, fwPath string) error {
// Open init packet file
initPktFl, err := os.Open(initPath)
if err != nil {
return err
}
dfu.initPacket = initPktFl
// Open firmware image file
fwImgFl, err := os.Open(fwPath)
if err != nil {
return err
}
dfu.fwImage = fwImgFl
// Get firmware file size
dfu.fwSize, err = getFlSize(dfu.fwImage)
if err != nil {
return err
}
return nil
}
type archiveManifest struct {
Manifest struct {
Application struct {
BinFile string `json:"bin_file"`
DatFile string `json:"dat_file"`
} `json:"application"`
} `json:"manifest"`
}
// LoadArchive loads an init packet and firmware image from a zip archive
// using a maifest.json also stored in the archive.
func (dfu *DFU) LoadArchive(archivePath string) error {
// Open archive file
archiveFl, err := os.Open(archivePath)
if err != nil {
return err
}
// Get archive size
archiveSize, err := getFlSize(archiveFl)
if err != nil {
return err
}
// Create zip reader from archive file
zipReader, err := zip.NewReader(archiveFl, archiveSize)
if err != nil {
return err
}
// Open manifest.json from zip archive
manifestFl, err := zipReader.Open("manifest.json")
if err != nil {
return err
}
var manifest archiveManifest
// Decode manifest file as JSON
err = json.NewDecoder(manifestFl).Decode(&manifest)
if err != nil {
return err
}
// Open init packet from zip archive
initPktFl, err := zipReader.Open(manifest.Manifest.Application.DatFile)
if err != nil {
return err
}
dfu.initPacket = initPktFl
// Open firmware image from zip archive
fwImgFl, err := zipReader.Open(manifest.Manifest.Application.BinFile)
if err != nil {
return err
}
dfu.fwImage = fwImgFl
// Get file size of firmware image
dfu.fwSize, err = getFlSize(dfu.fwImage)
if err != nil {
return err
}
return nil
}
// getFlSize uses Stat to get the size of a file
func getFlSize(fl fs.File) (int64, error) {
// Get file information
flInfo, err := fl.Stat()
if err != nil {
return 0, err
}
return flInfo.Size(), nil
}
// Start DFU process
func (dfu *DFU) Start() error {
if dfu.fwImage == nil || dfu.initPacket == nil {
return ErrDFUNoFilesLoaded
}
// Start notifications on control point
err := dfu.ctrlPointChar.StartNotify()
if err != nil {
return err
}
// Watch for property changes on control point
dfu.ctrlRespCh, err = dfu.ctrlPointChar.WatchProperties()
if err != nil {
return err
}
// Run step one
err = dfu.stepOne()
if err != nil {
return err
}
// Run step two
err = dfu.stepTwo()
if err != nil {
return err
}
// When 0x100101 received, run step three
err = dfu.on(DFUResponseStart, func(_ []byte) error {
return dfu.stepThree()
})
if err != nil {
return err
}
// Run step three
err = dfu.stepFour()
if err != nil {
return err
}
// When 0x100201 received. run step five
err = dfu.on(DFUResponseInitParams, func(_ []byte) error {
return dfu.stepFive()
})
if err != nil {
return err
}
// Run step six
err = dfu.stepSix()
if err != nil {
return err
}
// Run step seven
err = dfu.stepSeven()
if err != nil {
return err
}
// When 0x100301 received, run step eight
err = dfu.on(DFUResponseRecvFwImgSuccess, func(_ []byte) error {
return dfu.stepEight()
})
if err != nil {
return err
}
// When 0x100401 received, run step nine
err = dfu.on(DFUResponseValidate, func(_ []byte) error {
return dfu.stepNine()
})
if err != nil {
return err
}
return nil
}
// on waits for the given command to be received on
// the control point characteristic, then runs the callback.
func (dfu *DFU) on(cmd []byte, onCmdCb func(data []byte) error) error {
select {
case propChanged := <-dfu.ctrlRespCh:
if propChanged.Name != "Value" {
return ErrDFUInvalidResponse
}
// Assert propery value as byte slice
data := propChanged.Value.([]byte)
// If command has prefix of given command
if bytes.HasPrefix(data, cmd) {
// Return callback with data after command
return onCmdCb(data[len(cmd):])
}
return ErrDFUInvalidResponse
case <-time.After(50 * time.Second):
return ErrDFUTimeout
}
}
func (dfu *DFU) stepOne() error {
return dfu.ctrlPointChar.WriteValue(DFUCmdStart, nil)
}
func (dfu *DFU) stepTwo() error {
// Create byte slice with 4 bytes allocated
data := make([]byte, 4)
// Write little endian uint32 to data slice
binary.LittleEndian.PutUint32(data, uint32(dfu.fwSize))
// Pad data with 8 bytes
data = append(make([]byte, 8), data...)
// Write data to packet characteristic
return dfu.packetChar.WriteValue(data, nil)
}
func (dfu *DFU) stepThree() error {
return dfu.ctrlPointChar.WriteValue(DFUCmdRecvInitPkt, nil)
}
func (dfu *DFU) stepFour() error {
// Read init packet
data, err := ioutil.ReadAll(dfu.initPacket)
if err != nil {
return err
}
// Write init packet to packet characteristic
err = dfu.packetChar.WriteValue(data, nil)
if err != nil {
return err
}
// Write init packet complete command to control point
return dfu.ctrlPointChar.WriteValue(DFUCmdInitPktComplete, nil)
}
func (dfu *DFU) stepFive() error {
return dfu.ctrlPointChar.WriteValue(DFUCmdPktReceiptInterval, nil)
}
func (dfu *DFU) stepSix() error {
return dfu.ctrlPointChar.WriteValue(DFUCmdRecvFirmware, nil)
}
func (dfu *DFU) stepSeven() error {
// While send is not done
for !dfu.fwSendDone {
for i := 0; i < DFUPktRecvInterval; i++ {
// Create byte slice with segment size
segment := make([]byte, DFUSegmentSize)
// Write firmware image into slice
n, err := dfu.fwImage.Read(segment)
// If EOF, send is done
if err == io.EOF {
dfu.fwSendDone = true
return nil
} else if err != nil {
return err
}
// Write segment to packet characteristic
err = dfu.packetChar.WriteValue(segment, nil)
if err != nil {
return err
}
// Increment bytes sent by amount read
dfu.bytesSent += n
}
// On 0x11, verify packet receipt size
err := dfu.on(DFUNotifPktRecvd, func(data []byte) error {
// Set bytes received to data returned by InfiniTime
dfu.bytesRecvd = int(binary.LittleEndian.Uint32(data))
if dfu.bytesRecvd != dfu.bytesSent {
return ErrDFUSizeMismatch
}
return nil
})
if err != nil {
return err
}
}
return nil
}
func (dfu *DFU) stepEight() error {
return dfu.ctrlPointChar.WriteValue(DFUCmdValidate, nil)
}
func (dfu *DFU) stepNine() error {
return dfu.ctrlPointChar.WriteValue(DFUCmdActivateReset, btOptsCmd)
}