From aabb4279a0fa683ebfae15601db8a7ab1a5944e0 Mon Sep 17 00:00:00 2001 From: Gabor Date: Thu, 4 Apr 2024 13:13:44 +0200 Subject: [PATCH 01/43] Introduce callback for firmware update messages --- src/dividat-driver/firmware/main.go | 53 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 19d51807..fb7634ef 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -42,7 +42,12 @@ func Command(flags []string) { os.Exit(1) } - err = Update(context.Background(), file, deviceSerial, configuredAddr) + onProgress := func(progressMsg string) { + fmt.Println(progressMsg) + } + + Update(context.Background(), file, deviceSerial, configuredAddr, onProgress) + if err != nil { fmt.Println(err.Error()) fmt.Println() @@ -51,21 +56,23 @@ func Command(flags []string) { } } +type OnProgress func(msg string) + // Firmware update workhorse -func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, configuredAddr *string) (fail error) { +func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, configuredAddr *string, onProgress OnProgress) (fail error) { // 1: Find address of a Senso in normal mode var controllerHost string if *configuredAddr != "" { // Use specified controller address controllerHost = *configuredAddr - fmt.Printf("Using specified controller address '%s'.\n", controllerHost) + onProgress(fmt.Sprintf("Using specified controller address '%s'.", controllerHost)) } else { // Discover controller address via mDNS ctx, cancel := context.WithTimeout(parentCtx, 15*time.Second) - discoveredAddr, err := discover("_sensoControl._tcp", deviceSerial, ctx) + discoveredAddr, err := discover("_sensoControl._tcp", deviceSerial, ctx, onProgress) cancel() if err != nil { - fmt.Printf("Error: %s\n", err) + onProgress(fmt.Sprintf("Error: %s", err)) } else { controllerHost = discoveredAddr } @@ -73,15 +80,15 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co // 2: Switch the Senso to bootloader mode if controllerHost != "" { - err := sendDfuCommand(controllerHost, controllerPort) + err := sendDfuCommand(controllerHost, controllerPort, onProgress) if err != nil { // Log the failure, but continue anyway, as the Senso might have been left in // bootloader mode when a previous update process failed. Not all versions of // the firmware automtically exit from the bootloader mode upon restart. - fmt.Printf("Could not send DFU command to Senso at %s: %s\n", controllerHost, err) + onProgress(fmt.Sprintf("Could not send DFU command to Senso at %s: %s", controllerHost, err)) } } else { - fmt.Printf("Could not discover a Senso in regular mode, now trying to detect a Senso already in bootloader mode.\n") + onProgress("Could not discover a Senso in regular mode, now trying to detect a Senso already in bootloader mode.") } // 3: Find address of Senso in bootloader mode @@ -90,7 +97,7 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co dfuHost = *configuredAddr } else { ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) - discoveredAddr, err := discover("_sensoUpdate._udp", deviceSerial, ctx) + discoveredAddr, err := discover("_sensoUpdate._udp", deviceSerial, ctx, onProgress) cancel() if err != nil { // Up to firmware 2.0.0.0 the bootloader advertised itself with the same @@ -100,15 +107,17 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co // We do need to rediscover, as the legacy device may still just have // restarted into the bootloader and obtained a new IP address. ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) - legacyDiscoveredAddr, err := discover("_sensoControl._tcp", deviceSerial, ctx) + legacyDiscoveredAddr, err := discover("_sensoControl._tcp", deviceSerial, ctx, onProgress) cancel() if err == nil { dfuHost = legacyDiscoveredAddr } else if controllerHost != "" { - fmt.Printf("Could not discover update service, trying to fall back to previous discovery %s.\n", controllerHost) + onProgress(fmt.Sprintf("Could not discover update service, trying to fall back to previous discovery %s.", controllerHost)) dfuHost = controllerHost } else { - fail = fmt.Errorf("Could not find any Senso bootloader to transmit firmware to: %s", err) + msg := fmt.Sprintf("Could not find any Senso bootloader to transmit firmware to: %s", err) + onProgress(msg) + fail = fmt.Errorf(msg) return } } else { @@ -118,17 +127,17 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co // 4: Transmit firmware via TFTP time.Sleep(5 * time.Second) // Wait to ensure proper TFTP startup - err := putTFTP(dfuHost, tftpPort, image) + err := putTFTP(dfuHost, tftpPort, image, onProgress) if err != nil { fail = err return } - fmt.Println("Success! Firmware transmitted to Senso.") + onProgress("Success! Firmware transmitted to Senso.") return } -func sendDfuCommand(host string, port string) error { +func sendDfuCommand(host string, port string, onProgress OnProgress) error { // Header const PROTOCOL_VERSION = 0x00 const NUMOFBLOCKS = 0x01 @@ -158,12 +167,12 @@ func sendDfuCommand(host string, port string) error { return fmt.Errorf("Could not send DFU command: %v", err) } - fmt.Printf("Sent DFU command to %s:%s.\n", host, port) + onProgress(fmt.Sprintf("Sent DFU command to %s:%s.", host, port)) return nil } -func putTFTP(host string, port string, image io.Reader) error { +func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) error { client, err := tftp.NewClient(fmt.Sprintf("%s:%s", host, port)) if err != nil { return fmt.Errorf("Could not create tftp client: %v", err) @@ -176,18 +185,18 @@ func putTFTP(host string, port string, image io.Reader) error { if err != nil { return fmt.Errorf("Could not read from file: %v", err) } - fmt.Printf("%d bytes sent\n", n) + onProgress(fmt.Sprintf("%d bytes sent", n)) return nil } -func discover(service string, deviceSerial *string, ctx context.Context) (addr string, err error) { +func discover(service string, deviceSerial *string, ctx context.Context, onProgress OnProgress) (addr string, err error) { resolver, err := zeroconf.NewResolver(nil) if err != nil { err = fmt.Errorf("Initializing discovery failed: %v", err) return } - fmt.Printf("Starting discovery: %s\n", service) + onProgress(fmt.Sprintf("Starting discovery: %s", service)) entries := make(chan *zeroconf.ServiceEntry) @@ -220,7 +229,7 @@ func discover(service string, deviceSerial *string, ctx context.Context) (addr s } for _, addrCandidate := range entry.AddrIPv4 { if addrCandidate.String() == "0.0.0.0" { - fmt.Printf("Skipping discovered address 0.0.0.0 for %s.\n", serial) + onProgress(fmt.Sprintf("Skipping discovered address 0.0.0.0 for %s.", serial)) } else { devices[serial] = append(devices[serial], addrCandidate.String()) } @@ -234,7 +243,7 @@ func discover(service string, deviceSerial *string, ctx context.Context) (addr s } else if len(devices) == 1 { for serial, addrs := range devices { addr = addrs[0] - fmt.Printf("Discovered %s at %v, using %s.\n", serial, addrs, addr) + onProgress(fmt.Sprintf("Discovered %s at %v, using %s.", serial, addrs, addr)) return } } else { From 333ac8d33080c2bd014e6c7d49ae7885fff2a438 Mon Sep 17 00:00:00 2001 From: Gabor Date: Thu, 4 Apr 2024 13:13:45 +0200 Subject: [PATCH 02/43] Receive firmware update command and send progress messages --- src/dividat-driver/senso/update_firmware.go | 49 ++++++++++++++++++ src/dividat-driver/senso/websocket.go | 55 ++++++++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/dividat-driver/senso/update_firmware.go diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go new file mode 100644 index 00000000..8f481287 --- /dev/null +++ b/src/dividat-driver/senso/update_firmware.go @@ -0,0 +1,49 @@ +package senso + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + + "github.com/dividat/driver/src/dividat-driver/firmware" +) + +type OnUpdate func(msg FirmwareUpdateMessage) + +// Disconnect from current connection +func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpdate OnUpdate) { + handle.log.Info("Processing firmware update request.") + if handle.cancelCurrentConnection != nil { + handle.cancelCurrentConnection() + } + image, err := decodeImage(command.Image) + if err != nil { + msg := fmt.Sprintf("Error decoding base64 string: %v", err) + onUpdate(FirmwareUpdateMessage{FirmwareUpdateFailure: &msg}) + handle.log.Error(msg) + } + + onProgress := func(progressMsg string) { + onUpdate(FirmwareUpdateMessage{FirmwareUpdateProgress: &progressMsg}) + } + + err = firmware.Update(context.Background(), image, nil, &command.Address, onProgress) + if err != nil { + failureMsg := fmt.Sprintf("Failed to update firmware: %v", err) + onUpdate(FirmwareUpdateMessage{FirmwareUpdateFailure: &failureMsg}) + handle.log.Error(failureMsg) + } else { + successMsg := "Firmware successfully transmitted." + onUpdate(FirmwareUpdateMessage{FirmwareUpdateSuccess: &successMsg}) + } +} + +func decodeImage(base64Str string) (io.Reader, error) { + data, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + return nil, err + } + return bytes.NewReader(data), nil +} diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index 4d0bb72a..6a0ae1e3 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -24,6 +24,7 @@ type Command struct { *Disconnect *Discover + *UpdateFirmware } func prettyPrintCommand(command Command) string { @@ -55,6 +56,11 @@ type Discover struct { Duration int `json:"duration"` } +type UpdateFirmware struct { + Address string `json:"address"` + Image string `json:"image"` +} + // UnmarshalJSON implements encoding/json Unmarshaler interface func (command *Command) UnmarshalJSON(data []byte) error { @@ -85,6 +91,11 @@ func (command *Command) UnmarshalJSON(data []byte) error { return err } + } else if temp.Type == "UpdateFirmware" { + err := json.Unmarshal(data, &command.UpdateFirmware) + if err != nil { + return err + } } else { return errors.New("can not decode unknown command") } @@ -95,8 +106,8 @@ func (command *Command) UnmarshalJSON(data []byte) error { // Message that can be sent to Play type Message struct { *Status - - Discovered *zeroconf.ServiceEntry + Discovered *zeroconf.ServiceEntry + FirmwareUpdateMessage *FirmwareUpdateMessage } // Status is a message containing status information @@ -104,6 +115,12 @@ type Status struct { Address *string } +type FirmwareUpdateMessage struct { + FirmwareUpdateProgress *string + FirmwareUpdateSuccess *string + FirmwareUpdateFailure *string +} + // MarshalJSON ipmlements JSON encoder for messages func (message *Message) MarshalJSON() ([]byte, error) { if message.Status != nil { @@ -126,6 +143,34 @@ func (message *Message) MarshalJSON() ([]byte, error) { IP: append(message.Discovered.AddrIPv4, message.Discovered.AddrIPv6...), }) + } else if message.FirmwareUpdateMessage != nil { + fwUpdate := struct { + Type string `json:"type"` + Message string `json:"message"` + }{} + + firmwareUpdateMessage := *message.FirmwareUpdateMessage + + if firmwareUpdateMessage.FirmwareUpdateProgress != nil { + + fwUpdate.Type = "FirmwareUpdateProgress" + fwUpdate.Message = *firmwareUpdateMessage.FirmwareUpdateProgress + + } else if firmwareUpdateMessage.FirmwareUpdateFailure != nil { + + fwUpdate.Type = "FirmwareUpdateFailure" + fwUpdate.Message = *firmwareUpdateMessage.FirmwareUpdateFailure + + } else if firmwareUpdateMessage.FirmwareUpdateSuccess != nil { + + fwUpdate.Type = "FirmwareUpdateSuccess" + fwUpdate.Message = *firmwareUpdateMessage.FirmwareUpdateSuccess + + } else { + return nil, errors.New("could not marshal firmware update message") + } + + return json.Marshal(fwUpdate) } return nil, errors.New("could not marshal message") @@ -293,6 +338,12 @@ func (handle *Handle) dispatchCommand(ctx context.Context, log *logrus.Entry, co return nil + } else if command.UpdateFirmware != nil { + handle.ProcessFirmwareUpdateRequest(*command.UpdateFirmware, func(msg FirmwareUpdateMessage) { + sendMessage(Message{ + FirmwareUpdateMessage: &msg, + }) + }) } return nil } From c3e331d3126748431f01e4383165089dfc827bf5 Mon Sep 17 00:00:00 2001 From: Gabor Date: Thu, 4 Apr 2024 13:13:46 +0200 Subject: [PATCH 03/43] Ignore commands while firmware update is in progress --- src/dividat-driver/senso/main.go | 5 +++++ src/dividat-driver/senso/update_firmware.go | 15 +++++++++++++++ src/dividat-driver/senso/websocket.go | 15 +++++++++++---- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/dividat-driver/senso/main.go b/src/dividat-driver/senso/main.go index 5064f0b7..4b32652e 100644 --- a/src/dividat-driver/senso/main.go +++ b/src/dividat-driver/senso/main.go @@ -20,6 +20,9 @@ type Handle struct { cancelCurrentConnection context.CancelFunc connectionChangeMutex *sync.Mutex + firmwareUpdateInProgress bool + firmwareUpdateMutex *sync.Mutex + log *logrus.Entry } @@ -32,6 +35,8 @@ func New(ctx context.Context, log *logrus.Entry) *Handle { handle.log = log handle.connectionChangeMutex = &sync.Mutex{} + handle.firmwareUpdateMutex = &sync.Mutex{} + handle.firmwareUpdateInProgress = false // PubSub broker handle.broker = pubsub.New(32) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index 8f481287..ad4a3b2a 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -12,9 +12,23 @@ import ( type OnUpdate func(msg FirmwareUpdateMessage) +func (handle *Handle) isUpdatingFirmware() bool { + handle.firmwareUpdateMutex.Lock() + state := handle.firmwareUpdateInProgress + handle.firmwareUpdateMutex.Unlock() + return state +} + +func (handle *Handle) setUpdatingFirmware(state bool) { + handle.firmwareUpdateMutex.Lock() + handle.firmwareUpdateInProgress = state + handle.firmwareUpdateMutex.Unlock() +} + // Disconnect from current connection func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpdate OnUpdate) { handle.log.Info("Processing firmware update request.") + handle.setUpdatingFirmware(true) if handle.cancelCurrentConnection != nil { handle.cancelCurrentConnection() } @@ -38,6 +52,7 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpd successMsg := "Firmware successfully transmitted." onUpdate(FirmwareUpdateMessage{FirmwareUpdateSuccess: &successMsg}) } + handle.setUpdatingFirmware(false) } func decodeImage(base64Str string) (io.Reader, error) { diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index 6a0ae1e3..c89b5f7e 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -265,6 +265,11 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + if handle.isUpdatingFirmware() { + handle.log.Info("Firmware update in progress, ignoring websocket message.") + continue + } + if messageType == websocket.BinaryMessage { handle.broker.TryPub(msg, "tx") @@ -339,11 +344,13 @@ func (handle *Handle) dispatchCommand(ctx context.Context, log *logrus.Entry, co return nil } else if command.UpdateFirmware != nil { - handle.ProcessFirmwareUpdateRequest(*command.UpdateFirmware, func(msg FirmwareUpdateMessage) { - sendMessage(Message{ - FirmwareUpdateMessage: &msg, + go func(){ + handle.ProcessFirmwareUpdateRequest(*command.UpdateFirmware, func(msg FirmwareUpdateMessage) { + sendMessage(Message{ + FirmwareUpdateMessage: &msg, + }) }) - }) + }() } return nil } From ccd94c9c5fcb9bc7e6cf31ade5cb0ab3b0271b78 Mon Sep 17 00:00:00 2001 From: Gabor Date: Thu, 4 Apr 2024 13:13:47 +0200 Subject: [PATCH 04/43] Add some more firmware update progress messages --- src/dividat-driver/firmware/main.go | 4 ++++ src/dividat-driver/senso/update_firmware.go | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index fb7634ef..38a04b67 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -97,6 +97,7 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co dfuHost = *configuredAddr } else { ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) + onProgress("Looking for Senso in bootloader mode.") discoveredAddr, err := discover("_sensoUpdate._udp", deviceSerial, ctx, onProgress) cancel() if err != nil { @@ -111,6 +112,7 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co cancel() if err == nil { dfuHost = legacyDiscoveredAddr + onProgress("Senso discovered via _sensoControl._tcp") } else if controllerHost != "" { onProgress(fmt.Sprintf("Could not discover update service, trying to fall back to previous discovery %s.", controllerHost)) dfuHost = controllerHost @@ -122,9 +124,11 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co } } else { dfuHost = discoveredAddr + onProgress("Senso discovered via _sensoUpdate._udp") } } + onProgress("Preparing to transmit firmware.") // 4: Transmit firmware via TFTP time.Sleep(5 * time.Second) // Wait to ensure proper TFTP startup err := putTFTP(dfuHost, tftpPort, image, onProgress) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index ad4a3b2a..deed05ac 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -30,6 +30,8 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpd handle.log.Info("Processing firmware update request.") handle.setUpdatingFirmware(true) if handle.cancelCurrentConnection != nil { + msg := "Disconnecting from the Senso" + onUpdate(FirmwareUpdateMessage{FirmwareUpdateProgress: &msg}) handle.cancelCurrentConnection() } image, err := decodeImage(command.Image) From 71b4137921c9d8db29d0cf360a018aa61a919c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 05/43] Use libp2p/zeroconf This is a fork of grandcat/zeroconf. It is better maintained and running the discovery command seems more robust with this version. --- go.mod | 2 +- go.sum | 42 ++++++++++++--------------- src/dividat-driver/firmware/main.go | 17 ++++------- src/dividat-driver/senso/discovery.go | 18 +++++------- src/dividat-driver/senso/websocket.go | 2 +- 5 files changed, 32 insertions(+), 49 deletions(-) diff --git a/go.mod b/go.mod index 70df1ae5..5d6f1909 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95 github.com/gorilla/websocket v1.4.2 - github.com/grandcat/zeroconf v1.0.0 github.com/kardianos/service v1.2.0 + github.com/libp2p/zeroconf/v2 v2.2.0 github.com/pin/tftp v2.1.0+incompatible github.com/sirupsen/logrus v1.8.1 go.bug.st/serial v1.1.3 diff --git a/go.sum b/go.sum index 19f68e1e..25886269 100644 --- a/go.sum +++ b/go.sum @@ -13,16 +13,14 @@ github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95 h1:OM0MnUcXBysj7ZtXvThV github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95/go.mod h1:8hHvF8DlEq5kE3KWOsZQezdWq1OTOVxZArZMscS954E= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= -github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= -github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= -github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q= +github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= +github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/pin/tftp v2.1.0+incompatible h1:Yng4J7jv6lOc6IF4XoB5mnd3P7ZrF60XQq+my3FAMus= github.com/pin/tftp v2.1.0+incompatible/go.mod h1:xVpZOMCXTy+A5QMjEVN0Glwa1sUvaJhFXbr/aAxuxGY= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= @@ -33,27 +31,23 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= go.bug.st/serial v1.1.3 h1:YEBxJa9pKS9Wdg46B/jiaKbvvbUrjhZZZITfJHEJhaE= go.bug.st/serial v1.1.3/go.mod h1:8TT7u/SwwNIpJ8QaG4s+HTjFt9ReXs2cdOU7ZEk50Dk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk= +golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 h1:kHSDPqCtsHZOg0nVylfTo20DDhE9gG4Y0jn7hKQ0QAM= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 38a04b67..73252245 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "github.com/grandcat/zeroconf" + "github.com/libp2p/zeroconf/v2" "github.com/pin/tftp" ) @@ -194,21 +194,14 @@ func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) e } func discover(service string, deviceSerial *string, ctx context.Context, onProgress OnProgress) (addr string, err error) { - resolver, err := zeroconf.NewResolver(nil) - if err != nil { - err = fmt.Errorf("Initializing discovery failed: %v", err) - return - } - onProgress(fmt.Sprintf("Starting discovery: %s", service)) entries := make(chan *zeroconf.ServiceEntry) - err = resolver.Browse(ctx, service, "local.", entries) - if err != nil { - err = fmt.Errorf("Browsing failed: %v", err) - return - } + go func() { + browseErr := zeroconf.Browse(ctx, service, "local.", entries) + onProgress(fmt.Sprintf("Failed to initialize browsing %v", browseErr)) + }() devices := make(map[string][]string) entriesWithoutSerial := 0 diff --git a/src/dividat-driver/senso/discovery.go b/src/dividat-driver/senso/discovery.go index d173f65d..1b563d4c 100644 --- a/src/dividat-driver/senso/discovery.go +++ b/src/dividat-driver/senso/discovery.go @@ -2,8 +2,7 @@ package senso import ( "context" - - "github.com/grandcat/zeroconf" + "github.com/libp2p/zeroconf/v2" ) // Discover Sensos for a certain duration @@ -11,20 +10,17 @@ func (handle *Handle) Discover(ctx context.Context) chan *zeroconf.ServiceEntry log := handle.log - resolver, err := zeroconf.NewResolver(nil) - if err != nil { - log.WithError(err).Error("Initializing discovery failed.") - } - log.Debug("Initialized discovery.") // create an intermediary channel for logging discoveries and handling the case when there is no reader entries := make(chan *zeroconf.ServiceEntry) - err = resolver.Browse(ctx, "_sensoControl._tcp", "local.", entries) - if err != nil { - log.WithError(err).Error("Browsing failed.") - } + go func() { + err := zeroconf.Browse(ctx, "_sensoControl._tcp", "local.", entries) + if err != nil { + log.WithError(err).Error("Browsing failed.") + } + }() return entries diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index c89b5f7e..90304a4c 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -10,7 +10,7 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/grandcat/zeroconf" + "github.com/libp2p/zeroconf/v2" "github.com/sirupsen/logrus" ) From c3f027f73089c6892a86b737ffcd2446facca480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 06/43] Report firmware update errors on the command line --- src/dividat-driver/firmware/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 73252245..367c3d1c 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -46,7 +46,7 @@ func Command(flags []string) { fmt.Println(progressMsg) } - Update(context.Background(), file, deviceSerial, configuredAddr, onProgress) + err = Update(context.Background(), file, deviceSerial, configuredAddr, onProgress) if err != nil { fmt.Println(err.Error()) From fb1a47255359e36d0141294488a3a64de8087faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 07/43] Configure exponential backoff on TFTP client --- src/dividat-driver/firmware/main.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 367c3d1c..6c55e140 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -7,6 +7,7 @@ import ( "flag" "fmt" "io" + "math" "net" "os" "strings" @@ -181,6 +182,22 @@ func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) e if err != nil { return fmt.Errorf("Could not create tftp client: %v", err) } + + maxRetries := 5 + client.SetRetries(maxRetries) + + expDelay := func(attempt int) time.Duration { + exp := math.Pow(2, float64(attempt)) + exp = math.Min(exp, 60) + return time.Duration(int(exp)) * time.Second + } + + client.SetBackoff(func(attempt int) time.Duration { + a1 := attempt + 1 + msg := fmt.Sprintf("Failed on attempt %d, retrying in %v", a1, expDelay(a1)) + onProgress(msg) + return expDelay(attempt) + }) rf, err := client.Send("controller-app.bin", "octet") if err != nil { return fmt.Errorf("Could not create send connection: %v", err) From 262aaddb6b28844080e331692b54c51457bc04d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 08/43] Add more messages --- src/dividat-driver/firmware/main.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 6c55e140..84dc5908 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -129,7 +129,7 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co } } - onProgress("Preparing to transmit firmware.") + onProgress("Waiting 5 seconds to ensure proper TFTP startup") // 4: Transmit firmware via TFTP time.Sleep(5 * time.Second) // Wait to ensure proper TFTP startup err := putTFTP(dfuHost, tftpPort, image, onProgress) @@ -178,6 +178,7 @@ func sendDfuCommand(host string, port string, onProgress OnProgress) error { } func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) error { + onProgress("Creating TFTP client") client, err := tftp.NewClient(fmt.Sprintf("%s:%s", host, port)) if err != nil { return fmt.Errorf("Could not create tftp client: %v", err) @@ -198,10 +199,13 @@ func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) e onProgress(msg) return expDelay(attempt) }) + + onProgress("Preparing transmission") rf, err := client.Send("controller-app.bin", "octet") if err != nil { return fmt.Errorf("Could not create send connection: %v", err) } + onProgress("Transmitting...") n, err := rf.ReadFrom(image) if err != nil { return fmt.Errorf("Could not read from file: %v", err) From 94b819a57baf8bb70375236b5cba8590852c5102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 09/43] Wait 10 seconds for tcp connection teardown Otherwise SendDFU might fail, as cancelling the connection doesn't take effect immediately. --- src/dividat-driver/senso/update_firmware.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index deed05ac..02a75482 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "fmt" "io" + "time" "github.com/dividat/driver/src/dividat-driver/firmware" ) @@ -29,11 +30,16 @@ func (handle *Handle) setUpdatingFirmware(state bool) { func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpdate OnUpdate) { handle.log.Info("Processing firmware update request.") handle.setUpdatingFirmware(true) + + onProgress := func(progressMsg string) { + onUpdate(FirmwareUpdateMessage{FirmwareUpdateProgress: &progressMsg}) + } + if handle.cancelCurrentConnection != nil { - msg := "Disconnecting from the Senso" - onUpdate(FirmwareUpdateMessage{FirmwareUpdateProgress: &msg}) + onProgress("Disconnecting from the Senso") handle.cancelCurrentConnection() } + image, err := decodeImage(command.Image) if err != nil { msg := fmt.Sprintf("Error decoding base64 string: %v", err) @@ -41,10 +47,9 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpd handle.log.Error(msg) } - onProgress := func(progressMsg string) { - onUpdate(FirmwareUpdateMessage{FirmwareUpdateProgress: &progressMsg}) - } + onProgress("Waiting 10 seconds for connection teardown") + time.Sleep(10 * time.Second) err = firmware.Update(context.Background(), image, nil, &command.Address, onProgress) if err != nil { failureMsg := fmt.Sprintf("Failed to update firmware: %v", err) From 0e9025af42f997edfe6ec201fc034e8e07cb1b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 10/43] Make firmware update commands specify serial instead of address --- src/dividat-driver/senso/update_firmware.go | 2 +- src/dividat-driver/senso/websocket.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index 02a75482..2c31d939 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -50,7 +50,7 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpd onProgress("Waiting 10 seconds for connection teardown") time.Sleep(10 * time.Second) - err = firmware.Update(context.Background(), image, nil, &command.Address, onProgress) + err = firmware.Update(context.Background(), image, &command.SerialNumber, nil, onProgress) if err != nil { failureMsg := fmt.Sprintf("Failed to update firmware: %v", err) onUpdate(FirmwareUpdateMessage{FirmwareUpdateFailure: &failureMsg}) diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index 90304a4c..67239c71 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -57,8 +57,8 @@ type Discover struct { } type UpdateFirmware struct { - Address string `json:"address"` - Image string `json:"image"` + SerialNumber string `json:"serialNumber"` + Image string `json:"image"` } // UnmarshalJSON implements encoding/json Unmarshaler interface From 85bc2808b2d712a70f140a857ec1dd06b658b2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 11/43] Check for null when dereferencing address --- src/dividat-driver/firmware/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 84dc5908..acf69fc6 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -63,7 +63,7 @@ type OnProgress func(msg string) func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, configuredAddr *string, onProgress OnProgress) (fail error) { // 1: Find address of a Senso in normal mode var controllerHost string - if *configuredAddr != "" { + if configuredAddr != nil && *configuredAddr != "" { // Use specified controller address controllerHost = *configuredAddr onProgress(fmt.Sprintf("Using specified controller address '%s'.", controllerHost)) @@ -94,7 +94,7 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co // 3: Find address of Senso in bootloader mode var dfuHost string - if *configuredAddr != "" { + if configuredAddr != nil && *configuredAddr != "" { dfuHost = *configuredAddr } else { ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) From a23e6c34f089854bb45f57e57136e15fb489e4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 12/43] Allow Discovery and GetStatus commands while updating firmware The do not interfere with the firmware update, but can be relevant for the client. --- src/dividat-driver/senso/websocket.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index 67239c71..97fa0e76 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -265,12 +265,13 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if handle.isUpdatingFirmware() { - handle.log.Info("Firmware update in progress, ignoring websocket message.") - continue - } - if messageType == websocket.BinaryMessage { + + if handle.isUpdatingFirmware() { + handle.log.Info("Firmware update in progress, ignoring Senso command.") + continue + } + handle.broker.TryPub(msg, "tx") } else if messageType == websocket.TextMessage { @@ -283,6 +284,11 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { } log.WithField("command", prettyPrintCommand(command)).Debug("Received command.") + if handle.isUpdatingFirmware() && (command.GetStatus == nil || command.Discover == nil) { + handle.log.Info("Firmware update in progress, ignoring UpdateFirmware command.") + continue + } + err := handle.dispatchCommand(ctx, log, command, sendMessage) if err != nil { return From 4201904cb5da062b37efd052668b3d871370b80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 13/43] Wait 15 seconds for tftp startup --- src/dividat-driver/firmware/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index acf69fc6..36d8a56c 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -129,9 +129,9 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co } } - onProgress("Waiting 5 seconds to ensure proper TFTP startup") + onProgress("Waiting 15 seconds to ensure proper TFTP startup") // 4: Transmit firmware via TFTP - time.Sleep(5 * time.Second) // Wait to ensure proper TFTP startup + time.Sleep(15 * time.Second) // Wait to ensure proper TFTP startup err := putTFTP(dfuHost, tftpPort, image, onProgress) if err != nil { fail = err From 03b962d74561ee234be5e016be6fc3df5b698bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 14/43] Send DFU with exponential backoff --- src/dividat-driver/firmware/main.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 36d8a56c..603867b1 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/cenkalti/backoff" "github.com/libp2p/zeroconf/v2" "github.com/pin/tftp" ) @@ -81,7 +82,19 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co // 2: Switch the Senso to bootloader mode if controllerHost != "" { - err := sendDfuCommand(controllerHost, controllerPort, onProgress) + + trySendDfu := func() error { + err := sendDfuCommand(controllerHost, controllerPort, onProgress) + return err + } + + backoffStrategy := backoff.NewExponentialBackOff() + backoffStrategy.MaxElapsedTime = 30 * time.Second + backoffStrategy.MaxInterval = 10 * time.Second + err := backoff.RetryNotify(trySendDfu, backoffStrategy, func(e error, d time.Duration) { + onProgress(fmt.Sprintf("%v\nRetrying in %v", e, d)) + }) + if err != nil { // Log the failure, but continue anyway, as the Senso might have been left in // bootloader mode when a previous update process failed. Not all versions of From bd1fa13c9350e9a9473c864f0cea29bd524f2c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 15/43] Do not abort discovery when device serials don't match The discovery function gave up immediately when it encountered a serial that didn't match the expected one, instead of continuing to look. This is problematic if there are multiple Sensos on the network. --- src/dividat-driver/firmware/main.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 603867b1..b3ec4b15 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -239,10 +239,9 @@ func discover(service string, deviceSerial *string, ctx context.Context, onProgr devices := make(map[string][]string) entriesWithoutSerial := 0 - select { - case entry := <-entries: + for entry := range entries { if entry == nil { - break + continue } var serial string @@ -255,14 +254,20 @@ func discover(service string, deviceSerial *string, ctx context.Context, onProgr serial = fmt.Sprintf("UNKNOWN-%d", entriesWithoutSerial) } } - if deviceSerial != nil && serial != *deviceSerial { - break + isMatchingSerial := deviceSerial != nil && serial == *deviceSerial + if !isMatchingSerial { + continue } for _, addrCandidate := range entry.AddrIPv4 { if addrCandidate.String() == "0.0.0.0" { onProgress(fmt.Sprintf("Skipping discovered address 0.0.0.0 for %s.", serial)) } else { - devices[serial] = append(devices[serial], addrCandidate.String()) + serviceAddr := addrCandidate.String() + if isMatchingSerial { + return serviceAddr, nil + } else { + devices[serial] = append(devices[serial], serviceAddr) + } } } } From c7608d7ff76ea1cdbb47d48a1f56cf3597b9122a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 16/43] Refactor discovery code This helps simplify update logic, makes it easier to determine what mode the discovered Sensos are in, and simplifies including Sensos both in normal and bootloader mode in the discovery results sent to the client. --- src/dividat-driver/firmware/main.go | 190 +++++++------------------- src/dividat-driver/senso/discovery.go | 27 ---- src/dividat-driver/senso/websocket.go | 10 +- src/dividat-driver/service/main.go | 164 ++++++++++++++++++++++ 4 files changed, 220 insertions(+), 171 deletions(-) delete mode 100644 src/dividat-driver/senso/discovery.go create mode 100644 src/dividat-driver/service/main.go diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index b3ec4b15..7ac9d0bd 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -10,12 +10,12 @@ import ( "math" "net" "os" - "strings" "time" "github.com/cenkalti/backoff" - "github.com/libp2p/zeroconf/v2" "github.com/pin/tftp" + + "github.com/dividat/driver/src/dividat-driver/service" ) const tftpPort = "69" @@ -51,40 +51,50 @@ func Command(flags []string) { err = Update(context.Background(), file, deviceSerial, configuredAddr, onProgress) if err != nil { - fmt.Println(err.Error()) fmt.Println() - fmt.Println("Update failed. Try turning the Senso off and on, waiting for 30 seconds and then running this update tool again.") + fmt.Printf("Update failed: %v \n", err) os.Exit(1) } } type OnProgress func(msg string) -// Firmware update workhorse -func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, configuredAddr *string, onProgress OnProgress) (fail error) { - // 1: Find address of a Senso in normal mode - var controllerHost string +const tryPowerCycling = "Try turning the Senso off and on, waiting for 30 seconds and then running this update tool again." + +func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, configuredAddr *string, onProgress OnProgress) error { + var target service.Service + if configuredAddr != nil && *configuredAddr != "" { - // Use specified controller address - controllerHost = *configuredAddr - onProgress(fmt.Sprintf("Using specified controller address '%s'.", controllerHost)) + onProgress(fmt.Sprintf("Using specified address %s", *configuredAddr)) + match := service.Find(parentCtx, 15*time.Second, service.AddressFilter(*configuredAddr)) + if match == nil { + return fmt.Errorf("Failed to find Senso with address %s.\n%s", *configuredAddr, tryPowerCycling) + } + target = *match + } else if deviceSerial != nil && *deviceSerial != "" { + onProgress(fmt.Sprintf("Using specified serial %s", *deviceSerial)) + match := service.Find(parentCtx, 15*time.Second, service.SerialNumberFilter(*deviceSerial)) + if match == nil { + return fmt.Errorf("Failed to find Senso with serial number %s.\n%s", *configuredAddr, tryPowerCycling) + } + target = *match } else { // Discover controller address via mDNS - ctx, cancel := context.WithTimeout(parentCtx, 15*time.Second) - discoveredAddr, err := discover("_sensoControl._tcp", deviceSerial, ctx, onProgress) - cancel() - if err != nil { - onProgress(fmt.Sprintf("Error: %s", err)) + onProgress("Discovering sensos") + services := service.List(parentCtx, 15*time.Second) + if len(services) == 1 { + target = services[0] + onProgress(fmt.Sprintf("Discovered Senso: %s (%s)", target.Text.Serial, target.Address)) + } else if len(services) == 0 { + return fmt.Errorf("Could not find any Sensos.\n%s", tryPowerCycling) } else { - controllerHost = discoveredAddr + return fmt.Errorf("discovered multiple Sensos: %v, please specify a serial or IP", services) } } - // 2: Switch the Senso to bootloader mode - if controllerHost != "" { - + if !service.IsDfuService(target) { trySendDfu := func() error { - err := sendDfuCommand(controllerHost, controllerPort, onProgress) + err := sendDfuCommand(target.Address, controllerPort, onProgress) return err } @@ -96,63 +106,34 @@ func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, co }) if err != nil { - // Log the failure, but continue anyway, as the Senso might have been left in - // bootloader mode when a previous update process failed. Not all versions of - // the firmware automtically exit from the bootloader mode upon restart. - onProgress(fmt.Sprintf("Could not send DFU command to Senso at %s: %s", controllerHost, err)) + return fmt.Errorf("could not send DFU command to Senso at %s: %s", target.Address, err) } - } else { - onProgress("Could not discover a Senso in regular mode, now trying to detect a Senso already in bootloader mode.") - } - // 3: Find address of Senso in bootloader mode - var dfuHost string - if configuredAddr != nil && *configuredAddr != "" { - dfuHost = *configuredAddr - } else { - ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) - onProgress("Looking for Senso in bootloader mode.") - discoveredAddr, err := discover("_sensoUpdate._udp", deviceSerial, ctx, onProgress) - cancel() - if err != nil { - // Up to firmware 2.0.0.0 the bootloader advertised itself with the same - // service identifier as the application level firmware. To support such - // legacy devices, we look for `_sensoControl` again at this point, if - // the other service has not been found. - // We do need to rediscover, as the legacy device may still just have - // restarted into the bootloader and obtained a new IP address. - ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) - legacyDiscoveredAddr, err := discover("_sensoControl._tcp", deviceSerial, ctx, onProgress) - cancel() - if err == nil { - dfuHost = legacyDiscoveredAddr - onProgress("Senso discovered via _sensoControl._tcp") - } else if controllerHost != "" { - onProgress(fmt.Sprintf("Could not discover update service, trying to fall back to previous discovery %s.", controllerHost)) - dfuHost = controllerHost - } else { - msg := fmt.Sprintf("Could not find any Senso bootloader to transmit firmware to: %s", err) - onProgress(msg) - fail = fmt.Errorf(msg) - return - } - } else { - dfuHost = discoveredAddr - onProgress("Senso discovered via _sensoUpdate._udp") + onProgress("Looking for senso in bootloader mode") + dfuService := service.Find(parentCtx, 30*time.Second, func(discovered service.Service) bool { + return service.SerialNumberFilter(target.Text.Serial)(discovered) && service.IsDfuService(discovered) + }) + + if dfuService == nil { + return fmt.Errorf("Could not rediscover Senso in bootloader mode.\n%s", tryPowerCycling) } + + target = *dfuService + onProgress(fmt.Sprintf("Re-discovered Senso in bootloader mode at %s", target.Address)) + onProgress("Waiting 15 seconds to ensure proper TFTP startup") + // Wait to ensure proper TFTP startup + time.Sleep(15 * time.Second) + } else { + onProgress("Senso discovered in bootloader mode") } - onProgress("Waiting 15 seconds to ensure proper TFTP startup") - // 4: Transmit firmware via TFTP - time.Sleep(15 * time.Second) // Wait to ensure proper TFTP startup - err := putTFTP(dfuHost, tftpPort, image, onProgress) + err := putTFTP(target.Address, tftpPort, image, onProgress) if err != nil { - fail = err - return + return err } onProgress("Success! Firmware transmitted to Senso.") - return + return nil } func sendDfuCommand(host string, port string, onProgress OnProgress) error { @@ -226,74 +207,3 @@ func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) e onProgress(fmt.Sprintf("%d bytes sent", n)) return nil } - -func discover(service string, deviceSerial *string, ctx context.Context, onProgress OnProgress) (addr string, err error) { - onProgress(fmt.Sprintf("Starting discovery: %s", service)) - - entries := make(chan *zeroconf.ServiceEntry) - - go func() { - browseErr := zeroconf.Browse(ctx, service, "local.", entries) - onProgress(fmt.Sprintf("Failed to initialize browsing %v", browseErr)) - }() - - devices := make(map[string][]string) - entriesWithoutSerial := 0 - for entry := range entries { - if entry == nil { - continue - } - - var serial string - for ix, txt := range entry.Text { - if strings.HasPrefix(txt, "ser_no=") { - serial = cleanSerial(strings.TrimPrefix(txt, "ser_no=")) - break - } else if ix == len(entry.Text)-1 { - entriesWithoutSerial++ - serial = fmt.Sprintf("UNKNOWN-%d", entriesWithoutSerial) - } - } - isMatchingSerial := deviceSerial != nil && serial == *deviceSerial - if !isMatchingSerial { - continue - } - for _, addrCandidate := range entry.AddrIPv4 { - if addrCandidate.String() == "0.0.0.0" { - onProgress(fmt.Sprintf("Skipping discovered address 0.0.0.0 for %s.", serial)) - } else { - serviceAddr := addrCandidate.String() - if isMatchingSerial { - return serviceAddr, nil - } else { - devices[serial] = append(devices[serial], serviceAddr) - } - } - } - } - - if len(devices) == 0 && deviceSerial == nil { - err = fmt.Errorf("Could not find any devices for service %s.", service) - } else if len(devices) == 0 && deviceSerial != nil { - err = fmt.Errorf("Could not find Senso %s.", *deviceSerial) - } else if len(devices) == 1 { - for serial, addrs := range devices { - addr = addrs[0] - onProgress(fmt.Sprintf("Discovered %s at %v, using %s.", serial, addrs, addr)) - return - } - } else { - err = fmt.Errorf("Discovered multiple Sensos: %v. Please specify a serial or IP.", devices) - return - } - return -} - -func cleanSerial(serialStr string) string { - // Senso firmware up to 3.8.0 adds garbage at end of serial in mDNS - // entries due to improper string sizing. Because bootloader firmware - // will not be updated via Ethernet, the problem will stay around for a - // while and we clean up the serial here to produce readable output for - // older devices. - return strings.Split(serialStr, "\\000")[0] -} diff --git a/src/dividat-driver/senso/discovery.go b/src/dividat-driver/senso/discovery.go deleted file mode 100644 index 1b563d4c..00000000 --- a/src/dividat-driver/senso/discovery.go +++ /dev/null @@ -1,27 +0,0 @@ -package senso - -import ( - "context" - "github.com/libp2p/zeroconf/v2" -) - -// Discover Sensos for a certain duration -func (handle *Handle) Discover(ctx context.Context) chan *zeroconf.ServiceEntry { - - log := handle.log - - log.Debug("Initialized discovery.") - - // create an intermediary channel for logging discoveries and handling the case when there is no reader - entries := make(chan *zeroconf.ServiceEntry) - - go func() { - err := zeroconf.Browse(ctx, "_sensoControl._tcp", "local.", entries) - if err != nil { - log.WithError(err).Error("Browsing failed.") - } - }() - - return entries - -} diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index 97fa0e76..bf2187e9 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -12,6 +12,8 @@ import ( "github.com/gorilla/websocket" "github.com/libp2p/zeroconf/v2" "github.com/sirupsen/logrus" + + "github.com/dividat/driver/src/dividat-driver/service" ) // WEBSOCKET PROTOCOL @@ -329,14 +331,14 @@ func (handle *Handle) dispatchCommand(ctx context.Context, log *logrus.Entry, co discoveryCtx, _ := context.WithTimeout(ctx, time.Duration(command.Discover.Duration)*time.Second) - entries := handle.Discover(discoveryCtx) + entries := service.Scan(discoveryCtx) - go func(entries chan *zeroconf.ServiceEntry) { + go func(entries chan service.Service) { for entry := range entries { log.WithField("service", entry).Debug("Discovered service.") var message Message - message.Discovered = entry + message.Discovered = &entry.ServiceEntry err := sendMessage(message) if err != nil { @@ -350,7 +352,7 @@ func (handle *Handle) dispatchCommand(ctx context.Context, log *logrus.Entry, co return nil } else if command.UpdateFirmware != nil { - go func(){ + go func() { handle.ProcessFirmwareUpdateRequest(*command.UpdateFirmware, func(msg FirmwareUpdateMessage) { sendMessage(Message{ FirmwareUpdateMessage: &msg, diff --git a/src/dividat-driver/service/main.go b/src/dividat-driver/service/main.go new file mode 100644 index 00000000..44c49fcf --- /dev/null +++ b/src/dividat-driver/service/main.go @@ -0,0 +1,164 @@ +package service + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/libp2p/zeroconf/v2" +) + +type Service struct { + Text Text + Address string + ServiceEntry zeroconf.ServiceEntry +} + +func (s Service) String() string { + return fmt.Sprintf("{ Serial: %s, Address: %s}", s.Text.Serial, s.Address) +} + +type ServiceType string + +const ( + SensoUpdate ServiceType = "_sensoUpdate._udp" + SensoControl ServiceType = "_sensoControl._tcp" +) + +const ( + ApplicationMode = "Application" + BootloaderMode = "Bootloader" +) + +func scanForType(ctx context.Context, t ServiceType, results chan<- Service, wg *sync.WaitGroup) { + wg.Add(2) + // Zeroconf closes the channel on context cancellation, + // so we cannot share channels between multiple browse calls. + // Doing so would lead to panic as one instance would try to close + // a channel that was already closed by another instance. + localEntries := make(chan *zeroconf.ServiceEntry) + go func() { + defer wg.Done() + err := zeroconf.Browse(ctx, string(t), "local.", localEntries) + if err != nil { + fmt.Println("Discovery error:", err) + } + }() + + // Forward entries from localEntries to the main results channel + go func() { + defer wg.Done() + entriesWithoutSerial := 0 + for entry := range localEntries { + if entry != nil { + text := getText(*entry) + if text.Serial == "" { + text.Serial = fmt.Sprintf("UNKNOWN-%d", entriesWithoutSerial) + entriesWithoutSerial++ + } + var address string + if entry.AddrIPv4[0] != nil { + address = entry.AddrIPv4[0].String() + } else { + continue + } + results <- Service{ + Address: address, + Text: text, + ServiceEntry: *entry, + } + } + } + }() +} + +func Scan(ctx context.Context) chan Service { + var wg sync.WaitGroup + services := make(chan Service) + scanForType(ctx, SensoUpdate, services, &wg) + scanForType(ctx, SensoControl, services, &wg) + go func() { + wg.Wait() + close(services) + }() + return services +} + +func List(ctx context.Context, timeout time.Duration) []Service { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var result []Service + services := Scan(ctx) + + for service := range services { + result = append(result, service) + } + + return result +} + +type Filter = func(service Service) bool + +func SerialNumberFilter(wantedSerial string) Filter { + return func(service Service) bool { + return service.Text.Serial == wantedSerial + } +} + +func AddressFilter(wantedAddress string) Filter { + return func(service Service) bool { + return service.Address == wantedAddress + } +} + +func IsDfuService(service Service) bool { + return service.ServiceEntry.Service == string(SensoUpdate) || service.Text.Mode == BootloaderMode +} + +func Find(ctx context.Context, timeout time.Duration, filter Filter) *Service { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + services := Scan(ctx) + + for service := range services { + if filter(service) { + cancel() + return &service + } + } + + return nil +} + +type Text struct { + Serial string + Mode string +} + +func getText(entry zeroconf.ServiceEntry) Text { + text := Text{ + Serial: "", + Mode: "", + } + for _, txtField := range entry.Text { + if strings.HasPrefix(txtField, "ser_no=") { + text.Serial = cleanSerial(strings.TrimPrefix(txtField, "ser_no=")) + } else if strings.HasPrefix(txtField, "mode=") { + text.Mode = strings.TrimPrefix(txtField, "mode=") + } + } + return text +} + +func cleanSerial(serialStr string) string { + // Senso firmware up to 3.8.0 adds garbage at end of serial in mDNS + // entries due to improper string sizing. Because bootloader firmware + // will not be updated via Ethernet, the problem will stay around for a + // while and we clean up the serial here to produce readable output for + // older devices. + return strings.Split(serialStr, "\\000")[0] +} From 8e57f6b2b66f158bb2654cce549cd444c5f0d19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 17/43] Separate updating by different inputs --- src/dividat-driver/firmware/cli.go | 0 src/dividat-driver/firmware/main.go | 72 +++++++++++---------- src/dividat-driver/senso/update_firmware.go | 3 +- 3 files changed, 40 insertions(+), 35 deletions(-) create mode 100644 src/dividat-driver/firmware/cli.go diff --git a/src/dividat-driver/firmware/cli.go b/src/dividat-driver/firmware/cli.go new file mode 100644 index 00000000..e69de29b diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 7ac9d0bd..f30a3939 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -29,11 +29,6 @@ func Command(flags []string) { sensoSerial := updateFlags.String("s", "", "Senso serial (optional)") updateFlags.Parse(flags) - var deviceSerial *string = nil - if *sensoSerial != "" { - deviceSerial = sensoSerial - } - if *imagePath == "" { flag.PrintDefaults() return @@ -48,7 +43,13 @@ func Command(flags []string) { fmt.Println(progressMsg) } - err = Update(context.Background(), file, deviceSerial, configuredAddr, onProgress) + if *sensoSerial != "" { + err = UpdateBySerial(context.Background(), *sensoSerial, file, onProgress) + } else if *configuredAddr != "" { + err = updateByAddress(context.Background(), *configuredAddr, file, onProgress) + } else { + err = updateByDiscovery(context.Background(), file, onProgress) + } if err != nil { fmt.Println() @@ -61,37 +62,42 @@ type OnProgress func(msg string) const tryPowerCycling = "Try turning the Senso off and on, waiting for 30 seconds and then running this update tool again." -func Update(parentCtx context.Context, image io.Reader, deviceSerial *string, configuredAddr *string, onProgress OnProgress) error { - var target service.Service +func UpdateBySerial(ctx context.Context, deviceSerial string, image io.Reader, onProgress OnProgress) error { + onProgress(fmt.Sprintf("Using specified serial %s", deviceSerial)) + match := service.Find(ctx, 15*time.Second, service.SerialNumberFilter(deviceSerial)) + if match == nil { + return fmt.Errorf("Failed to find Senso with serial number %s.\n%s", deviceSerial, tryPowerCycling) + } - if configuredAddr != nil && *configuredAddr != "" { - onProgress(fmt.Sprintf("Using specified address %s", *configuredAddr)) - match := service.Find(parentCtx, 15*time.Second, service.AddressFilter(*configuredAddr)) - if match == nil { - return fmt.Errorf("Failed to find Senso with address %s.\n%s", *configuredAddr, tryPowerCycling) - } - target = *match - } else if deviceSerial != nil && *deviceSerial != "" { - onProgress(fmt.Sprintf("Using specified serial %s", *deviceSerial)) - match := service.Find(parentCtx, 15*time.Second, service.SerialNumberFilter(*deviceSerial)) - if match == nil { - return fmt.Errorf("Failed to find Senso with serial number %s.\n%s", *configuredAddr, tryPowerCycling) - } - target = *match + onProgress(fmt.Sprintf("Discovered Senso at %s", match.Address)) + return update(ctx, *match, image, onProgress) +} + +func updateByAddress(ctx context.Context, address string, image io.Reader, onProgress OnProgress) error { + onProgress(fmt.Sprintf("Using specified address %s", address)) + match := service.Find(ctx, 15*time.Second, service.AddressFilter(address)) + if match == nil { + return fmt.Errorf("Failed to find Senso with address %s.\n%s", address, tryPowerCycling) + } + + return update(ctx, *match, image, onProgress) +} + +func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) error { + onProgress("Discovering sensos") + services := service.List(ctx, 15*time.Second) + if len(services) == 1 { + target := services[0] + onProgress(fmt.Sprintf("Discovered Senso: %s (%s)", target.Text.Serial, target.Address)) + return update(ctx, target, image, onProgress) + } else if len(services) == 0 { + return fmt.Errorf("Could not find any Sensos.\n%s", tryPowerCycling) } else { - // Discover controller address via mDNS - onProgress("Discovering sensos") - services := service.List(parentCtx, 15*time.Second) - if len(services) == 1 { - target = services[0] - onProgress(fmt.Sprintf("Discovered Senso: %s (%s)", target.Text.Serial, target.Address)) - } else if len(services) == 0 { - return fmt.Errorf("Could not find any Sensos.\n%s", tryPowerCycling) - } else { - return fmt.Errorf("discovered multiple Sensos: %v, please specify a serial or IP", services) - } + return fmt.Errorf("discovered multiple Sensos: %v, please specify a serial or IP", services) } +} +func update(parentCtx context.Context, target service.Service, image io.Reader, onProgress OnProgress) error { if !service.IsDfuService(target) { trySendDfu := func() error { err := sendDfuCommand(target.Address, controllerPort, onProgress) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index 2c31d939..6cad6eb7 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -47,10 +47,9 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpd handle.log.Error(msg) } - onProgress("Waiting 10 seconds for connection teardown") time.Sleep(10 * time.Second) - err = firmware.Update(context.Background(), image, &command.SerialNumber, nil, onProgress) + err = firmware.UpdateBySerial(context.Background(), command.SerialNumber, image, onProgress) if err != nil { failureMsg := fmt.Sprintf("Failed to update firmware: %v", err) onUpdate(FirmwareUpdateMessage{FirmwareUpdateFailure: &failureMsg}) From 38c2516b975923f6470c5cd8fe5730a4385e6f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 18/43] Move cli-specific code to another file --- src/dividat-driver/firmware/cli.go | 73 +++++++++++++++++++++++++++++ src/dividat-driver/firmware/main.go | 63 ------------------------- 2 files changed, 73 insertions(+), 63 deletions(-) diff --git a/src/dividat-driver/firmware/cli.go b/src/dividat-driver/firmware/cli.go index e69de29b..1e47801e 100644 --- a/src/dividat-driver/firmware/cli.go +++ b/src/dividat-driver/firmware/cli.go @@ -0,0 +1,73 @@ +package firmware + +import ( + "context" + "flag" + "fmt" + "io" + "os" + "time" + + "github.com/dividat/driver/src/dividat-driver/service" +) + +// Command-line interface to Update +func Command(flags []string) { + updateFlags := flag.NewFlagSet("update", flag.ExitOnError) + imagePath := updateFlags.String("i", "", "Firmware image path") + configuredAddr := updateFlags.String("a", "", "Senso address (optional)") + sensoSerial := updateFlags.String("s", "", "Senso serial (optional)") + updateFlags.Parse(flags) + + if *imagePath == "" { + flag.PrintDefaults() + return + } + file, err := os.Open(*imagePath) + if err != nil { + fmt.Printf("Could not open image file: %v\n", err) + os.Exit(1) + } + + onProgress := func(progressMsg string) { + fmt.Println(progressMsg) + } + + if *sensoSerial != "" { + err = UpdateBySerial(context.Background(), *sensoSerial, file, onProgress) + } else if *configuredAddr != "" { + err = updateByAddress(context.Background(), *configuredAddr, file, onProgress) + } else { + err = updateByDiscovery(context.Background(), file, onProgress) + } + + if err != nil { + fmt.Println() + fmt.Printf("Update failed: %v \n", err) + os.Exit(1) + } +} + +func updateByAddress(ctx context.Context, address string, image io.Reader, onProgress OnProgress) error { + onProgress(fmt.Sprintf("Using specified address %s", address)) + match := service.Find(ctx, 15*time.Second, service.AddressFilter(address)) + if match == nil { + return fmt.Errorf("Failed to find Senso with address %s.\n%s", address, tryPowerCycling) + } + + return update(ctx, *match, image, onProgress) +} + +func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) error { + onProgress("Discovering sensos") + services := service.List(ctx, 15*time.Second) + if len(services) == 1 { + target := services[0] + onProgress(fmt.Sprintf("Discovered Senso: %s (%s)", target.Text.Serial, target.Address)) + return update(ctx, target, image, onProgress) + } else if len(services) == 0 { + return fmt.Errorf("Could not find any Sensos.\n%s", tryPowerCycling) + } else { + return fmt.Errorf("discovered multiple Sensos: %v, please specify a serial or IP", services) + } +} diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index f30a3939..b4a88b21 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -4,12 +4,10 @@ import ( "bytes" "context" "encoding/binary" - "flag" "fmt" "io" "math" "net" - "os" "time" "github.com/cenkalti/backoff" @@ -21,43 +19,6 @@ import ( const tftpPort = "69" const controllerPort = "55567" -// Command-line interface to Update -func Command(flags []string) { - updateFlags := flag.NewFlagSet("update", flag.ExitOnError) - imagePath := updateFlags.String("i", "", "Firmware image path") - configuredAddr := updateFlags.String("a", "", "Senso address (optional)") - sensoSerial := updateFlags.String("s", "", "Senso serial (optional)") - updateFlags.Parse(flags) - - if *imagePath == "" { - flag.PrintDefaults() - return - } - file, err := os.Open(*imagePath) - if err != nil { - fmt.Printf("Could not open image file: %v\n", err) - os.Exit(1) - } - - onProgress := func(progressMsg string) { - fmt.Println(progressMsg) - } - - if *sensoSerial != "" { - err = UpdateBySerial(context.Background(), *sensoSerial, file, onProgress) - } else if *configuredAddr != "" { - err = updateByAddress(context.Background(), *configuredAddr, file, onProgress) - } else { - err = updateByDiscovery(context.Background(), file, onProgress) - } - - if err != nil { - fmt.Println() - fmt.Printf("Update failed: %v \n", err) - os.Exit(1) - } -} - type OnProgress func(msg string) const tryPowerCycling = "Try turning the Senso off and on, waiting for 30 seconds and then running this update tool again." @@ -73,30 +34,6 @@ func UpdateBySerial(ctx context.Context, deviceSerial string, image io.Reader, o return update(ctx, *match, image, onProgress) } -func updateByAddress(ctx context.Context, address string, image io.Reader, onProgress OnProgress) error { - onProgress(fmt.Sprintf("Using specified address %s", address)) - match := service.Find(ctx, 15*time.Second, service.AddressFilter(address)) - if match == nil { - return fmt.Errorf("Failed to find Senso with address %s.\n%s", address, tryPowerCycling) - } - - return update(ctx, *match, image, onProgress) -} - -func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) error { - onProgress("Discovering sensos") - services := service.List(ctx, 15*time.Second) - if len(services) == 1 { - target := services[0] - onProgress(fmt.Sprintf("Discovered Senso: %s (%s)", target.Text.Serial, target.Address)) - return update(ctx, target, image, onProgress) - } else if len(services) == 0 { - return fmt.Errorf("Could not find any Sensos.\n%s", tryPowerCycling) - } else { - return fmt.Errorf("discovered multiple Sensos: %v, please specify a serial or IP", services) - } -} - func update(parentCtx context.Context, target service.Service, image io.Reader, onProgress OnProgress) error { if !service.IsDfuService(target) { trySendDfu := func() error { From 340ba57f27aa75c771346b8beacf199132edc631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 19/43] Simplify code --- src/dividat-driver/senso/websocket.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index bf2187e9..b061083d 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -352,13 +352,11 @@ func (handle *Handle) dispatchCommand(ctx context.Context, log *logrus.Entry, co return nil } else if command.UpdateFirmware != nil { - go func() { - handle.ProcessFirmwareUpdateRequest(*command.UpdateFirmware, func(msg FirmwareUpdateMessage) { - sendMessage(Message{ - FirmwareUpdateMessage: &msg, - }) + go handle.ProcessFirmwareUpdateRequest(*command.UpdateFirmware, func(msg FirmwareUpdateMessage) { + sendMessage(Message{ + FirmwareUpdateMessage: &msg, }) - }() + }) } return nil } From 66d2bb80da4746df7df7a2e7c615eae2458219ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 20/43] Add some helpers for constructing firmware update messages --- src/dividat-driver/senso/update_firmware.go | 26 +++++++++--------- src/dividat-driver/senso/websocket.go | 30 ++++++++++++++++++--- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index 6cad6eb7..0405b9ba 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -11,7 +11,11 @@ import ( "github.com/dividat/driver/src/dividat-driver/firmware" ) -type OnUpdate func(msg FirmwareUpdateMessage) +type SendMsg struct { + progress func(string) + failure func(string) + success func(string) +} func (handle *Handle) isUpdatingFirmware() bool { handle.firmwareUpdateMutex.Lock() @@ -26,37 +30,33 @@ func (handle *Handle) setUpdatingFirmware(state bool) { handle.firmwareUpdateMutex.Unlock() } + // Disconnect from current connection -func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, onUpdate OnUpdate) { +func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, send SendMsg) { handle.log.Info("Processing firmware update request.") handle.setUpdatingFirmware(true) - onProgress := func(progressMsg string) { - onUpdate(FirmwareUpdateMessage{FirmwareUpdateProgress: &progressMsg}) - } - if handle.cancelCurrentConnection != nil { - onProgress("Disconnecting from the Senso") + send.progress("Disconnecting from the Senso") handle.cancelCurrentConnection() } image, err := decodeImage(command.Image) if err != nil { msg := fmt.Sprintf("Error decoding base64 string: %v", err) - onUpdate(FirmwareUpdateMessage{FirmwareUpdateFailure: &msg}) + send.failure(msg) handle.log.Error(msg) } - onProgress("Waiting 10 seconds for connection teardown") + send.progress("Waiting 10 seconds for connection teardown") time.Sleep(10 * time.Second) - err = firmware.UpdateBySerial(context.Background(), command.SerialNumber, image, onProgress) + err = firmware.UpdateBySerial(context.Background(), command.SerialNumber, image, send.progress) if err != nil { failureMsg := fmt.Sprintf("Failed to update firmware: %v", err) - onUpdate(FirmwareUpdateMessage{FirmwareUpdateFailure: &failureMsg}) + send.failure(failureMsg) handle.log.Error(failureMsg) } else { - successMsg := "Firmware successfully transmitted." - onUpdate(FirmwareUpdateMessage{FirmwareUpdateSuccess: &successMsg}) + send.success("Firmware successfully transmitted.") } handle.setUpdatingFirmware(false) } diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index b061083d..3f62161b 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -352,15 +352,37 @@ func (handle *Handle) dispatchCommand(ctx context.Context, log *logrus.Entry, co return nil } else if command.UpdateFirmware != nil { - go handle.ProcessFirmwareUpdateRequest(*command.UpdateFirmware, func(msg FirmwareUpdateMessage) { - sendMessage(Message{ - FirmwareUpdateMessage: &msg, - }) + go handle.ProcessFirmwareUpdateRequest(*command.UpdateFirmware, SendMsg{ + progress: func(msg string) { + sendMessage(firmwareUpdateProgress(msg)) + }, + failure: func(msg string) { + sendMessage(firmwareUpdateFailure(msg)) + }, + success: func(msg string) { + sendMessage(firmwareUpdateSuccess(msg)) + }, }) } return nil } +func firmwareUpdateSuccess(msg string) Message { + return firmwareUpdateMessage(FirmwareUpdateMessage{FirmwareUpdateSuccess: &msg}) +} + +func firmwareUpdateFailure(msg string) Message { + return firmwareUpdateMessage(FirmwareUpdateMessage{FirmwareUpdateFailure: &msg}) +} + +func firmwareUpdateProgress(msg string) Message { + return firmwareUpdateMessage(FirmwareUpdateMessage{FirmwareUpdateProgress: &msg}) +} + +func firmwareUpdateMessage(msg FirmwareUpdateMessage) Message { + return Message{FirmwareUpdateMessage: &msg} +} + // rx_data_loop reads data from Senso and forwards it up the WebSocket func rx_data_loop(ctx context.Context, rx chan interface{}, send func([]byte) error) { var err error From b7338d8c2b277dcda8281c4a950cbe68be10f291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:21:26 +0200 Subject: [PATCH 21/43] Reduce timeouts Stress testing after the refactor indicates that this is sufficient. --- src/dividat-driver/firmware/main.go | 2 +- src/dividat-driver/senso/update_firmware.go | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index b4a88b21..55501cc2 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -65,7 +65,7 @@ func update(parentCtx context.Context, target service.Service, image io.Reader, onProgress(fmt.Sprintf("Re-discovered Senso in bootloader mode at %s", target.Address)) onProgress("Waiting 15 seconds to ensure proper TFTP startup") // Wait to ensure proper TFTP startup - time.Sleep(15 * time.Second) + time.Sleep(5 * time.Second) } else { onProgress("Senso discovered in bootloader mode") } diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index 0405b9ba..ddc41018 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "fmt" "io" - "time" "github.com/dividat/driver/src/dividat-driver/firmware" ) @@ -48,8 +47,6 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, send handle.log.Error(msg) } - send.progress("Waiting 10 seconds for connection teardown") - time.Sleep(10 * time.Second) err = firmware.UpdateBySerial(context.Background(), command.SerialNumber, image, send.progress) if err != nil { failureMsg := fmt.Sprintf("Failed to update firmware: %v", err) From 6da5b8bb37abf2a9b5d1d3d4c68e6393c4d0d4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 4 Apr 2024 13:37:13 +0200 Subject: [PATCH 22/43] Fix build Switching to libp2p/zeroconf introduced a transitive dependency not compatible with older go versions. --- nix/nixpkgs.nix | 81 ++----------------------------------------------- shell.nix | 2 +- 2 files changed, 3 insertions(+), 80 deletions(-) diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix index cf5d349f..f2b910fd 100644 --- a/nix/nixpkgs.nix +++ b/nix/nixpkgs.nix @@ -3,85 +3,8 @@ import ((import {}).fetchFromGitHub { owner = "NixOS"; repo = "nixpkgs"; - rev = "18.09"; - sha256 = "1ib96has10v5nr6bzf7v8kw7yzww8zanxgw2qi1ll1sbv6kj6zpd"; + rev = "4077a0e4ac3356222bc1f0a070af7939c3098535"; # 23.05 + sha256 = "sha256-1+28KQl4YC4IBzKo/epvEyK5KH4MlgoYueJ8YwLGbOc="; }) { - crossSystem = crossSystem; - - overlays = [ (self: super: { - - # Backport Go 1.12.9 to old Nixpkgs - # Nixpkgs 19.03 and up seem to break crossbuilding with musl - # Based on https://github.com/NixOS/nixpkgs/blob/19.09/pkgs/development/compilers/go/1.12.nix - go_1_12 = super.go_1_11.overrideAttrs (old: rec { - version = "1.12.9"; - name = "go-${version}"; - src = self.fetchFromGitHub { - owner = "golang"; - repo = "go"; - rev = "go${version}"; - sha256 = "1q316wgxhskwn5p622bcv81dhg93mads1591fppcf0dwyzpnl6wb"; - }; - patches = ( - (self.lib.filter - (x: !(self.lib.hasSuffix "ssl-cert-file-1.9.patch" (builtins.toString x)) && !(self.lib.hasSuffix "remove-fhs-test-references.patch" (builtins.toString x))) - old.patches - ) ++ [ - (self.fetchurl { - url = "https://github.com/NixOS/nixpkgs/raw/19.09/pkgs/development/compilers/go/ssl-cert-file-1.12.1.patch"; - sha256 = "1645yrz36w35lnpalin4ygg39s7hpllamf81w1yr08g8div227f1"; - }) - ] - ); - GOCACHE = null; - GO_BUILDER_NAME = "nix"; - configurePhase = ""; - postConfigure = '' - export GOCACHE=$TMPDIR/go-cache - # this is compiled into the binary - export GOROOT_FINAL=$out/share/go - export PATH=$(pwd)/bin:$PATH - # Independent from host/target, CC should produce code for the building system. - export CC=${self.buildPackages.stdenv.cc}/bin/cc - ulimit -a - ''; - postBuild = '' - (cd src && ./make.bash) - ''; - preInstall = '' - #rm -r pkg/{bootstrap,obj} - # Contains the wrong perl shebang when cross compiling, - # since it is not used for anything we can deleted as well. - rm src/regexp/syntax/make_perl_groups.pl - '' + (if (self.stdenv.buildPlatform != self.stdenv.hostPlatform) then '' - mv bin/*_*/* bin - rmdir bin/*_* - ${self.optionalString (!(self.GOHOSTARCH == self.GOARCH && self.GOOS == self.GOHOSTOS)) '' - rm -rf pkg/${self.GOHOSTOS}_${self.GOHOSTARCH} pkg/tool/${self.GOHOSTOS}_${self.GOHOSTARCH} - ''} - '' else if (self.stdenv.hostPlatform != self.stdenv.targetPlatform) then '' - rm -rf bin/*_* - ${self.optionalString (!(self.GOHOSTARCH == self.GOARCH && self.GOOS == self.GOHOSTOS)) '' - rm -rf pkg/${self.GOOS}_${self.GOARCH} pkg/tool/${self.GOOS}_${self.GOARCH} - ''} - '' else ""); - installPhase = '' - runHook preInstall - mkdir -p $GOROOT_FINAL - cp -a bin pkg src lib misc api doc $GOROOT_FINAL - ln -s $GOROOT_FINAL/bin $out/bin - runHook postInstall - ''; - }); - - # GOCACHE can not be disabled in Go 1.12 but buildGoPackage definition hardcodes - # turning it off in NixOS 18.09. - buildGo112Package = - self.callPackage (self.fetchurl { url = "https://github.com/NixOS/nixpkgs/raw/19.09/pkgs/development/go-packages/generic/default.nix"; sha256 = "1bwkjbfxfym3v6z2zv0yrygpzck2cx63dpv46jil3py0yndaqrwa"; }) { - go = self.go_1_12; - }; - - })]; - } diff --git a/shell.nix b/shell.nix index a89d8272..463a78a1 100644 --- a/shell.nix +++ b/shell.nix @@ -4,7 +4,7 @@ mkShell { buildInputs = [ - go_1_12 + go gcc nix-prefetch-git From 72527779b1073ef4a462a11c89977912cc80d25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 11 Apr 2024 12:57:37 +0200 Subject: [PATCH 23/43] Document and re-organize service module --- src/dividat-driver/service/main.go | 78 +++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/src/dividat-driver/service/main.go b/src/dividat-driver/service/main.go index 44c49fcf..ea3478e1 100644 --- a/src/dividat-driver/service/main.go +++ b/src/dividat-driver/service/main.go @@ -1,5 +1,6 @@ package service +// This module contains functions to discover Sensos via mDNS. import ( "context" "fmt" @@ -10,16 +11,28 @@ import ( "github.com/libp2p/zeroconf/v2" ) +// Represents a service that has been discovered. +// Relevant information about the service is lifted +// out of the zeroconf record for ease of use. type Service struct { Text Text Address string ServiceEntry zeroconf.ServiceEntry } +// Information parsed from services' txt records. +type Text struct { + Serial string + Mode string +} + func (s Service) String() string { return fmt.Sprintf("{ Serial: %s, Address: %s}", s.Text.Serial, s.Address) } +// Service type, indicating what mode a Senso is in. +// SensoUpdate means the Senso is in bootloader mode, +// while SensoControl means the Senso is in "normal" mode. type ServiceType string const ( @@ -27,17 +40,27 @@ const ( SensoControl ServiceType = "_sensoControl._tcp" ) +// Up to firmware 2.0.0.0 the bootloader advertised itself +// with the same service identifier as the application level +// firmware. Because of this we also have to check the `mode` +// field on a service's txt records to determine what mode a +// Senso is in. The enum below represents the possible values +// of this field. const ( ApplicationMode = "Application" BootloaderMode = "Bootloader" ) +// Scan for services of a specific type, ie `SensoUpdate` or `SensoControl`. func scanForType(ctx context.Context, t ServiceType, results chan<- Service, wg *sync.WaitGroup) { wg.Add(2) // Zeroconf closes the channel on context cancellation, // so we cannot share channels between multiple browse calls. // Doing so would lead to panic as one instance would try to close // a channel that was already closed by another instance. + // To prevent this, we create an intermediate channel for each instance, + // then forward the discovered service entries to the main results channel in + // a separate goroutine. localEntries := make(chan *zeroconf.ServiceEntry) go func() { defer wg.Done() @@ -74,6 +97,7 @@ func scanForType(ctx context.Context, t ServiceType, results chan<- Service, wg }() } +// Scan for both types of services concurrently. func Scan(ctx context.Context) chan Service { var wg sync.WaitGroup services := make(chan Service) @@ -86,6 +110,8 @@ func Scan(ctx context.Context) chan Service { return services } +// Like `Scan`, but blocking. +// Returns a slice of services found within the specified timeout. func List(ctx context.Context, timeout time.Duration) []Service { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() @@ -100,24 +126,13 @@ func List(ctx context.Context, timeout time.Duration) []Service { return result } +// Type alias to a filter function that can be used with `Find`. +// Helpers to construct commonly used filters are defined below. type Filter = func(service Service) bool -func SerialNumberFilter(wantedSerial string) Filter { - return func(service Service) bool { - return service.Text.Serial == wantedSerial - } -} - -func AddressFilter(wantedAddress string) Filter { - return func(service Service) bool { - return service.Address == wantedAddress - } -} - -func IsDfuService(service Service) bool { - return service.ServiceEntry.Service == string(SensoUpdate) || service.Text.Mode == BootloaderMode -} - +// Looks for a service specified by the filter function. +// As soon as a match is found, it cancels scanning +// and returns the match. func Find(ctx context.Context, timeout time.Duration, filter Filter) *Service { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() @@ -134,11 +149,26 @@ func Find(ctx context.Context, timeout time.Duration, filter Filter) *Service { return nil } -type Text struct { - Serial string - Mode string +// Commonly used filters to look for services. + +func SerialNumberFilter(wantedSerial string) Filter { + return func(service Service) bool { + return service.Text.Serial == wantedSerial + } +} + +func AddressFilter(wantedAddress string) Filter { + return func(service Service) bool { + return service.Address == wantedAddress + } +} + +func IsDfuService(service Service) bool { + return service.ServiceEntry.Service == string(SensoUpdate) || service.Text.Mode == BootloaderMode } +// Helper to parse relevant information from the +// txt record of a service entry. func getText(entry zeroconf.ServiceEntry) Text { text := Text{ Serial: "", @@ -154,11 +184,11 @@ func getText(entry zeroconf.ServiceEntry) Text { return text } +// Senso firmware up to 3.8.0 adds garbage at end of serial in mDNS +// entries due to improper string sizing. Because bootloader firmware +// will not be updated via Ethernet, the problem will stay around for a +// while and we clean up the serial here to produce readable output for +// older devices. func cleanSerial(serialStr string) string { - // Senso firmware up to 3.8.0 adds garbage at end of serial in mDNS - // entries due to improper string sizing. Because bootloader firmware - // will not be updated via Ethernet, the problem will stay around for a - // while and we clean up the serial here to produce readable output for - // older devices. return strings.Split(serialStr, "\\000")[0] } From c66c09cf5893b57822a83c4222d123a1c7b2f6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 11 Apr 2024 13:04:19 +0200 Subject: [PATCH 24/43] Add some docs to the firmware modules --- src/dividat-driver/firmware/cli.go | 5 ++++- src/dividat-driver/firmware/main.go | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/dividat-driver/firmware/cli.go b/src/dividat-driver/firmware/cli.go index 1e47801e..50a81a0d 100644 --- a/src/dividat-driver/firmware/cli.go +++ b/src/dividat-driver/firmware/cli.go @@ -11,7 +11,7 @@ import ( "github.com/dividat/driver/src/dividat-driver/service" ) -// Command-line interface to Update +// Command-line interface to running a firmware update func Command(flags []string) { updateFlags := flag.NewFlagSet("update", flag.ExitOnError) imagePath := updateFlags.String("i", "", "Firmware image path") @@ -48,6 +48,9 @@ func Command(flags []string) { } } +// The following functions are only used when updating the firmware via the command line. +// This is why they are private, and not part of the main module. + func updateByAddress(ctx context.Context, address string, image io.Reader, onProgress OnProgress) error { onProgress(fmt.Sprintf("Using specified address %s", address)) match := service.Find(ctx, 15*time.Second, service.AddressFilter(address)) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 55501cc2..8bcbccc5 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -1,5 +1,16 @@ package firmware +// Functions for performing a firmware update. +// The update procedure consists of the following high-level steps: +// +// 1. Discover Senso via mDNS +// +// 2. If the Senso is found to be in application mode, +// send a DFU (Device Firmware Update) command +// to make the Senso reboot into bootloader mode. +// +// 3. Transfer the firmware image via TFTP. + import ( "bytes" "context" From 22f6e337e2282b61c1d99940c3436e7d35bade8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 11 Apr 2024 13:31:57 +0200 Subject: [PATCH 25/43] Encapsulate state management in firmware module Ensures privacy of state internals. --- src/dividat-driver/firmware/main.go | 29 +++++++++++++++++++++ src/dividat-driver/senso/main.go | 8 +++--- src/dividat-driver/senso/update_firmware.go | 17 ++---------- src/dividat-driver/senso/websocket.go | 4 +-- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 8bcbccc5..b7ee3cbd 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -19,6 +19,7 @@ import ( "io" "math" "net" + "sync" "time" "github.com/cenkalti/backoff" @@ -161,3 +162,31 @@ func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) e onProgress(fmt.Sprintf("%d bytes sent", n)) return nil } + +// State to keep track of when an update is in progress. +// This is used by the senso module, but is kept here to +// ensure privacy of internals. + +type Update struct { + stateMutex sync.Mutex + inProgress bool +} + +func InitialUpdateState() *Update { + return &Update{ + inProgress: false, + stateMutex: sync.Mutex{}, + } +} + +func (u *Update) IsUpdating() bool { + u.stateMutex.Lock() + defer u.stateMutex.Unlock() + return u.inProgress +} + +func (u *Update) SetUpdating(state bool) { + u.stateMutex.Lock() + defer u.stateMutex.Unlock() + u.inProgress = state +} diff --git a/src/dividat-driver/senso/main.go b/src/dividat-driver/senso/main.go index 4b32652e..2b0ce4f1 100644 --- a/src/dividat-driver/senso/main.go +++ b/src/dividat-driver/senso/main.go @@ -7,6 +7,8 @@ import ( "github.com/cskr/pubsub" "github.com/sirupsen/logrus" + + "github.com/dividat/driver/src/dividat-driver/firmware" ) // Handle for managing Senso @@ -20,8 +22,7 @@ type Handle struct { cancelCurrentConnection context.CancelFunc connectionChangeMutex *sync.Mutex - firmwareUpdateInProgress bool - firmwareUpdateMutex *sync.Mutex + firmwareUpdate *firmware.Update log *logrus.Entry } @@ -35,8 +36,7 @@ func New(ctx context.Context, log *logrus.Entry) *Handle { handle.log = log handle.connectionChangeMutex = &sync.Mutex{} - handle.firmwareUpdateMutex = &sync.Mutex{} - handle.firmwareUpdateInProgress = false + handle.firmwareUpdate = firmware.InitialUpdateState() // PubSub broker handle.broker = pubsub.New(32) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index ddc41018..ecc8abed 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -16,24 +16,11 @@ type SendMsg struct { success func(string) } -func (handle *Handle) isUpdatingFirmware() bool { - handle.firmwareUpdateMutex.Lock() - state := handle.firmwareUpdateInProgress - handle.firmwareUpdateMutex.Unlock() - return state -} - -func (handle *Handle) setUpdatingFirmware(state bool) { - handle.firmwareUpdateMutex.Lock() - handle.firmwareUpdateInProgress = state - handle.firmwareUpdateMutex.Unlock() -} - // Disconnect from current connection func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, send SendMsg) { handle.log.Info("Processing firmware update request.") - handle.setUpdatingFirmware(true) + handle.firmwareUpdate.SetUpdating(true) if handle.cancelCurrentConnection != nil { send.progress("Disconnecting from the Senso") @@ -55,7 +42,7 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, send } else { send.success("Firmware successfully transmitted.") } - handle.setUpdatingFirmware(false) + handle.firmwareUpdate.SetUpdating(false) } func decodeImage(base64Str string) (io.Reader, error) { diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index 3f62161b..ac1b60ba 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -269,7 +269,7 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { if messageType == websocket.BinaryMessage { - if handle.isUpdatingFirmware() { + if handle.firmwareUpdate.IsUpdating() { handle.log.Info("Firmware update in progress, ignoring Senso command.") continue } @@ -286,7 +286,7 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { } log.WithField("command", prettyPrintCommand(command)).Debug("Received command.") - if handle.isUpdatingFirmware() && (command.GetStatus == nil || command.Discover == nil) { + if handle.firmwareUpdate.IsUpdating() && (command.GetStatus == nil || command.Discover == nil) { handle.log.Info("Firmware update in progress, ignoring UpdateFirmware command.") continue } From 28fe54ed5da2288bd05d540f23ba1ceb13f1fdce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Thu, 11 Apr 2024 13:34:11 +0200 Subject: [PATCH 26/43] Update changelog --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index a345cc5f..b3b4c19d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ - Let firmware update command look for bootloader if no Senso in regular mode is found - Update build system and development environment +- Add support for triggering firmware updates via websocket ## [2.3.0] - 2022-10-01 From ccb18c5430ec4ed65fff79aaeb5dfcf6131a1eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 15 Apr 2024 14:37:15 +0200 Subject: [PATCH 27/43] Improve clarity of firmware update message Co-authored-by: Johannes Emerich --- src/dividat-driver/firmware/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index b7ee3cbd..91e9480c 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -36,7 +36,7 @@ type OnProgress func(msg string) const tryPowerCycling = "Try turning the Senso off and on, waiting for 30 seconds and then running this update tool again." func UpdateBySerial(ctx context.Context, deviceSerial string, image io.Reader, onProgress OnProgress) error { - onProgress(fmt.Sprintf("Using specified serial %s", deviceSerial)) + onProgress(fmt.Sprintf("Looking for Senso with specified serial %s", deviceSerial)) match := service.Find(ctx, 15*time.Second, service.SerialNumberFilter(deviceSerial)) if match == nil { return fmt.Errorf("Failed to find Senso with serial number %s.\n%s", deviceSerial, tryPowerCycling) From 8097df36c246f9a1d700cbb5815e3f28acad5b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 15 Apr 2024 14:53:45 +0200 Subject: [PATCH 28/43] Allow 60 seconds for mDNS discovery Devices try waiting for an IP via DHCP for ~30 seconds before assigning themselves a link-local address, so in our typical setup they are only going to announce services around 30 seconds after the physical link is established. 60 seconds should be more than enough. --- src/dividat-driver/firmware/cli.go | 5 ++--- src/dividat-driver/firmware/main.go | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dividat-driver/firmware/cli.go b/src/dividat-driver/firmware/cli.go index 50a81a0d..bf1bf358 100644 --- a/src/dividat-driver/firmware/cli.go +++ b/src/dividat-driver/firmware/cli.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "os" - "time" "github.com/dividat/driver/src/dividat-driver/service" ) @@ -53,7 +52,7 @@ func Command(flags []string) { func updateByAddress(ctx context.Context, address string, image io.Reader, onProgress OnProgress) error { onProgress(fmt.Sprintf("Using specified address %s", address)) - match := service.Find(ctx, 15*time.Second, service.AddressFilter(address)) + match := service.Find(ctx, discoveryTimeout, service.AddressFilter(address)) if match == nil { return fmt.Errorf("Failed to find Senso with address %s.\n%s", address, tryPowerCycling) } @@ -63,7 +62,7 @@ func updateByAddress(ctx context.Context, address string, image io.Reader, onPro func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) error { onProgress("Discovering sensos") - services := service.List(ctx, 15*time.Second) + services := service.List(ctx, discoveryTimeout) if len(services) == 1 { target := services[0] onProgress(fmt.Sprintf("Discovered Senso: %s (%s)", target.Text.Serial, target.Address)) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 91e9480c..f54c4cb9 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -30,6 +30,7 @@ import ( const tftpPort = "69" const controllerPort = "55567" +const discoveryTimeout = 60 * time.Second type OnProgress func(msg string) @@ -37,7 +38,7 @@ const tryPowerCycling = "Try turning the Senso off and on, waiting for 30 second func UpdateBySerial(ctx context.Context, deviceSerial string, image io.Reader, onProgress OnProgress) error { onProgress(fmt.Sprintf("Looking for Senso with specified serial %s", deviceSerial)) - match := service.Find(ctx, 15*time.Second, service.SerialNumberFilter(deviceSerial)) + match := service.Find(ctx, discoveryTimeout, service.SerialNumberFilter(deviceSerial)) if match == nil { return fmt.Errorf("Failed to find Senso with serial number %s.\n%s", deviceSerial, tryPowerCycling) } @@ -65,7 +66,7 @@ func update(parentCtx context.Context, target service.Service, image io.Reader, } onProgress("Looking for senso in bootloader mode") - dfuService := service.Find(parentCtx, 30*time.Second, func(discovered service.Service) bool { + dfuService := service.Find(parentCtx, discoveryTimeout, func(discovered service.Service) bool { return service.SerialNumberFilter(target.Text.Serial)(discovered) && service.IsDfuService(discovered) }) From a59e24e4873457a0fbb8ccad38130ca8c93792d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 15 Apr 2024 14:24:22 +0200 Subject: [PATCH 29/43] Prevent unnecessary TFTP send failure messages --- src/dividat-driver/firmware/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index f54c4cb9..c3f9fb12 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -136,6 +136,10 @@ func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) e maxRetries := 5 client.SetRetries(maxRetries) + // It can take a while for the Senso to respond to the TFTP write request. + // Setting timeout to 10 seconds prevents unnecessary messages about failed + // send attempts. + client.SetTimeout(10 * time.Second) expDelay := func(attempt int) time.Duration { exp := math.Pow(2, float64(attempt)) From 5bb2e6db46482ad7474597f3cb6e3f91659cb567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 15 Apr 2024 14:24:22 +0200 Subject: [PATCH 30/43] Fix TFTP send fail message It should report the delay time for `attempt`, rather than `attempt+1`. `attempt` is still being reported as `attempt+1` for user-friendliness. --- src/dividat-driver/firmware/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index c3f9fb12..d1dd0f45 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -148,10 +148,10 @@ func putTFTP(host string, port string, image io.Reader, onProgress OnProgress) e } client.SetBackoff(func(attempt int) time.Duration { - a1 := attempt + 1 - msg := fmt.Sprintf("Failed on attempt %d, retrying in %v", a1, expDelay(a1)) + delay := expDelay(attempt) + msg := fmt.Sprintf("Failed on attempt %d, retrying in %v", attempt+1, delay) onProgress(msg) - return expDelay(attempt) + return delay }) onProgress("Preparing transmission") From 7349c61ae6c45cb015c8f2f8b98d7887ac5eadcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 15 Apr 2024 14:35:30 +0200 Subject: [PATCH 31/43] Capitalize "Senso" where appropriate --- src/dividat-driver/firmware/cli.go | 2 +- src/dividat-driver/firmware/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dividat-driver/firmware/cli.go b/src/dividat-driver/firmware/cli.go index bf1bf358..baf82acb 100644 --- a/src/dividat-driver/firmware/cli.go +++ b/src/dividat-driver/firmware/cli.go @@ -61,7 +61,7 @@ func updateByAddress(ctx context.Context, address string, image io.Reader, onPro } func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) error { - onProgress("Discovering sensos") + onProgress("Discovering Sensos") services := service.List(ctx, discoveryTimeout) if len(services) == 1 { target := services[0] diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index d1dd0f45..47061c16 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -65,7 +65,7 @@ func update(parentCtx context.Context, target service.Service, image io.Reader, return fmt.Errorf("could not send DFU command to Senso at %s: %s", target.Address, err) } - onProgress("Looking for senso in bootloader mode") + onProgress("Looking for Senso in bootloader mode") dfuService := service.Find(parentCtx, discoveryTimeout, func(discovered service.Service) bool { return service.SerialNumberFilter(target.Text.Serial)(discovered) && service.IsDfuService(discovered) }) From 5f03ddf2f0b818364d32a0edcd3994256afab91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 15 Apr 2024 14:59:13 +0200 Subject: [PATCH 32/43] Remove unused nix files This were deleted in #129, but were resurrected when merging main into this branch. --- nix/nixpkgs.nix | 10 ---------- shell.nix | 33 --------------------------------- 2 files changed, 43 deletions(-) delete mode 100644 nix/nixpkgs.nix delete mode 100644 shell.nix diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix deleted file mode 100644 index f2b910fd..00000000 --- a/nix/nixpkgs.nix +++ /dev/null @@ -1,10 +0,0 @@ -{ crossSystem ? null }: - -import ((import {}).fetchFromGitHub { - owner = "NixOS"; - repo = "nixpkgs"; - rev = "4077a0e4ac3356222bc1f0a070af7939c3098535"; # 23.05 - sha256 = "sha256-1+28KQl4YC4IBzKo/epvEyK5KH4MlgoYueJ8YwLGbOc="; -}) { - crossSystem = crossSystem; -} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 463a78a1..00000000 --- a/shell.nix +++ /dev/null @@ -1,33 +0,0 @@ -with (import ./nix/nixpkgs.nix) {}; - -mkShell { - - buildInputs = [ - - go - gcc - nix-prefetch-git - - # node for tests - nodejs - - # for building releases - openssl upx - - # for deployment to S3 - awscli - - # Required for building go dependencies - autoconf automake libtool flex pkgconfig - - ] ++ lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.PCSC # PCSC on Darwin - ++ lib.optional stdenv.isLinux pcsclite; - - # GOPATH is set to a readonly directory - # This seems to be fixed with nixpkgs 20.03 - # https://github.com/NixOS/nixpkgs/issues/90136 - shellHook = '' - export GOPATH="$HOME/.go" - ''; - -} From b3352123eb8f6cecc9a494be4859960a8b333d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 15 Apr 2024 15:00:21 +0200 Subject: [PATCH 33/43] Format file --- src/dividat-driver/senso/update_firmware.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index ecc8abed..d7dbd0bc 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -16,7 +16,6 @@ type SendMsg struct { success func(string) } - // Disconnect from current connection func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, send SendMsg) { handle.log.Info("Processing firmware update request.") From ffa7b584a1a33fedbb92dad271044cc829518aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 15 Apr 2024 18:00:43 +0200 Subject: [PATCH 34/43] Increase wait time before attempting TFTP transfer This seems to improve robustness. Background: After rebooting to bootloader mode, the Senso needs some time to start the TFTP service. Experimentation has shown that a premature a TFTP write request to the Senso can potentially cause it to enter a non-responsive state. The Senso can only be recovered from this state via power-cycling. Sometimes even re-flashing the firmware via USB may be necessary. The previous 5 second wait already mitigated the risk quite well, but during a stress test of performing back-to-back updates, the TFTP transfer failed during the ~50th run. With a 10 second timeout, over 200 back-to-back updates were performed successfully before shutting down the test script. --- src/dividat-driver/firmware/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 47061c16..1522b779 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -76,9 +76,9 @@ func update(parentCtx context.Context, target service.Service, image io.Reader, target = *dfuService onProgress(fmt.Sprintf("Re-discovered Senso in bootloader mode at %s", target.Address)) - onProgress("Waiting 15 seconds to ensure proper TFTP startup") + onProgress("Waiting 10 seconds to ensure proper TFTP startup") // Wait to ensure proper TFTP startup - time.Sleep(5 * time.Second) + time.Sleep(10 * time.Second) } else { onProgress("Senso discovered in bootloader mode") } From 1233e05f2c8d0ef1516e7f23950558b372340e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Tue, 16 Apr 2024 12:05:16 +0200 Subject: [PATCH 35/43] Do not proceed with firmware update if image could not be decoded --- src/dividat-driver/senso/update_firmware.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index d7dbd0bc..eabdafa2 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -31,6 +31,7 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, send msg := fmt.Sprintf("Error decoding base64 string: %v", err) send.failure(msg) handle.log.Error(msg) + return } err = firmware.UpdateBySerial(context.Background(), command.SerialNumber, image, send.progress) From 8743fda024225174829514e81639e00105dced06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Tue, 16 Apr 2024 12:14:51 +0200 Subject: [PATCH 36/43] Remove firmware update by fixed address Make mDNS a requirement to run firmware updates. Reasoning: - mDNS is used by 99% of installations - The robustness of the update mechanism hinges on the insight into peer state provided by the mDNS records - We can focus our resources on making this one mechanism work well --- src/dividat-driver/firmware/cli.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/dividat-driver/firmware/cli.go b/src/dividat-driver/firmware/cli.go index baf82acb..8fbb840c 100644 --- a/src/dividat-driver/firmware/cli.go +++ b/src/dividat-driver/firmware/cli.go @@ -14,7 +14,6 @@ import ( func Command(flags []string) { updateFlags := flag.NewFlagSet("update", flag.ExitOnError) imagePath := updateFlags.String("i", "", "Firmware image path") - configuredAddr := updateFlags.String("a", "", "Senso address (optional)") sensoSerial := updateFlags.String("s", "", "Senso serial (optional)") updateFlags.Parse(flags) @@ -34,8 +33,6 @@ func Command(flags []string) { if *sensoSerial != "" { err = UpdateBySerial(context.Background(), *sensoSerial, file, onProgress) - } else if *configuredAddr != "" { - err = updateByAddress(context.Background(), *configuredAddr, file, onProgress) } else { err = updateByDiscovery(context.Background(), file, onProgress) } @@ -47,19 +44,6 @@ func Command(flags []string) { } } -// The following functions are only used when updating the firmware via the command line. -// This is why they are private, and not part of the main module. - -func updateByAddress(ctx context.Context, address string, image io.Reader, onProgress OnProgress) error { - onProgress(fmt.Sprintf("Using specified address %s", address)) - match := service.Find(ctx, discoveryTimeout, service.AddressFilter(address)) - if match == nil { - return fmt.Errorf("Failed to find Senso with address %s.\n%s", address, tryPowerCycling) - } - - return update(ctx, *match, image, onProgress) -} - func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) error { onProgress("Discovering Sensos") services := service.List(ctx, discoveryTimeout) From cb649fcaf64a7bb0e40068584cb841cc02421e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Tue, 16 Apr 2024 12:49:27 +0200 Subject: [PATCH 37/43] Communicate power cycling suggestion only when updating via CLI Leave it up to applications on how to communicate failure. --- src/dividat-driver/firmware/cli.go | 25 ++++++++++++++++++++----- src/dividat-driver/firmware/main.go | 6 ++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/dividat-driver/firmware/cli.go b/src/dividat-driver/firmware/cli.go index 8fbb840c..3b6b49c5 100644 --- a/src/dividat-driver/firmware/cli.go +++ b/src/dividat-driver/firmware/cli.go @@ -31,29 +31,44 @@ func Command(flags []string) { fmt.Println(progressMsg) } + tryPowerCycling := "Try turning the Senso off and on, waiting for 30 seconds and then running this update tool again." + suggestPowerCycling := false + if *sensoSerial != "" { err = UpdateBySerial(context.Background(), *sensoSerial, file, onProgress) + if err != nil { + suggestPowerCycling = true + } } else { - err = updateByDiscovery(context.Background(), file, onProgress) + err, suggestPowerCycling = updateByDiscovery(context.Background(), file, onProgress) } if err != nil { fmt.Println() fmt.Printf("Update failed: %v \n", err) + if suggestPowerCycling { + fmt.Println(tryPowerCycling) + } os.Exit(1) } } -func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) error { +func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) (err error, suggestPowerCycling bool) { onProgress("Discovering Sensos") services := service.List(ctx, discoveryTimeout) if len(services) == 1 { target := services[0] onProgress(fmt.Sprintf("Discovered Senso: %s (%s)", target.Text.Serial, target.Address)) - return update(ctx, target, image, onProgress) + err = update(ctx, target, image, onProgress) + if err != nil { + suggestPowerCycling = true + } } else if len(services) == 0 { - return fmt.Errorf("Could not find any Sensos.\n%s", tryPowerCycling) + err = fmt.Errorf("Could not find any Sensos.") + suggestPowerCycling = true } else { - return fmt.Errorf("discovered multiple Sensos: %v, please specify a serial or IP", services) + err = fmt.Errorf("discovered multiple Sensos: %v, please specify a serial or IP", services) + suggestPowerCycling = false } + return } diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 1522b779..a283f225 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -34,13 +34,11 @@ const discoveryTimeout = 60 * time.Second type OnProgress func(msg string) -const tryPowerCycling = "Try turning the Senso off and on, waiting for 30 seconds and then running this update tool again." - func UpdateBySerial(ctx context.Context, deviceSerial string, image io.Reader, onProgress OnProgress) error { onProgress(fmt.Sprintf("Looking for Senso with specified serial %s", deviceSerial)) match := service.Find(ctx, discoveryTimeout, service.SerialNumberFilter(deviceSerial)) if match == nil { - return fmt.Errorf("Failed to find Senso with serial number %s.\n%s", deviceSerial, tryPowerCycling) + return fmt.Errorf("Failed to find Senso with serial number %s.\n%s", deviceSerial) } onProgress(fmt.Sprintf("Discovered Senso at %s", match.Address)) @@ -71,7 +69,7 @@ func update(parentCtx context.Context, target service.Service, image io.Reader, }) if dfuService == nil { - return fmt.Errorf("Could not rediscover Senso in bootloader mode.\n%s", tryPowerCycling) + return fmt.Errorf("Could not rediscover Senso in bootloader mode.") } target = *dfuService From 0121d02a32cb56394d6aa5be7c916d2ad5bea555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Tue, 16 Apr 2024 12:57:11 +0200 Subject: [PATCH 38/43] Improve log messages about ignored commands during firmware update --- src/dividat-driver/senso/websocket.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index ac1b60ba..683c7022 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -270,7 +270,7 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { if messageType == websocket.BinaryMessage { if handle.firmwareUpdate.IsUpdating() { - handle.log.Info("Firmware update in progress, ignoring Senso command.") + handle.log.Info("Ignoring Senso command during firmware update.") continue } @@ -287,7 +287,7 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.WithField("command", prettyPrintCommand(command)).Debug("Received command.") if handle.firmwareUpdate.IsUpdating() && (command.GetStatus == nil || command.Discover == nil) { - handle.log.Info("Firmware update in progress, ignoring UpdateFirmware command.") + log.WithField("command", prettyPrintCommand(command)).Info("Ignoring command during firmware update.") continue } From 80dfde6851a9755d618894d995ffad8e15022a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Tue, 16 Apr 2024 12:58:24 +0200 Subject: [PATCH 39/43] Implement pretty-printing for UpdateFirmware command --- src/dividat-driver/senso/websocket.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index 683c7022..7046b376 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -38,6 +38,8 @@ func prettyPrintCommand(command Command) string { return "Disconnect" } else if command.Discover != nil { return "Discover" + } else if command.UpdateFirmware != nil { + return "UpdateFirmware" } return "Unknown" } From 81d9acd5cab149d4f6bec11a0c71999f4aa8d1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Fri, 19 Apr 2024 10:00:45 +0200 Subject: [PATCH 40/43] Log messages about ignored commands at debug level --- src/dividat-driver/senso/websocket.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dividat-driver/senso/websocket.go b/src/dividat-driver/senso/websocket.go index 7046b376..ce19d837 100644 --- a/src/dividat-driver/senso/websocket.go +++ b/src/dividat-driver/senso/websocket.go @@ -272,7 +272,7 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { if messageType == websocket.BinaryMessage { if handle.firmwareUpdate.IsUpdating() { - handle.log.Info("Ignoring Senso command during firmware update.") + handle.log.Debug("Ignoring Senso command during firmware update.") continue } @@ -289,7 +289,7 @@ func (handle *Handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.WithField("command", prettyPrintCommand(command)).Debug("Received command.") if handle.firmwareUpdate.IsUpdating() && (command.GetStatus == nil || command.Discover == nil) { - log.WithField("command", prettyPrintCommand(command)).Info("Ignoring command during firmware update.") + log.WithField("command", prettyPrintCommand(command)).Debug("Ignoring command during firmware update.") continue } From ad56fd209c664e2b0fc8ea9fb71bcb06ada10d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Fri, 19 Apr 2024 10:10:39 +0200 Subject: [PATCH 41/43] Add notes about libp2p/zeroconf dependency choice --- go.mod | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/go.mod b/go.mod index fee2cccd..d1f69b14 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,15 @@ require ( github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95 github.com/gorilla/websocket v1.4.2 github.com/kardianos/service v1.2.0 + + // `libp2p/zeroconf` is a fork of `grandcat/zeroconf`, which we previously used. + // This fork includes some stability improvements and bug fixes that are absent + // in the grandcat version. However, it is libp2p's internal maintenance fork, + // which while being public, does not accept community contributions. + // Both projects are dormant at the moment, but we might want to re-evaluate this + // dependency choice as these projects evolve in the future. github.com/libp2p/zeroconf/v2 v2.2.0 + github.com/pin/tftp v2.1.0+incompatible github.com/sirupsen/logrus v1.8.1 go.bug.st/serial v1.6.1 From 8e0e4a7a883071112688c91ece53bb659d270c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Fri, 19 Apr 2024 10:22:52 +0200 Subject: [PATCH 42/43] Deduplicate firmware update success messages sent via websockets --- src/dividat-driver/firmware/cli.go | 2 ++ src/dividat-driver/firmware/main.go | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dividat-driver/firmware/cli.go b/src/dividat-driver/firmware/cli.go index 3b6b49c5..8c9eb487 100644 --- a/src/dividat-driver/firmware/cli.go +++ b/src/dividat-driver/firmware/cli.go @@ -51,6 +51,8 @@ func Command(flags []string) { } os.Exit(1) } + + fmt.Println("Success! Firmware transmitted to Senso.") } func updateByDiscovery(ctx context.Context, image io.Reader, onProgress OnProgress) (err error, suggestPowerCycling bool) { diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index a283f225..50bc8bc9 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -86,7 +86,6 @@ func update(parentCtx context.Context, target service.Service, image io.Reader, return err } - onProgress("Success! Firmware transmitted to Senso.") return nil } From 57b821c245b344778cb7dabe626f53ef2ec5a4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kerekes?= Date: Mon, 22 Apr 2024 16:18:29 +0200 Subject: [PATCH 43/43] Refine vocabulary and punctuation of firmware update messages Co-authored-by: Johannes Emerich --- src/dividat-driver/firmware/main.go | 14 +++++++------- src/dividat-driver/senso/update_firmware.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/dividat-driver/firmware/main.go b/src/dividat-driver/firmware/main.go index 50bc8bc9..ac535edd 100644 --- a/src/dividat-driver/firmware/main.go +++ b/src/dividat-driver/firmware/main.go @@ -38,10 +38,10 @@ func UpdateBySerial(ctx context.Context, deviceSerial string, image io.Reader, o onProgress(fmt.Sprintf("Looking for Senso with specified serial %s", deviceSerial)) match := service.Find(ctx, discoveryTimeout, service.SerialNumberFilter(deviceSerial)) if match == nil { - return fmt.Errorf("Failed to find Senso with serial number %s.\n%s", deviceSerial) + return fmt.Errorf("Failed to find Senso with serial number %s", deviceSerial) } - onProgress(fmt.Sprintf("Discovered Senso at %s", match.Address)) + onProgress(fmt.Sprintf("Found Senso at %s", match.Address)) return update(ctx, *match, image, onProgress) } @@ -60,7 +60,7 @@ func update(parentCtx context.Context, target service.Service, image io.Reader, }) if err != nil { - return fmt.Errorf("could not send DFU command to Senso at %s: %s", target.Address, err) + return fmt.Errorf("Could not send DFU command to Senso at %s: %s", target.Address, err) } onProgress("Looking for Senso in bootloader mode") @@ -69,16 +69,16 @@ func update(parentCtx context.Context, target service.Service, image io.Reader, }) if dfuService == nil { - return fmt.Errorf("Could not rediscover Senso in bootloader mode.") + return fmt.Errorf("Could not find Senso in bootloader mode") } target = *dfuService - onProgress(fmt.Sprintf("Re-discovered Senso in bootloader mode at %s", target.Address)) + onProgress(fmt.Sprintf("Found Senso in bootloader mode at %s", target.Address)) onProgress("Waiting 10 seconds to ensure proper TFTP startup") // Wait to ensure proper TFTP startup time.Sleep(10 * time.Second) } else { - onProgress("Senso discovered in bootloader mode") + onProgress("Found Senso in bootloader mode") } err := putTFTP(target.Address, tftpPort, image, onProgress) @@ -119,7 +119,7 @@ func sendDfuCommand(host string, port string, onProgress OnProgress) error { return fmt.Errorf("Could not send DFU command: %v", err) } - onProgress(fmt.Sprintf("Sent DFU command to %s:%s.", host, port)) + onProgress(fmt.Sprintf("Sent DFU command to %s:%s", host, port)) return nil } diff --git a/src/dividat-driver/senso/update_firmware.go b/src/dividat-driver/senso/update_firmware.go index eabdafa2..b68bddd2 100644 --- a/src/dividat-driver/senso/update_firmware.go +++ b/src/dividat-driver/senso/update_firmware.go @@ -40,7 +40,7 @@ func (handle *Handle) ProcessFirmwareUpdateRequest(command UpdateFirmware, send send.failure(failureMsg) handle.log.Error(failureMsg) } else { - send.success("Firmware successfully transmitted.") + send.success("Firmware successfully transmitted") } handle.firmwareUpdate.SetUpdating(false) }