diff --git a/.licenses/go/github.com/fxamacker/cbor/v2.dep.yml b/.licenses/go/github.com/fxamacker/cbor/v2.dep.yml new file mode 100644 index 00000000..ac73f337 --- /dev/null +++ b/.licenses/go/github.com/fxamacker/cbor/v2.dep.yml @@ -0,0 +1,41 @@ +--- +name: github.com/fxamacker/cbor/v2 +version: v2.8.0 +type: go +summary: Package cbor is a modern CBOR codec (RFC 8949 & RFC 8742) with CBOR tags, + Go struct tag options (toarray/keyasint/omitempty/omitzero), Core Deterministic + Encoding, CTAP2, Canonical CBOR, float64->32->16, and duplicate map key detection. +homepage: https://pkg.go.dev/github.com/fxamacker/cbor/v2 +license: mit +licenses: +- sources: LICENSE + text: |- + MIT License + + Copyright (c) 2019-present Faye Amacker + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +- sources: README.md + text: |- + Copyright © 2019-2024 [Faye Amacker](https://github.com/fxamacker). + + fxamacker/cbor is licensed under the MIT License. See [LICENSE](LICENSE) for the full license text. + +
+notices: [] diff --git a/.licenses/go/github.com/x448/float16.dep.yml b/.licenses/go/github.com/x448/float16.dep.yml new file mode 100644 index 00000000..901125f9 --- /dev/null +++ b/.licenses/go/github.com/x448/float16.dep.yml @@ -0,0 +1,38 @@ +--- +name: github.com/x448/float16 +version: v0.8.4 +type: go +summary: +homepage: https://pkg.go.dev/github.com/x448/float16 +license: mit +licenses: +- sources: LICENSE + text: |+ + MIT License + + Copyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +- sources: README.md + text: |- + Copyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker + + Licensed under [MIT License](LICENSE) +notices: [] diff --git a/cli/device/configure.go b/cli/device/configure.go new file mode 100644 index 00000000..c8834640 --- /dev/null +++ b/cli/device/configure.go @@ -0,0 +1,220 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package device + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "os" + "strings" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/device" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.bug.st/cleanup" +) + +type netConfigurationFlags struct { + port string + connectionType int32 + fqbn string + configFile string +} + +func initConfigureCommand() *cobra.Command { + flags := &netConfigurationFlags{} + createCommand := &cobra.Command{ + Use: "configure", + Short: "Configure the network settings of a device running a sketch with the Network Configurator lib enabled", + Long: "Configure the network settings of a device running a sketch with the Network Configurator lib enabled", + Run: func(cmd *cobra.Command, args []string) { + if err := runConfigureCommand(flags); err != nil { + feedback.Errorf("Error during device configuration: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + }, + } + createCommand.Flags().StringVarP(&flags.port, "port", "p", "", "Device port") + createCommand.Flags().StringVarP(&flags.fqbn, "fqbn", "b", "", "Device fqbn") + createCommand.Flags().Int32VarP(&flags.connectionType, "connection", "c", 0, "Device connection type (1: WiFi, 2: Ethernet, 3: NB-IoT, 4: GSM, 5: LoRaWan, 6:CAT-M1, 7: Cellular)") + createCommand.Flags().StringVarP(&flags.configFile, "config-file", "f", "", "Path to the configuration file (optional). View online documentation for the format") + createCommand.MarkFlagRequired("connection") + + return createCommand +} + +func runConfigureCommand(flags *netConfigurationFlags) error { + logrus.Infof("Configuring device with connection type %d", flags.connectionType) + + netParams := &device.NetConfig{ + Type: flags.connectionType, + } + + if flags.configFile != "" { + file, err := os.ReadFile(flags.configFile) + if err != nil { + logrus.Errorf("Error reading file %s: %v", flags.configFile, err) + return err + } + err = json.Unmarshal(file, &netParams) + if err != nil { + logrus.Errorf("Error parsing JSON from file %s: %v", flags.configFile, err) + return err + } + } else { + feedback.Print("Insert network configuration") + getInputFromMenu(netParams) + } + + boardFilterParams := &device.CreateParams{} + + if flags.port != "" { + boardFilterParams.Port = &flags.port + } + if flags.fqbn != "" { + boardFilterParams.FQBN = &flags.fqbn + } + + ctx, cancel := cleanup.InterruptableContext(context.Background()) + defer cancel() + feedback.Print("Starting network configuration...") + err := device.NetConfigure(ctx, boardFilterParams, netParams) + if err != nil { + return err + } + feedback.Print("Network configuration successfully completed.") + return nil +} + +func getInputFromMenu(config *device.NetConfig) error { + + switch config.Type { + case 1: + config.WiFi = getWiFiSetting() + case 2: + config.Eth = getEthernetSetting() + case 3: + config.NB = getCellularSetting() + case 4: + config.GSM = getCellularSetting() + case 5: + config.Lora = getLoraSetting() + case 6: + config.CATM1 = getCatM1Setting() + case 7: + config.CellularSetting = getCellularSetting() + default: + return errors.New("invalid connection type, please try again") + } + return nil +} + +func getWiFiSetting() device.WiFiSetting { + var wifi device.WiFiSetting + fmt.Print("Enter SSID: ") + fmt.Scanln(&wifi.SSID) + fmt.Print("Enter Password: ") + fmt.Scanln(&wifi.PWD) + return wifi +} + +func getEthernetSetting() device.EthernetSetting { + var eth device.EthernetSetting + fmt.Println("Do you want to use DHCP? (yes/no): ") + var useDHCP string + fmt.Scanln(&useDHCP) + if useDHCP == "yes" || useDHCP == "y" { + eth.IP = device.IPAddr{Type: 0, Bytes: [16]byte{}} + eth.Gateway = device.IPAddr{Type: 0, Bytes: [16]byte{}} + eth.Netmask = device.IPAddr{Type: 0, Bytes: [16]byte{}} + eth.DNS = device.IPAddr{Type: 0, Bytes: [16]byte{}} + } else { + fmt.Println("Enter IP Address: ") + eth.IP = getIPAddr() + fmt.Println("Enter DNS: ") + eth.DNS = getIPAddr() + fmt.Println("Enter Gateway: ") + eth.Gateway = getIPAddr() + fmt.Println("Enter Netmask: ") + eth.Netmask = getIPAddr() + } + + return eth +} + +func getIPAddr() device.IPAddr { + var ip device.IPAddr + var ipString string + fmt.Scanln(&ipString) + if ipString == "" { + return ip + } + if strings.Count(ipString, ":") > 0 { + ip.Type = 1 // IPv6 + } else { + ip.Type = 0 // IPv4 + } + ip.Bytes = [16]byte(net.ParseIP(ipString).To16()) + return ip +} + +func getCellularSetting() device.CellularSetting { + var cellular device.CellularSetting + fmt.Println("Enter PIN: ") + fmt.Scanln(&cellular.PIN) + fmt.Print("Enter APN: ") + fmt.Scanln(&cellular.APN) + fmt.Print("Enter Login: ") + fmt.Scanln(&cellular.Login) + fmt.Print("Enter Password: ") + fmt.Scanln(&cellular.Pass) + return cellular +} + +func getCatM1Setting() device.CATM1Setting { + var catm1 device.CATM1Setting + fmt.Print("Enter PIN: ") + fmt.Scanln(&catm1.PIN) + fmt.Print("Enter APN: ") + fmt.Scanln(&catm1.APN) + fmt.Print("Enter Login: ") + fmt.Scanln(&catm1.Login) + fmt.Print("Enter Password: ") + fmt.Scanln(&catm1.Pass) + return catm1 +} + +func getLoraSetting() device.LoraSetting { + var lora device.LoraSetting + fmt.Print("Enter AppEUI: ") + fmt.Scanln(&lora.AppEUI) + fmt.Print("Enter AppKey: ") + fmt.Scanln(&lora.AppKey) + fmt.Print("Enter Band (Byte hex format): ") + fmt.Scanln(&lora.Band) + fmt.Print("Enter Channel Mask: ") + fmt.Scanln(&lora.ChannelMask) + fmt.Print("Enter Device Class: ") + fmt.Scanln(&lora.DeviceClass) + return lora +} diff --git a/cli/device/device.go b/cli/device/device.go index 854336e3..07359a8f 100644 --- a/cli/device/device.go +++ b/cli/device/device.go @@ -30,6 +30,7 @@ func NewCommand() *cobra.Command { } deviceCommand.AddCommand(initCreateCommand()) + deviceCommand.AddCommand(initConfigureCommand()) deviceCommand.AddCommand(initListCommand()) deviceCommand.AddCommand(initShowCommand()) deviceCommand.AddCommand(initDeleteCommand()) diff --git a/command/device/configure.go b/command/device/configure.go new file mode 100644 index 00000000..f8f642d5 --- /dev/null +++ b/command/device/configure.go @@ -0,0 +1,399 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package device + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/arduino/arduino-cloud-cli/arduino/cli" + configurationprotocol "github.com/arduino/arduino-cloud-cli/internal/board-protocols/configuration-protocol" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/configuration-protocol/cborcoders" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport" + "github.com/arduino/arduino-cloud-cli/internal/serial" + "github.com/sirupsen/logrus" +) + +func NetConfigure(ctx context.Context, boardFilters *CreateParams, NetConfig *NetConfig) error { + comm, err := cli.NewCommander() + if err != nil { + return err + } + + ports, err := comm.BoardList(ctx) + if err != nil { + return err + } + + board := boardFromPorts(ports, boardFilters) + if board == nil { + err = errors.New("no board found") + return err + } + var extInterface transport.TransportInterface + extInterface = &serial.Serial{} + configProtocol := configurationprotocol.NewNetworkConfigurationProtocol(&extInterface) + + err = configProtocol.Connect(board.address) + if err != nil { + return err + } + + nc := NewNetworkConfigure(extInterface) + err = nc.Run(ctx, NetConfig) + + return err +} + +type ConfigStatus int + +// This enum represents the different states of the network configuration process +// of the Arduino Board Configuration Protocol. +const ( + NoneState ConfigStatus = iota + WaitForConnection + WaitingForInitialStatus + WaitingForNetworkOptions + ConfigureNetwork + SendConnectionRequest + WaitingForConnectionCommandResult + WaitingForNetworkConfigResult + End +) + +type NetworkConfigure struct { + state ConfigStatus + extInterface transport.TransportInterface + configProtocol *configurationprotocol.NetworkConfigurationProtocol +} + +func NewNetworkConfigure(extInterface transport.TransportInterface) *NetworkConfigure { + return &NetworkConfigure{ + extInterface: extInterface, + configProtocol: configurationprotocol.NewNetworkConfigurationProtocol(&extInterface), + } +} + +func (nc *NetworkConfigure) Run(ctx context.Context, netConfig *NetConfig) error { + nc.state = WaitForConnection + var err error + for nc.state != End { + + switch nc.state { + case WaitForConnection: + err = nc.waitForConnection() + if err != nil { + nc.state = End + } + case WaitingForInitialStatus: + err = nc.waitingForInitialStatus() + if err != nil { + nc.state = End + } + case WaitingForNetworkOptions: + err = nc.waitingForNetworkOptions() + if err != nil { + nc.state = End + } + case ConfigureNetwork: + err = nc.configureNetwork(ctx, netConfig) + if err != nil { + nc.state = End + } + case SendConnectionRequest: + err = nc.sendConnectionRequest() + if err != nil { + nc.state = End + } + case WaitingForConnectionCommandResult: + err = nc.waitingForConnectionCommandResult() + if err != nil { + nc.state = End + } + case WaitingForNetworkConfigResult: + err = nc.waitingForNetworkConfigResult() + if err != nil { + nc.state = End + } + } + + } + + nc.configProtocol.Close() + return err +} + +func (nc *NetworkConfigure) waitForConnection() error { + if nc.extInterface.Connected() { + nc.state = WaitingForInitialStatus + } + return nil +} + +func (nc *NetworkConfigure) waitingForInitialStatus() error { + logrus.Info("NetworkConfigure: waiting for initial status from device") + res, err := nc.configProtocol.ReceiveData(30) + if err != nil { + return fmt.Errorf("communication error: %w, please check the NetworkConfigurator lib is activated in the sketch", err) + } + + if res == nil { + nc.state = WaitingForNetworkOptions + } else if res.Type() == cborcoders.ProvisioningStatusMessageType { + status := res.ToProvisioningStatusMessage() + if status.Status == 1 { + nc.state = WaitingForInitialStatus + } else if status.Status == -6 || status.Status <= -101 { + newState, err := nc.handleStatusMessage(status.Status) + if err != nil { + return err + } + if newState != NoneState { + nc.state = newState + } + } else { + nc.state = WaitingForNetworkOptions + } + + } else if res.Type() == cborcoders.WiFiNetworksType { + nc.state = ConfigureNetwork + } + + return nil +} + +// In this state the cli is waiting for the available network options as specified in the +// Arduino Board Configuration Protocol. +func (nc *NetworkConfigure) waitingForNetworkOptions() error { + logrus.Info("NetworkConfigure: waiting for network options from device") + res, err := nc.configProtocol.ReceiveData(30) + if err != nil { + return err + } + + if res != nil { + // At the moment of writing, the only type of message that can be received in this state is the + // WiFiNetworksType, which contains the available WiFi networks list. + if res.Type() == cborcoders.WiFiNetworksType { + nc.state = ConfigureNetwork + } else if res.Type() == cborcoders.ProvisioningStatusMessageType { + status := res.ToProvisioningStatusMessage() + if status.Status == 1 { + nc.state = WaitingForInitialStatus + } else { + newState, err := nc.handleStatusMessage(status.Status) + if err != nil { + return err + } + if newState != NoneState { + nc.state = newState + } + } + } + } + + return nil +} + +func (nc *NetworkConfigure) configureNetwork(ctx context.Context, c *NetConfig) error { + var cmd cborcoders.Cmd + if c.Type == 1 { // WiFi + cmd = cborcoders.From(cborcoders.ProvisioningWifiConfigMessage{ + SSID: c.WiFi.SSID, + PWD: c.WiFi.PWD, + }) + } else if c.Type == 2 { // Ethernet + cmd = cborcoders.From(cborcoders.ProvisioningEthernetConfigMessage{ + Static_ip: c.Eth.IP.Bytes[:], + Dns: c.Eth.DNS.Bytes[:], + Gateway: c.Eth.Gateway.Bytes[:], + Netmask: c.Eth.Netmask.Bytes[:], + Timeout: c.Eth.Timeout, + ResponseTimeout: c.Eth.ResponseTimeout, + }) + } else if c.Type == 3 { // NB-IoT + cmd = cborcoders.From(cborcoders.ProvisioningNBConfigMessage{ + PIN: c.NB.PIN, + Apn: c.NB.APN, + Login: c.NB.Login, + Pass: c.NB.Pass, + }) + } else if c.Type == 4 { // GSM + cmd = cborcoders.From(cborcoders.ProvisioningGSMConfigMessage{ + PIN: c.GSM.PIN, + Apn: c.GSM.APN, + Login: c.GSM.Login, + Pass: c.GSM.Pass, + }) + } else if c.Type == 5 { // LoRa + cmd = cborcoders.From(cborcoders.ProvisioningLoRaConfigMessage{ + AppEui: c.Lora.AppEUI, + AppKey: c.Lora.AppKey, + Band: c.Lora.Band, + ChannelMask: c.Lora.ChannelMask, + DeviceClass: c.Lora.DeviceClass, + }) + } else if c.Type == 6 { // CAT-M1 + cmd = cborcoders.From(cborcoders.ProvisioningCATM1ConfigMessage{ + PIN: c.CATM1.PIN, + Apn: c.CATM1.APN, + Login: c.CATM1.Login, + Pass: c.CATM1.Pass, + Band: c.CATM1.Band, + }) + } else if c.Type == 7 { // Cellular + cmd = cborcoders.From(cborcoders.ProvisioningCellularConfigMessage{ + PIN: c.CellularSetting.PIN, + Apn: c.CellularSetting.APN, + Login: c.CellularSetting.Login, + Pass: c.CellularSetting.Pass, + }) + } else { + return errors.New("invalid configuration type") + } + + err := nc.configProtocol.SendData(cmd) + if err != nil { + return err + } + + nc.state = SendConnectionRequest + sleepCtx(ctx, 1*time.Second) + return nil +} + +func (nc *NetworkConfigure) sendConnectionRequest() error { + connectMessage := cborcoders.From(cborcoders.ProvisioningCommandsMessage{Command: configurationprotocol.Commands["Connect"]}) + err := nc.configProtocol.SendData(connectMessage) + if err != nil { + return err + } + nc.state = WaitingForConnectionCommandResult + return nil +} + +func (nc *NetworkConfigure) waitingForConnectionCommandResult() error { + res, err := nc.configProtocol.ReceiveData(60) + if err != nil { + return err + } + + if res != nil && res.Type() == cborcoders.ProvisioningStatusMessageType { + status := res.ToProvisioningStatusMessage() + if status.Status == 1 { + nc.state = WaitingForNetworkConfigResult + } else { + newState, err := nc.handleStatusMessage(status.Status) + if err != nil { + return err + } + if newState != NoneState { + nc.state = newState + } + } + } + + return nil +} + +func (nc *NetworkConfigure) waitingForNetworkConfigResult() error { + res, err := nc.configProtocol.ReceiveData(200) + if err != nil { + return err + } + + if res != nil && res.Type() == cborcoders.ProvisioningStatusMessageType { + status := res.ToProvisioningStatusMessage() + + if status.Status == 2 { + nc.state = End + } else { + newState, err := nc.handleStatusMessage(status.Status) + if err != nil { + return err + } + if newState != NoneState { + nc.state = newState + } + } + } + + return nil +} + +func (nc *NetworkConfigure) printNetworkOption(msg *cborcoders.Cmd) { + if msg.Type() == cborcoders.WiFiNetworksType { + networks := msg.ToWiFiNetworks() + for _, network := range networks { + fmt.Printf("SSID: %s, RSSI %d \n", network.SSID, network.RSSI) + } + } +} + +func (nc *NetworkConfigure) handleStatusMessage(status int16) (ConfigStatus, error) { + statusMessage := configurationprotocol.StatusBoard[status] + logrus.Debugf("NetworkConfigure: status message received: %s", statusMessage) + + switch statusMessage { + case "Connecting": + return NoneState, nil + case "Connected": + return NoneState, nil + case "Resetted": + return NoneState, nil + case "Scanning for WiFi networks": + return WaitingForNetworkOptions, nil + case "Failed to connect": + return NoneState, errors.New("connection failed invalid credentials or network configuration") + case "Disconnected": + return NoneState, nil + case "Parameters not provided": + return ConfigureNetwork, nil + case "Invalid parameters": + return NoneState, errors.New("the provided parameters for network configuration are invalid") + case "Cannot execute anew request while another is pending": + return NoneState, errors.New("board is busy, restart the board and try again") + case "Invalid request": + return NoneState, errors.New("invalid request sent to the board") + case "Internet not available": + return NoneState, errors.New("internet not available, check your network connection") + case "HW Error connectivity module": + return NoneState, errors.New("hardware error in connectivity module, check the board") + case "HW Connectivity Module stopped": + return NoneState, errors.New("hardware connectivity module stopped, restart the board and check your sketch") + case "Error initializing secure element": + return NoneState, errors.New("error initializing secure element, check the board and try again") + case "Error configuring secure element": + return NoneState, errors.New("error configuring secure element, check the board and try again") + case "Error locking secure element": + return NoneState, errors.New("error locking secure element, check the board and try again") + case "Error generating UHWID": + return NoneState, errors.New("error generating UHWID, check the board and try again") + case "Error storage begin module": + return NoneState, errors.New("error beginning storage module, check the board storage partitioning and try again") + case "Fail to partition the storage": + return NoneState, errors.New("failed to partition the storage, check the board storage and try again") + case "Generic error": + return NoneState, errors.New("generic error, check the board and try again") + default: + return NoneState, errors.New("generic error, check the board and try again") + } + +} diff --git a/command/device/network_options.go b/command/device/network_options.go new file mode 100644 index 00000000..64a63c0f --- /dev/null +++ b/command/device/network_options.go @@ -0,0 +1,71 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package device + +type WiFiSetting struct { + SSID string `json:"ssid"` // Max length of ssid is 32 + \0 + PWD string `json:"pwd"` // Max length of password is 63 + \0 +} + +type IPAddr struct { + Type uint8 `json:"type"` + Bytes [16]byte `json:"bytes"` +} + +type EthernetSetting struct { + IP IPAddr `json:"ip"` + DNS IPAddr `json:"dns"` + Gateway IPAddr `json:"gateway"` + Netmask IPAddr `json:"netmask"` + Timeout uint `json:"timeout"` + ResponseTimeout uint `json:"response_timeout"` +} + +type CellularSetting struct { + PIN string `json:"pin"` // Max length of pin is 8 + \0 + APN string `json:"apn"` // Max length of apn is 100 + \0 + Login string `json:"login"` // Max length of login is 32 + \0 + Pass string `json:"pass"` // Max length of pass is 32 + \0 +} + +type CATM1Setting struct { + PIN string `json:"pin"` // Max length of pin is 8 + \0 + APN string `json:"apn"` // Max length of apn is 100 + \0 + Login string `json:"login"` // Max length of login is 32 + \0 + Pass string `json:"pass"` // Max length of pass is 32 + \0 + Band [4]uint32 `json:"band"` +} + +type LoraSetting struct { + AppEUI string `json:"appeui"` // appeui is 8 octets * 2 (hex format) + \0 + AppKey string `json:"appkey"` // appeui is 16 octets * 2 (hex format) + \0 + Band uint8 `json:"band"` + ChannelMask string `json:"channel_mask"` + DeviceClass string `json:"device_class"` +} + +type NetConfig struct { + Type int32 `json:"type"` + WiFi WiFiSetting `json:"wifi,omitempty"` + Eth EthernetSetting `json:"eth,omitempty"` + NB CellularSetting `json:"nb,omitempty"` + GSM CellularSetting `json:"gsm,omitempty"` + CATM1 CATM1Setting `json:"catm1,omitempty"` + CellularSetting CellularSetting `json:"cellular,omitempty"` + Lora LoraSetting `json:"lora,omitempty"` +} diff --git a/command/device/provision.go b/command/device/provision.go index 89ac0cab..62da9ba4 100644 --- a/command/device/provision.go +++ b/command/device/provision.go @@ -27,6 +27,8 @@ import ( "github.com/arduino/arduino-cloud-cli/arduino" "github.com/arduino/arduino-cloud-cli/internal/binary" + provisioningprotocol "github.com/arduino/arduino-cloud-cli/internal/board-protocols/provisioning-protocol" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport" "github.com/arduino/arduino-cloud-cli/internal/serial" "github.com/arduino/go-paths-helper" iotclient "github.com/arduino/iot-client-go/v3" @@ -72,10 +74,11 @@ type certificateCreator interface { // procedures for boards with crypto-chip. type provision struct { arduino.Commander - cert certificateCreator - ser *serial.Serial - board *board - id string + cert certificateCreator + serial transport.TransportInterface + provProt *provisioningprotocol.ProvisioningProtocol + board *board + id string } // run provisioning procedure for boards with crypto-chip. @@ -104,15 +107,21 @@ func (p provision) run(ctx context.Context) error { if err = sleepCtx(ctx, 1500*time.Millisecond); err != nil { return err } - p.ser = serial.NewSerial() + p.serial = serial.NewSerial() + + p.provProt = provisioningprotocol.NewProvisioningProtocol(&p.serial) errMsg = "Error while connecting to the board" err = retry(ctx, 5, time.Millisecond*1000, errMsg, func() error { - return p.ser.Connect(p.board.address) + params := transport.TransportInterfaceParams{ + Port: p.board.address, + BoundRate: 57600, + } + return p.serial.Connect(params) }) if err != nil { return err } - defer p.ser.Close() + defer p.serial.Close() logrus.Infof("%s\n\n", "Connected to the board") // Wait some time before using the serial port @@ -131,7 +140,7 @@ func (p provision) run(ctx context.Context) error { func (p provision) configBoard(ctx context.Context) error { logrus.Info("Receiving the certificate") - csr, err := p.ser.SendReceive(ctx, serial.CSR, []byte(p.id)) + csr, err := p.provProt.SendReceive(ctx, provisioningprotocol.CSR, []byte(p.id)) if err != nil { return err } @@ -141,37 +150,37 @@ func (p provision) configBoard(ctx context.Context) error { } logrus.Info("Requesting begin storage") - if err = p.ser.Send(ctx, serial.BeginStorage, nil); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.BeginStorage, nil); err != nil { return err } s := strconv.Itoa(cert.NotBefore.Year()) logrus.Info("Sending year: ", s) - if err = p.ser.Send(ctx, serial.SetYear, []byte(s)); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.SetYear, []byte(s)); err != nil { return err } s = fmt.Sprintf("%02d", int(cert.NotBefore.Month())) logrus.Info("Sending month: ", s) - if err = p.ser.Send(ctx, serial.SetMonth, []byte(s)); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.SetMonth, []byte(s)); err != nil { return err } s = fmt.Sprintf("%02d", cert.NotBefore.Day()) logrus.Info("Sending day: ", s) - if err = p.ser.Send(ctx, serial.SetDay, []byte(s)); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.SetDay, []byte(s)); err != nil { return err } s = fmt.Sprintf("%02d", cert.NotBefore.Hour()) logrus.Info("Sending hour: ", s) - if err = p.ser.Send(ctx, serial.SetHour, []byte(s)); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.SetHour, []byte(s)); err != nil { return err } s = strconv.Itoa(31) logrus.Info("Sending validity: ", s) - if err = p.ser.Send(ctx, serial.SetValidity, []byte(s)); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.SetValidity, []byte(s)); err != nil { return err } @@ -180,7 +189,7 @@ func (p provision) configBoard(ctx context.Context) error { if err != nil { return fmt.Errorf("decoding certificate serial: %w", err) } - if err = p.ser.Send(ctx, serial.SetCertSerial, b); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.SetCertSerial, b); err != nil { return err } @@ -189,7 +198,7 @@ func (p provision) configBoard(ctx context.Context) error { if err != nil { return fmt.Errorf("decoding certificate authority key id: %w", err) } - if err = p.ser.Send(ctx, serial.SetAuthKey, b); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.SetAuthKey, b); err != nil { return err } @@ -199,7 +208,7 @@ func (p provision) configBoard(ctx context.Context) error { err = fmt.Errorf("decoding certificate signature: %w", err) return err } - if err = p.ser.Send(ctx, serial.SetSignature, b); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.SetSignature, b); err != nil { return err } @@ -208,7 +217,7 @@ func (p provision) configBoard(ctx context.Context) error { } logrus.Info("Requesting end storage") - if err = p.ser.Send(ctx, serial.EndStorage, nil); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.EndStorage, nil); err != nil { return err } @@ -217,7 +226,7 @@ func (p provision) configBoard(ctx context.Context) error { } logrus.Info("Requesting certificate reconstruction") - if err = p.ser.Send(ctx, serial.ReconstructCert, nil); err != nil { + if err = p.provProt.Send(ctx, provisioningprotocol.ReconstructCert, nil); err != nil { return err } diff --git a/go.mod b/go.mod index e57b7f98..99b2ec48 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -67,6 +68,7 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.bug.st/downloader/v2 v2.1.1 // indirect go.bug.st/relaxed-semver v0.9.0 // indirect diff --git a/go.sum b/go.sum index 0601b9a9..4b8560ba 100644 --- a/go.sum +++ b/go.sum @@ -142,6 +142,8 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= @@ -438,6 +440,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= diff --git a/internal/board-protocols/configuration-protocol/cborcoders/enc_dec.go b/internal/board-protocols/configuration-protocol/cborcoders/enc_dec.go new file mode 100644 index 00000000..b76aff8f --- /dev/null +++ b/internal/board-protocols/configuration-protocol/cborcoders/enc_dec.go @@ -0,0 +1,276 @@ +package cborcoders + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "slices" + + "github.com/fxamacker/cbor/v2" +) + +var _dm cbor.DecMode +var _em cbor.EncMode + +var wifiListBeginMessage = []byte{0xda, 0x00, 0x01, 0x20, 0x01} + +// Provisioning commands +var ProvisioningStatusMessageType = reflect.TypeOf(ProvisioningStatusMessage{}) +var WiFiNetworksType = reflect.TypeOf(WiFiNetworks{}) +var ProvisioningUniqueIdMessageType = reflect.TypeOf(ProvisioningUniqueIdMessage{}) +var ProvisioningBLEMacAddressMessageType = reflect.TypeOf(ProvisioningBLEMacAddressMessage{}) +var ProvisioningWiFiFWVersionMessageType = reflect.TypeOf(ProvisioningWiFiFWVersionMessage{}) +var ProvisioningSketchVersionMessageType = reflect.TypeOf(ProvisioningSketchVersionMessage{}) +var ProvisioningNetConfigLibVersionMessageType = reflect.TypeOf(ProvisioningNetworkConfigLibVersionMessage{}) +var ProvisioningSignatureMessageType = reflect.TypeOf(ProvisioningSignatureMessage{}) +var ProvisioningPublicKeyMessageType = reflect.TypeOf(ProvisioningPublicKeyMessage{}) +var ProvisioningTimestampMessageType = reflect.TypeOf(ProvisioningTimestampMessage{}) +var ProvisioningCommandsMessageType = reflect.TypeOf(ProvisioningCommandsMessage{}) +var ProvisioningWifiConfigMessageType = reflect.TypeOf(ProvisioningWifiConfigMessage{}) +var ProvisioningLoRaConfigMessageType = reflect.TypeOf(ProvisioningLoRaConfigMessage{}) +var ProvisioningGSMConfigMessageType = reflect.TypeOf(ProvisioningGSMConfigMessage{}) +var ProvisioningNBIoTConfigMessageType = reflect.TypeOf(ProvisioningNBConfigMessage{}) +var ProvisioningCATM1ConfigMessageType = reflect.TypeOf(ProvisioningCATM1ConfigMessage{}) +var ProvisioningEthernetConfigMessageType = reflect.TypeOf(ProvisioningEthernetConfigMessage{}) +var ProvisioningCellularConfigMessageType = reflect.TypeOf(ProvisioningCellularConfigMessage{}) + +type tag struct { + tag uint64 + ty reflect.Type +} + +var tagCommands = []tag{ + // provisioning commands + {0x012000, ProvisioningStatusMessageType}, + {0x012001, WiFiNetworksType}, + {0x012013, ProvisioningBLEMacAddressMessageType}, + {0x012014, ProvisioningWiFiFWVersionMessageType}, + {0x012015, ProvisioningSketchVersionMessageType}, + {0x012016, ProvisioningNetConfigLibVersionMessageType}, + {0x012010, ProvisioningUniqueIdMessageType}, + {0x012011, ProvisioningSignatureMessageType}, + {0x012017, ProvisioningPublicKeyMessageType}, + {0x012002, ProvisioningTimestampMessageType}, + {0x012003, ProvisioningCommandsMessageType}, + {0x012004, ProvisioningWifiConfigMessageType}, + {0x012005, ProvisioningLoRaConfigMessageType}, + {0x012006, ProvisioningGSMConfigMessageType}, + {0x012007, ProvisioningNBIoTConfigMessageType}, + {0x012008, ProvisioningCATM1ConfigMessageType}, + {0x012009, ProvisioningEthernetConfigMessageType}, + {0x012012, ProvisioningCellularConfigMessageType}, +} + +func init() { + tags := cbor.NewTagSet() + for _, t := range tagCommands { + err := tags.Add( + cbor.TagOptions{EncTag: cbor.EncTagRequired, DecTag: cbor.DecTagRequired}, + t.ty, + t.tag) + if err != nil { + panic(err) + } + } + var err error + _dm, err = cbor.DecOptions{}.DecModeWithTags(tags) + if err != nil { + panic(err) + } + + _em, err = cbor.EncOptions{IndefLength: 1}.EncModeWithTags(tags) + + if err != nil { + panic(err) + } +} + +type Cmd struct { + inner interface{} +} + +func getCBORType(data byte) byte { + return data & 0xe0 +} + +func getCBORFieldLength(data []byte) (len, i int) { + additional_information := data[0] & 0x1f + if additional_information <= 23 { + return int(additional_information), 0 + } else if additional_information == 24 { + return int(data[1]), 1 + } else if additional_information == 25 { + return int(data[1])<<8 | int(data[2]), 2 + } + return 0, 0 +} + +func getString(data []byte, len int) string { + return string(data[:len]) +} + +func DecodeWiFiNetworks(data []byte, wf *WiFiNetworks) error { + len := len(data) + if len == 0 { + return errors.New("empty data") + } + networks := []WiFiNetwork{} + i := 0 + for i < len { + if getCBORType(data[i]) != 0x80 { //check if it's an array + return errors.New("invalid data not initial array") + } + + array_length, l := getCBORFieldLength(data[i:]) + i += l + i++ + for j := 0; j < array_length; j = j + 2 { + if getCBORType(data[i]) != 0x60 { //check if it's a text string + return errors.New("invalid data not text string") + } + + text_length, l := getCBORFieldLength(data[i:]) + i += l + i++ + ssid := getString(data[i:], text_length) + i += text_length + if getCBORType(data[i]) != 0x20 { //check if it's a negative number + return errors.New("invalid data not negative number") + } + val, l := getCBORFieldLength(data[i:]) + rssi := int(-1) ^ int(val) + i += l + i++ + networks = append(networks, WiFiNetwork{SSID: ssid, RSSI: rssi}) + } + } + *wf = networks + + return nil +} + +func Decode(message []byte) (cmd Cmd, err error) { + c := Cmd{} + if bytes.Equal(message[0:5], wifiListBeginMessage) { + wf := WiFiNetworks{} + e := DecodeWiFiNetworks(message[5:], &wf) + c.inner = wf + return c, e + } + + if err := _dm.Unmarshal(message, &c.inner); err != nil { + return Cmd{}, err + } + + match := slices.ContainsFunc(tagCommands, func(t tag) bool { + return t.ty == c.Type() + }) + if !match { + return Cmd{}, fmt.Errorf("unknown command type: %v", c.Type()) + } + + return c, nil + +} + +func (c Cmd) Encode() ([]byte, error) { + p, err := _em.Marshal(c.inner) + if err != nil { + return nil, err + } + return p, nil +} + +func (c Cmd) String() string { + idx := slices.IndexFunc(tagCommands, func(t tag) bool { + return t.ty == c.Type() + }) + return fmt.Sprintf("%x=%+v", tagCommands[idx].tag, c.inner) +} + +func From[T ProvisioningStatusMessage | WiFiNetworks | ProvisioningBLEMacAddressMessage | ProvisioningWiFiFWVersionMessage | + ProvisioningSketchVersionMessage | ProvisioningNetworkConfigLibVersionMessage | ProvisioningUniqueIdMessage | + ProvisioningSignatureMessage | ProvisioningPublicKeyMessage | ProvisioningTimestampMessage | + ProvisioningCommandsMessage | ProvisioningWifiConfigMessage | + ProvisioningLoRaConfigMessage | ProvisioningCellularConfigMessage | + ProvisioningEthernetConfigMessage | ProvisioningCATM1ConfigMessage | + ProvisioningGSMConfigMessage | ProvisioningNBConfigMessage](c T) Cmd { + return Cmd{inner: c} +} + +func (c Cmd) Type() reflect.Type { + return reflect.TypeOf(c.inner) +} + +func (c Cmd) ToProvisioningStatusMessage() ProvisioningStatusMessage { + return c.inner.(ProvisioningStatusMessage) +} + +func (c Cmd) ToWiFiNetworks() WiFiNetworks { + return c.inner.(WiFiNetworks) +} + +func (c Cmd) ToProvisioningBLEMacAddressMessage() ProvisioningBLEMacAddressMessage { + return c.inner.(ProvisioningBLEMacAddressMessage) +} + +func (c Cmd) ToProvisioningWiFiFWVersionMessage() ProvisioningWiFiFWVersionMessage { + return c.inner.(ProvisioningWiFiFWVersionMessage) +} + +func (c Cmd) ToProvisioningSketchVersionMessage() ProvisioningSketchVersionMessage { + return c.inner.(ProvisioningSketchVersionMessage) +} + +func (c Cmd) ToProvisioningNetworkConfigLibVersionMessage() ProvisioningNetworkConfigLibVersionMessage { + return c.inner.(ProvisioningNetworkConfigLibVersionMessage) +} + +func (c Cmd) ToProvisioningUniqueIdMessage() ProvisioningUniqueIdMessage { + return c.inner.(ProvisioningUniqueIdMessage) +} + +func (c Cmd) ToProvisioningSignatureMessage() ProvisioningSignatureMessage { + return c.inner.(ProvisioningSignatureMessage) +} + +func (c Cmd) ToProvisioningPublicKeyMessage() ProvisioningPublicKeyMessage { + return c.inner.(ProvisioningPublicKeyMessage) +} + +func (c Cmd) ToProvisioningTimestampMessage() ProvisioningTimestampMessage { + return c.inner.(ProvisioningTimestampMessage) +} + +func (c Cmd) ToProvisioningCommandsMessage() ProvisioningCommandsMessage { + return c.inner.(ProvisioningCommandsMessage) +} + +func (c Cmd) ToProvisioningWifiConfigMessage() ProvisioningWifiConfigMessage { + return c.inner.(ProvisioningWifiConfigMessage) +} + +func (c Cmd) ToProvisioningLoRaConfigMessage() ProvisioningLoRaConfigMessage { + return c.inner.(ProvisioningLoRaConfigMessage) +} + +func (c Cmd) ToProvisioningCellularConfigMessage() ProvisioningCellularConfigMessage { + return c.inner.(ProvisioningCellularConfigMessage) +} + +func (c Cmd) ToProvisioningEthernetConfigMessage() ProvisioningEthernetConfigMessage { + return c.inner.(ProvisioningEthernetConfigMessage) +} + +func (c Cmd) ToProvisioningCATM1ConfigMessage() ProvisioningCATM1ConfigMessage { + return c.inner.(ProvisioningCATM1ConfigMessage) +} + +func (c Cmd) ToProvisioningGSMConfigMessage() ProvisioningGSMConfigMessage { + return c.inner.(ProvisioningGSMConfigMessage) +} + +func (c Cmd) ToProvisioningNBConfigMessage() ProvisioningNBConfigMessage { + return c.inner.(ProvisioningNBConfigMessage) +} diff --git a/internal/board-protocols/configuration-protocol/cborcoders/enc_dec_test.go b/internal/board-protocols/configuration-protocol/cborcoders/enc_dec_test.go new file mode 100644 index 00000000..f0f47bdc --- /dev/null +++ b/internal/board-protocols/configuration-protocol/cborcoders/enc_dec_test.go @@ -0,0 +1,252 @@ +package cborcoders + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecode(t *testing.T) { + fixedTime := uint64(1709208245) // time.Date(2024, 2, 29, 12, 4, 5, 0, time.UTC) + + tests := []struct { + name string + in Cmd + want string + }{ + { + name: "provisioning status", + in: From(ProvisioningStatusMessage{Status: -100}), + want: "da00012000813863", + }, + { + name: "provisioning BLE mac address", + in: From(ProvisioningBLEMacAddressMessage{ + BLEMacAddress: [6]uint8{0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF}}), + want: "DA000120138146AFAFAFAFAFAF", + }, + { + name: "provisioning WiFi FW Version", + in: From(ProvisioningWiFiFWVersionMessage{ + WiFiFWVersion: "1.6.0"}), + want: "DA000120148165312E362E30", + }, + { + name: "provisioning sketch Version", + in: From(ProvisioningSketchVersionMessage{ + ProvisioningSketchVersion: "1.6.0"}), + want: "DA000120158165312E362E30", + }, + { + name: "provisioning network configurator Version", + in: From(ProvisioningNetworkConfigLibVersionMessage{ + NetworkConfigLibVersion: "1.6.0"}), + want: "DA000120168165312E362E30", + }, + { + name: "provisioning unique id", + in: From(ProvisioningUniqueIdMessage{ + UniqueId: [32]uint8{0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA}}), + want: "DA00012010815820CACACACACACACACACACACACACACACACACACACACACACACACACACACACACACACACA", + }, + { + name: "provisioning signature", + in: From(ProvisioningSignatureMessage{ + Signature: [268]uint8{0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, + 0xCA, 0xCA, 0xCA, 0xCA}}), + want: "DA000120118159010CCACACACACACACACACACACACACACACACACACA" + + "CACACACACACACACACACACACACACACACACACACACACACACACACACACACACACA" + + "CACACACACACACACACACACACACACACACACACACACACACACACACACACACACACA" + + "CACACACACACACACACACACACACACACACACACACACACACACACACACACACACACA" + + "CACACACACACACACACACACACACACACACACACACACACACACACACACACACACACA" + + "CACACACACACACACACACACACACACACACACACACACACACACACACACACACACACA" + + "CACACACACACACACACACACACACACACACACACACACACACACACACACACACACACA" + + "CACACACACACACACACACACACACACACACACACACACACACACACACACACACACACA" + + "cacacacacacacacacacacacacacacacacacacacacacacacacacacacacaca" + + "cacacacacacacacacaca", + }, + { + name: "provisioning public key", + in: From(ProvisioningPublicKeyMessage{ + ProvisioningPublicKey: "-----BEGIN PUBLIC KEY-----\n" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7JxCtXl5SvIrHmiasqyN4pyoXRlm44d5WXNpqmvJ\n" + + "k0tH8UpmIeHG7YPAkKLaqid95v/wLVoWeX5EbjxmlCkFtw==\n-----END PUBLIC KEY-----\n", + }), + want: "DA000120178178B22D2D2D2D2D424547494E205055424C4943204B45592D2D2D2D2" + + "D0A4D466B77457759484B6F5A497A6A3043415159494B6F5A497A6A304441516344" + + "51674145374A784374586C3553764972486D69617371794E3470796F58526C6D343" + + "4643557584E70716D764A0A6B3074483855706D49654847375950416B4B4C617169" + + "643935762F774C566F5765583545626A786D6C436B4674773D3D0A2D2D2D2D2D454" + + "E44205055424C4943204B45592D2D2D2D2D0A", + }, + { + name: "provisioning timestamp", + in: From(ProvisioningTimestampMessage{Timestamp: fixedTime}), + want: "DA00012002811A65E072B5", + }, + { + name: "provisioning commands", + in: From(ProvisioningCommandsMessage{Command: 100}), + want: "DA00012003811864", + }, + { + name: "provisioning wifi config", + in: From(ProvisioningWifiConfigMessage{SSID: "SSID1", PWD: "PASSWORDSSID1"}), + want: "DA00012004826553534944316D50415353574F52445353494431", + }, + { + name: "provisioning lora config", + in: From(ProvisioningLoRaConfigMessage{ + AppEui: "APPEUI1", + AppKey: "APPKEY", + Band: 5, + ChannelMask: "01110", + DeviceClass: "A", + }), + want: "DA00012005856741505045554931664150504B4559056530313131306141", + }, + { + name: "provisioning gsm config", + in: From(ProvisioningGSMConfigMessage{ + PIN: "12345678", + Apn: "apn.arduino.cc", + Login: "TESTUSER", + Pass: "TESTPASSWORD", + }), + want: "DA00012006846831323334353637386E61706E2E61726475696E6F2E63636854455354555345526C5445535450415353574F5244", + }, + { + name: "provisoning gsm config without pin", + in: From(ProvisioningGSMConfigMessage{ + Apn: "apn.arduino.cc", + Login: "TESTUSER", + Pass: "TESTPASSWORD", + }), + want: "DA0001200684606E61706E2E61726475696E6F2E63636854455354555345526C5445535450415353574F5244", + }, + { + name: "provisioning nb config", + in: From(ProvisioningNBConfigMessage{ + PIN: "12345678", + Apn: "apn.arduino.cc", + Login: "TESTUSER", + Pass: "TESTPASSWORD", + }), + want: "DA00012007846831323334353637386E61706E2E61726475696E6F2E63636854455354555345526C5445535450415353574F5244", + }, + { + name: "provisioning nb config without pin, login and pass", + in: From(ProvisioningNBConfigMessage{ + PIN: "", + Apn: "apn.arduino.cc", + Login: "", + Pass: "", + }), + want: "DA0001200784606E61706E2E61726475696E6F2E63636060", + }, + { + name: "provisioning catm1 config", + in: From(ProvisioningCATM1ConfigMessage{ + PIN: "12345678", + Band: [4]uint32{1, 2, 524288, 134217728}, + Apn: "apn.arduino.cc", + Login: "TESTUSER", + Pass: "TESTPASSWORD", + }), + want: "DA00012008856831323334353637388401021A000800001A080000006E61706E2E61726475696E6F2E63636854455354555345526C5445535450415353574F5244", + }, + { + name: "provisioning ethernet config ipv4", + in: From(ProvisioningEthernetConfigMessage{ + Static_ip: []byte{192, 168, 0, 2}, + Dns: []byte{8, 8, 8, 8}, + Gateway: []byte{192, 168, 1, 1}, + Netmask: []byte{255, 255, 255, 0}, + Timeout: 15, + ResponseTimeout: 200, + }), + want: "DA000120098644C0A80002440808080844C0A8010144FFFFFF000F18C8", + }, + { + name: "provisioning ethernet config ipv6", + in: From(ProvisioningEthernetConfigMessage{ + Static_ip: []byte{0x1a, 0x4f, 0xa7, 0xa9, 0x92, 0x8f, 0x7b, 0x1c, 0xec, 0x3b, 0x1e, 0xcd, 0x88, 0x58, 0x0d, 0x1e}, + Dns: []byte{0x21, 0xf6, 0x3b, 0x22, 0x99, 0x6f, 0x5b, 0x72, 0x25, 0xd9, 0xe0, 0x24, 0xf0, 0x36, 0xb5, 0xd2}, + Gateway: []byte{0x2e, 0xc2, 0x27, 0xf1, 0xf1, 0x9a, 0x0c, 0x11, 0x47, 0x1b, 0x84, 0xaf, 0x96, 0x10, 0xb0, 0x17}, + Netmask: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + Timeout: 15, + ResponseTimeout: 200, + }), + want: "DA0001200986501A4FA7A9928F7B1CEC3B1ECD88580D1E5021F63B22996F5B7225D9E024F036B5D2502EC227F1F19A0C11471B84AF9610B01750FFFFFFFFFFFFFFFF00000000000000000F18C8", + }, + { + name: "provisioning cellular config", + in: From(ProvisioningCellularConfigMessage{ + PIN: "12345678", + Apn: "apn.arduino.cc", + Login: "TESTUSER", + Pass: "TESTPASSWORD", + }), + want: "DA00012012846831323334353637386E61706E2E61726475696E6F2E63636854455354555345526C5445535450415353574F5244", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got, err := tt.in.Encode() + assert.NoError(t, err) + tt.want = strings.ToLower(tt.want) + hexGot := hex.EncodeToString(got) + assert.Equal(t, tt.want, hexGot) + + cmd, err := Decode(got) + assert.NoError(t, err) + assert.Equal(t, tt.in, cmd) + }) + } +} + +func TestDecodeWiFiList(t *testing.T) { + list := From(WiFiNetworks{{SSID: "SSID1", RSSI: -76}, {SSID: "SSID2", RSSI: -56}}) + encoded_list, _ := hex.DecodeString("DA0001200184655353494431384B6553534944323837") + decoded, err := Decode(encoded_list) + assert.NoError(t, err) + assert.Equal(t, list, decoded) +} diff --git a/internal/board-protocols/configuration-protocol/cborcoders/model.go b/internal/board-protocols/configuration-protocol/cborcoders/model.go new file mode 100644 index 00000000..ccb3f066 --- /dev/null +++ b/internal/board-protocols/configuration-protocol/cborcoders/model.go @@ -0,0 +1,194 @@ +package cborcoders + +import ( + "fmt" +) + +// Provisioning commands +type ProvisioningStatusMessage struct { + _ struct{} `cbor:",toarray"` + Status int16 +} + +func (t ProvisioningStatusMessage) String() string { + return fmt.Sprintf("ProvisioningStatusMessage{Status: %d}", t.Status) +} + +type WiFiNetwork struct { + _ struct{} `cbor:",toarray"` + SSID string + RSSI int +} + +func (w WiFiNetwork) String() string { + return fmt.Sprintf("WiFiNetwork{SSID: %s, RSSI: %d}", w.SSID, w.RSSI) +} + +type WiFiNetworks []WiFiNetwork + +type ProvisioningUniqueIdMessage struct { + _ struct{} `cbor:",toarray"` + UniqueId [32]uint8 +} + +func (t ProvisioningUniqueIdMessage) String() string { + return fmt.Sprintf("ProvisioningUniqueIdMessage{UniqueId: %v}", t.UniqueId) +} + +type ProvisioningSignatureMessage struct { + _ struct{} `cbor:",toarray"` + Signature [268]uint8 +} + +func (t ProvisioningSignatureMessage) String() string { + return fmt.Sprintf("ProvisioningSignatureMessage{Signature: %v}", t.Signature) +} + +type ProvisioningPublicKeyMessage struct { + _ struct{} `cbor:",toarray"` + ProvisioningPublicKey string +} + +func (t ProvisioningPublicKeyMessage) String() string { + return fmt.Sprintf("ProvisioningPublicKeyMessage{ProvisioningPublicKey: %s}", t.ProvisioningPublicKey) +} + +type ProvisioningBLEMacAddressMessage struct { + _ struct{} `cbor:",toarray"` + BLEMacAddress [6]uint8 +} + +func (t ProvisioningBLEMacAddressMessage) String() string { + return fmt.Sprintf("ProvisioningSignatureMessage{Signature: %v}", t.BLEMacAddress) +} + +type ProvisioningWiFiFWVersionMessage struct { + _ struct{} `cbor:",toarray"` + WiFiFWVersion string +} + +func (t ProvisioningWiFiFWVersionMessage) String() string { + return fmt.Sprintf("ProvisioningWiFiFWVersionMessage{WiFiFWVersion: %s}", t.WiFiFWVersion) +} + +type ProvisioningSketchVersionMessage struct { + _ struct{} `cbor:",toarray"` + ProvisioningSketchVersion string +} + +func (t ProvisioningSketchVersionMessage) String() string { + return fmt.Sprintf("ProvisioningSketchVersionMessage{ProvisioningSketchVersion: %s}", t.ProvisioningSketchVersion) +} + +type ProvisioningNetworkConfigLibVersionMessage struct { + _ struct{} `cbor:",toarray"` + NetworkConfigLibVersion string +} + +func (t ProvisioningNetworkConfigLibVersionMessage) String() string { + return fmt.Sprintf("ProvisioningNetworkConfigLibVersionMessage{NetworkConfigLibVersion: %s}", t.NetworkConfigLibVersion) +} + +type ProvisioningTimestampMessage struct { + _ struct{} `cbor:",toarray"` + Timestamp uint64 +} + +func (t ProvisioningTimestampMessage) String() string { + return fmt.Sprintf("ProvisioningTimestampMessage{Timestamp: %d}", t.Timestamp) +} + +type ProvisioningCommandsMessage struct { + _ struct{} `cbor:",toarray"` + Command uint8 +} + +func (t ProvisioningCommandsMessage) String() string { + return fmt.Sprintf("ProvisioningCommandsMessage{Command: %d}", t.Command) +} + +type ProvisioningWifiConfigMessage struct { + _ struct{} `cbor:",toarray"` + SSID string + PWD string +} + +func (t ProvisioningWifiConfigMessage) String() string { + return fmt.Sprintf("ProvisioningWifiConfigMessage{SSID: %s, PWD: %s}", t.SSID, t.PWD) +} + +type ProvisioningLoRaConfigMessage struct { + _ struct{} `cbor:",toarray"` + AppEui string + AppKey string + Band uint8 + ChannelMask string + DeviceClass string +} + +func (t ProvisioningLoRaConfigMessage) String() string { + return fmt.Sprintf("ProvisioningLoRaConfigMessage{appEui: %s, appKey: %s, band: %d, channelMask: %s, deviceClass: %s}", t.AppEui, t.AppKey, t.Band, t.ChannelMask, t.DeviceClass) +} + +type ProvisioningCATM1ConfigMessage struct { + _ struct{} `cbor:",toarray"` + PIN string + Band [4]uint32 + Apn string + Login string + Pass string +} + +func (t ProvisioningCATM1ConfigMessage) String() string { + return fmt.Sprintf("ProvisioningCATM1ConfigMessage{PIN: %s, Band: %v, Apn: %s, Login: %s, Pass: %s}", t.PIN, t.Band, t.Apn, t.Login, t.Pass) +} + +type ProvisioningEthernetConfigMessage struct { + _ struct{} `cbor:",toarray"` + Static_ip []uint8 `cbor:",toarray"` + Dns []uint8 `cbor:",toarray"` + Gateway []uint8 `cbor:",toarray"` + Netmask []uint8 `cbor:",toarray"` + Timeout uint + ResponseTimeout uint +} + +func (t ProvisioningEthernetConfigMessage) String() string { + return fmt.Sprintf("ProvisioningEthernetConfigMessage{Static_ip: %v, Dns: %v, Gateway: %v, Netmask: %v, Timeout: %d, ResponseTimeout: %d}", t.Static_ip, t.Dns, t.Gateway, t.Netmask, t.Timeout, t.ResponseTimeout) +} + +type ProvisioningCellularConfigMessage struct { + _ struct{} `cbor:",toarray"` + PIN string + Apn string + Login string + Pass string +} + +func (t ProvisioningCellularConfigMessage) String() string { + return fmt.Sprintf("ProvisioningCellularConfigMessage{PIN: %s, Apn: %s, Login: %s, Pass: %s}", t.PIN, t.Apn, t.Login, t.Pass) +} + +type ProvisioningGSMConfigMessage struct { + _ struct{} `cbor:",toarray"` + PIN string + Apn string + Login string + Pass string +} + +func (t ProvisioningGSMConfigMessage) String() string { + return fmt.Sprintf("ProvisioningGSMConfigMessage{PIN: %s, Apn: %s, Login: %s, Pass: %s}", t.PIN, t.Apn, t.Login, t.Pass) +} + +type ProvisioningNBConfigMessage struct { + _ struct{} `cbor:",toarray"` + PIN string + Apn string + Login string + Pass string +} + +func (t ProvisioningNBConfigMessage) String() string { + return fmt.Sprintf("ProvisioningNBConfigMessage{PIN: %s, Apn: %s, Login: %s, Pass: %s}", t.PIN, t.Apn, t.Login, t.Pass) +} diff --git a/internal/board-protocols/configuration-protocol/configuration_protocol.go b/internal/board-protocols/configuration-protocol/configuration_protocol.go new file mode 100644 index 00000000..cfff4382 --- /dev/null +++ b/internal/board-protocols/configuration-protocol/configuration_protocol.go @@ -0,0 +1,208 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configurationprotocol + +import ( + "fmt" + + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/configuration-protocol/cborcoders" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/frame" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport" + "github.com/sirupsen/logrus" +) + +var StatusBoard = map[int16]string{ + 1: "Connecting", + 2: "Connected", + 4: "Resetted", + 100: "Scanning for WiFi networks", + -1: "Failed to connect", + -3: "Disconnected", + -4: "Parameters not provided", + -5: "Invalid parameters", + -6: "Cannot execute anew request while another is pending", + -7: "Invalid request", + -8: "Internet not available", + -101: "HW Error connectivity module", + -102: "HW Connectivity Module stopped", + -150: "Error initializing secure element", + -151: "Error configuring secure element", + -152: "Error locking secure element", + -160: "Error generating UHWID", + -200: "Error storage begin module", + -201: "Fail to partition the storage", + -255: "Generic error", +} + +var Commands = map[string]uint8{ + "Connect": 1, + "GetID": 2, + "GetBLEMac": 3, + "Reset": 4, + "ScanWiFi": 100, + "GetWiFiFWVersion": 101, + "GetSketchVersion": 200, + "GetNetConfigLibVersion": 201, +} + +const ( + serialInitByte = 0x01 + serialEndByte = 0x02 + nackByte = 0x03 +) + +type NetworkConfigurationProtocol struct { + transport *transport.TransportInterface + msgList []cborcoders.Cmd + lastPacket frame.Frame +} + +func NewNetworkConfigurationProtocol(transport *transport.TransportInterface) *NetworkConfigurationProtocol { + return &NetworkConfigurationProtocol{ + transport: transport, + msgList: make([]cborcoders.Cmd, 0), + } +} + +func (ncp *NetworkConfigurationProtocol) Connect(address string) error { + err := (*ncp.transport).Connect(transport.TransportInterfaceParams{ + Port: address, + BoundRate: 9600, + }) + + if err != nil { + err = fmt.Errorf("%s: %w", "connecting to serial port", err) + return err + } + + if (*ncp.transport).Type() == transport.Serial { + p := frame.CreateFrame([]byte{serialInitByte}, frame.TransmissionControl) + + err = (*ncp.transport).Send(p.ToBytes()) + } + return err + +} + +func (ncp *NetworkConfigurationProtocol) Close() error { + if ncp.transport == nil || *ncp.transport == nil { + return fmt.Errorf("NetworkConfigurationProtocol: transport interface is not initialized") + } + + if (*ncp.transport).Type() == transport.Serial { + p := frame.CreateFrame([]byte{serialEndByte}, frame.TransmissionControl) + + err := (*ncp.transport).Send(p.ToBytes()) + if err != nil { + return fmt.Errorf("error sending end of transmission: %w", err) + } + } + + err := (*ncp.transport).Close() + if err != nil { + return fmt.Errorf("error closing transport: %w", err) + } + + ncp.msgList = make([]cborcoders.Cmd, 0) + ncp.lastPacket = frame.Frame{} + + return nil +} + +func (ncp *NetworkConfigurationProtocol) SendData(msg cborcoders.Cmd) error { + databuf, err := msg.Encode() + if err != nil { + return err + } + + if !(*ncp.transport).Connected() { + return fmt.Errorf("ProvisioningProtocol: transport interface is not connected") + } + + packet := frame.CreateFrame(databuf, frame.Data) + + ncp.lastPacket = packet + + return (*ncp.transport).Send(packet.ToBytes()) +} + +func (ncp *NetworkConfigurationProtocol) ReceiveData(timeoutSeconds int) (*cborcoders.Cmd, error) { + if (ncp.msgList != nil) && (len(ncp.msgList) > 0) { + + return ncp.popMsg(), nil + } + + if ncp.transport == nil || *ncp.transport == nil { + return nil, fmt.Errorf("NetworkConfigurationProtocol: transport interface is not initialized") + } + + if !(*ncp.transport).Connected() { + return nil, fmt.Errorf("NetworkConfigurationProtocol: transport interface is not connected") + } + + frames, err := (*ncp.transport).Receive(timeoutSeconds) + if err != nil { + return nil, err + } + + for _, f := range frames { + if !f.Validate() { + ncp.SendNack() + return nil, nil + } + + msgType := f.GetType() + if msgType == frame.TransmissionControl { + if f.GetPayload()[0] == nackByte { + // Resend packet + logrus.Debug("NetworkConfigurationProtocol: Received NACK, resending last packet") + (*ncp.transport).Send(ncp.lastPacket.ToBytes()) + break + } else if f.GetPayload()[0] == serialEndByte { + logrus.Debug("NetworkConfigurationProtocol: Received end of transmission signal") + (*ncp.transport).Close() + break + } + } + payload := f.GetPayload() + + res, err := cborcoders.Decode(payload) + if err != nil { + logrus.Warnf("NetworkConfigurationProtocol: error decoding payload: %s", err.Error()) + continue + } + + ncp.msgList = append(ncp.msgList, res) + } + + return ncp.popMsg(), nil +} + +func (ncp *NetworkConfigurationProtocol) SendNack() error { + packet := frame.CreateFrame([]byte{nackByte}, frame.TransmissionControl) + return (*ncp.transport).Send(packet.ToBytes()) +} + +func (ncp *NetworkConfigurationProtocol) popMsg() *cborcoders.Cmd { + if len(ncp.msgList) == 0 { + return nil + } + msg := ncp.msgList[0] + ncp.msgList = ncp.msgList[1:] + return &msg +} diff --git a/internal/board-protocols/configuration-protocol/configuration_protocol_test.go b/internal/board-protocols/configuration-protocol/configuration_protocol_test.go new file mode 100644 index 00000000..69641e32 --- /dev/null +++ b/internal/board-protocols/configuration-protocol/configuration_protocol_test.go @@ -0,0 +1,223 @@ +package configurationprotocol + +import ( + "errors" + "testing" + + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/configuration-protocol/cborcoders" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/frame" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func newMockTransportSerial() *mocks.TransportInterface { + m := &mocks.TransportInterface{} + m.On("Type").Return(transport.Serial) + + return m +} + +func TestNewNetworkConfigurationProtocolSerial(t *testing.T) { + mockTr := newMockTransportSerial() + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + assert.NotNil(t, ncp) + assert.Equal(t, &tr, ncp.transport) + assert.Empty(t, ncp.msgList) +} + +func TestConnectSerial_Success(t *testing.T) { + mockTr := newMockTransportSerial() + mockTr.On("Connect", mock.Anything).Return(nil) + mockTr.On("Send", mock.Anything).Return(nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + connectMsg := frame.CreateFrame([]byte{serialInitByte}, frame.TransmissionControl) + connectParams := transport.TransportInterfaceParams{ + Port: "COM1", + BoundRate: 9600, + } + + err := ncp.Connect("COM1") + assert.NoError(t, err) + mockTr.AssertCalled(t, "Connect", connectParams) + mockTr.AssertCalled(t, "Send", connectMsg.ToBytes()) +} + +func TestConnectSerial_Error(t *testing.T) { + mockTr := newMockTransportSerial() + mockTr.On("Connect", mock.Anything).Return(errors.New("port busy")) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + err := ncp.Connect("COM1") + assert.Error(t, err) + assert.Contains(t, err.Error(), "connecting to serial port") +} + +func TestCloseSerial_Success(t *testing.T) { + mockTr := newMockTransportSerial() + mockTr.On("Send", mock.Anything).Return(nil) + mockTr.On("Close").Return(nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + closeMsg := []byte{0x55, 0xaa, 0x03, 0x00, 0x03, 0x02, 0xD3, 0x6A, 0xaa, 0x55} + + err := ncp.Close() + assert.NoError(t, err) + mockTr.AssertCalled(t, "Send", closeMsg) + mockTr.AssertCalled(t, "Close") + assert.Empty(t, ncp.msgList) +} + +func TestSendData_Success(t *testing.T) { + mockTr := newMockTransportSerial() + mockTr.On("Connected").Return(true) + mockTr.On("Send", mock.Anything).Return(nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + connectMessage := cborcoders.From(cborcoders.ProvisioningCommandsMessage{Command: Commands["Connect"]}) + + want := []byte{0x55, 0xaa, 0x02, 0x00, 0x09, 0xda, 0x00, 0x01, 0x20, 0x03, 0x81, 0x01, 0x7e, 0x1b, 0xaa, 0x55} + err := ncp.SendData(connectMessage) + assert.NoError(t, err) + mockTr.AssertCalled(t, "Send", want) +} + +func TestReceiveData_TransportNotConnected(t *testing.T) { + mockTr := newMockTransportSerial() + mockTr.On("Connected").Return(false) + mockTr.On("Receive", mock.Anything).Return(nil, nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + res, err := ncp.ReceiveData(1) + assert.Nil(t, res) + assert.Error(t, err) + assert.Contains(t, err.Error(), "transport interface is not connected") +} + +func TestReceiveData_ReceiveTimeout(t *testing.T) { + mockTr := newMockTransportSerial() + mockTr.On("Connected").Return(true) + mockTr.On("Receive", mock.Anything).Return(nil, errors.New("recv error")) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + res, err := ncp.ReceiveData(1) + assert.Nil(t, res) + assert.Error(t, err) + assert.Contains(t, err.Error(), "recv error") +} + +func TestReceiveData_FrameInvalid(t *testing.T) { + mockTr := newMockTransportSerial() + invalidFrame := frame.Frame{} + invalidFrame.SetHeader([]byte{0x55, 0xaa, 0x02, 0x00, 0x03}) + invalidFrame.SetPayload([]byte{0x04}) + invalidFrame.SetCrc([]byte{0x00, 0x00}) + invalidFrame.SetFooter([]byte{0xaa, 0x55}) + want := []byte{0x55, 0xaa, 0x03, 0x00, 0x03, 0x03, 0xC2, 0xE3, 0xaa, 0x55} + mockTr.On("Connected").Return(true) + mockTr.On("Receive", mock.Anything).Return([]frame.Frame{invalidFrame}, nil) + mockTr.On("Send", mock.Anything).Return(nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + res, err := ncp.ReceiveData(1) + assert.Nil(t, res) + assert.NoError(t, err) + mockTr.AssertCalled(t, "Send", want) +} + +func TestReceiveData_NackReceived(t *testing.T) { + mockTr := newMockTransportSerial() + nackFrame := frame.CreateFrame([]byte{nackByte}, frame.TransmissionControl) + want := []byte{0x55, 0xaa, 0x02, 0x00, 0x09, 0xda, 0x00, 0x01, 0x20, 0x03, 0x81, 0x01, 0x7e, 0x1b, 0xaa, 0x55} + mockTr.On("Connected").Return(true) + mockTr.On("Receive", mock.Anything).Return([]frame.Frame{nackFrame}, nil) + mockTr.On("Send", mock.Anything).Return(nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + err := ncp.SendData(cborcoders.From(cborcoders.ProvisioningCommandsMessage{Command: Commands["Connect"]})) + assert.NoError(t, err) + mockTr.AssertCalled(t, "Send", want) + res, err := ncp.ReceiveData(1) + assert.Nil(t, res) + assert.NoError(t, err) + mockTr.AssertCalled(t, "Send", want) +} + +func TestReceiveData_SerialEndReceived(t *testing.T) { + mockTr := newMockTransportSerial() + endFrame := frame.CreateFrame([]byte{serialEndByte}, frame.TransmissionControl) + mockTr.On("Connected").Return(true) + mockTr.On("Close").Return(nil) + mockTr.On("Receive", mock.Anything).Return([]frame.Frame{endFrame}, nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + res, err := ncp.ReceiveData(1) + assert.Nil(t, res) + assert.NoError(t, err) + mockTr.AssertCalled(t, "Close") +} + +func TestReceiveData_MsgAndSerialEndReceived(t *testing.T) { + mockTr := newMockTransportSerial() + frameData := frame.CreateFrame([]byte{0xda, 0x00, 0x01, 0x20, 0x00, 0x81, 0x01}, frame.Data) + endFrame := frame.CreateFrame([]byte{serialEndByte}, frame.TransmissionControl) + mockTr.On("Connected").Return(true) + mockTr.On("Close").Return(nil) + mockTr.On("Receive", mock.Anything).Return([]frame.Frame{frameData, endFrame}, nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + res, err := ncp.ReceiveData(1) + assert.NoError(t, err) + mockTr.AssertCalled(t, "Close") + assert.NotNil(t, res) + assert.Equal(t, res.Type(), cborcoders.ProvisioningStatusMessageType) + assert.Equal(t, res.ToProvisioningStatusMessage().Status, int16(1)) +} + +func TestReceiveData_MsgAndNackReceived(t *testing.T) { + mockTr := newMockTransportSerial() + frameData := frame.CreateFrame([]byte{0xda, 0x00, 0x01, 0x20, 0x00, 0x81, 0x01}, frame.Data) + nackFrame := frame.CreateFrame([]byte{nackByte}, frame.TransmissionControl) + want := []byte{0x55, 0xaa, 0x02, 0x00, 0x09, 0xda, 0x00, 0x01, 0x20, 0x03, 0x81, 0x01, 0x7e, 0x1b, 0xaa, 0x55} + mockTr.On("Connected").Return(true) + mockTr.On("Receive", mock.Anything).Return([]frame.Frame{frameData, nackFrame}, nil) + mockTr.On("Send", mock.Anything).Return(nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + err := ncp.SendData(cborcoders.From(cborcoders.ProvisioningCommandsMessage{Command: Commands["Connect"]})) + assert.NoError(t, err) + mockTr.AssertCalled(t, "Send", want) + res, err := ncp.ReceiveData(1) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, res.Type(), cborcoders.ProvisioningStatusMessageType) + assert.Equal(t, res.ToProvisioningStatusMessage().Status, int16(1)) + mockTr.AssertCalled(t, "Send", want) +} + +func TestReceiveData_ReceiveData(t *testing.T) { + mockTr := newMockTransportSerial() + frameData := frame.CreateFrame([]byte{0xda, 0x00, 0x01, 0x20, 0x00, 0x81, 0x01}, frame.Data) + mockTr.On("Connected").Return(true) + mockTr.On("Receive", mock.Anything).Return([]frame.Frame{frameData}, nil) + tr := transport.TransportInterface(mockTr) + ncp := NewNetworkConfigurationProtocol(&tr) + + res, err := ncp.ReceiveData(1) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, res.Type(), cborcoders.ProvisioningStatusMessageType) + assert.Equal(t, res.ToProvisioningStatusMessage().Status, int16(1)) +} diff --git a/internal/board-protocols/frame/frame.go b/internal/board-protocols/frame/frame.go new file mode 100644 index 00000000..b7b60de9 --- /dev/null +++ b/internal/board-protocols/frame/frame.go @@ -0,0 +1,209 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package frame + +import ( + "bytes" + "encoding/binary" + + "github.com/howeyc/crc16" +) + +/* + * The ArduinoBoardConfiguration Protocol frame structure + * 0x55 0xaa 0xaa 0x55 + * ____________________________________________________________________________________________________________________________________ + * | Byte[0] | Byte[1] | Byte[2] | Byte[3] | Byte[4] | Byte[5].......Byte[len -3] | Byte[len-2] | Byte[len-1] | Byte[len] | Byte[len+1] | + * |______________________HEADER_____________________|__________ PAYLOAD _________|___________ CRC ___________|________ FOOTER _________| + * | 0x55 | 0xaa | | | | | 0xaa | 0x55 | + * |____________________________________________________________________________________________________________________________________| + * = MessageType: 2 = DATA, 3 = TRANSMISSION_CONTROL + * = length of the payload + 2 bytes for the CRC + * = the data to be sent or received + * = CRC16 of the payload + */ + +var ( + // msgStart is the initial byte sequence of every packet. + msgStart = [2]byte{0x55, 0xAA} + // msgEnd is the final byte sequence of every packet. + msgEnd = [2]byte{0xAA, 0x55} +) + +const ( + // headerLen indicates the length of the header. + headerLen = 5 + // payloadField indicates the position of payload field. + payloadField = 5 + // payloadLenField indicates the position of payload length field. + payloadLenField = 3 + // payloadLenFieldLen indicatest the length of payload length field. + payloadLenFieldLen = 2 + // crcFieldLen indicates the length of the signature field. + crcFieldLen = 2 +) + +// MsgType indicates the type of the packet. +type MsgType byte + +const ( + None MsgType = iota + Cmd + Data + Response + TransmissionControl = Response // Alias for Response, used for clarity in ArduinoBoardConfiguration Protocol +) + +type Frame struct { + header []byte + payload []byte + crc []byte + footer []byte + payloadLen int + length int +} + +func (p *Frame) FillByte(b byte) bool { + + if len(p.header) < headerLen { + if p.fillHeader(b) { + p.extractPayloadLength() + p.length = headerLen + p.payloadLen + crcFieldLen + len(msgEnd) + } + } else if len(p.payload) < p.payloadLen { + p.payload = append(p.payload, b) + } else if len(p.crc) < crcFieldLen { + p.crc = append(p.crc, b) + } else if len(p.footer) < 2 { + p.footer = append(p.footer, b) + } + + return len(p.header) == headerLen && len(p.payload) == p.payloadLen && len(p.crc) == crcFieldLen && len(p.footer) == 2 +} + +func (p *Frame) fillHeader(data byte) bool { + p.header = append(p.header, data) + return len(p.header) == 5 +} + +func (p *Frame) extractPayloadLength() bool { + if len(p.header) < 5 { + return false + } + p.payloadLen = int(binary.BigEndian.Uint16(p.header[payloadLenField:])) - crcFieldLen + return true +} + +func (p *Frame) Validate() bool { + if len(p.header) != headerLen && !bytes.Equal(p.header[:2], msgStart[:]) { + return false + } + + if p.payloadLen == 0 { + return false + } + + if len(p.payload) != p.payloadLen { + return false + } + + if len(p.crc) != crcFieldLen { + return false + } + + if len(p.footer) != 2 && !bytes.Equal(p.footer[:], msgEnd[:]) { + return false + } + + ch := crc16.Checksum(p.payload, crc16.CCITTTable) + // crc is contained in the last bytes of the payload + cp := binary.BigEndian.Uint16(p.crc) + if ch != cp { + return false + } + + return true +} + +func (p *Frame) GetPayload() []byte { + if !p.Validate() { + return nil + } + return p.payload +} + +func (p *Frame) GetType() MsgType { + if !p.Validate() { + return None + } + return MsgType(p.header[2]) +} + +func (p *Frame) GetLength() int { + return p.length +} + +func (p *Frame) ToBytes() []byte { + return append(append(append(p.header, p.payload...), p.crc...), p.footer...) +} + +func (p *Frame) SetPayload(payload []byte) { + p.length += len(payload) + p.payload = payload + p.payloadLen = len(payload) +} + +func (p *Frame) SetHeader(header []byte) { + p.length += len(header) + p.header = header +} + +func (p *Frame) SetCrc(crc []byte) { + p.length += len(crc) + p.crc = crc +} + +func (p *Frame) SetFooter(footer []byte) { + p.length += len(footer) + p.footer = footer +} + +func CreateFrame(data []byte, mType MsgType) Frame { + // Create the packet + packet := Frame{} + packetHeader := append(msgStart[:], byte(mType)) + + // Append the packet length + bLen := make([]byte, payloadLenFieldLen) + binary.BigEndian.PutUint16(bLen, (uint16(len(data) + crcFieldLen))) + packetHeader = append(packetHeader, bLen...) + + packet.SetHeader(packetHeader) + // Append the message payload + packet.SetPayload(data) + + // Calculate and append the message signature + ch := crc16.Checksum(data, crc16.CCITTTable) + checksum := make([]byte, crcFieldLen) + binary.BigEndian.PutUint16(checksum, ch) + packet.SetCrc(checksum) + + // Append final byte sequence + packet.SetFooter(msgEnd[:]) + return packet +} diff --git a/internal/board-protocols/provisioning-protocol/provisioning_protocol.go b/internal/board-protocols/provisioning-protocol/provisioning_protocol.go new file mode 100644 index 00000000..1d038e64 --- /dev/null +++ b/internal/board-protocols/provisioning-protocol/provisioning_protocol.go @@ -0,0 +1,143 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package provisioningprotocol + +import ( + "context" + "errors" + "fmt" + + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/frame" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport" +) + +// Command indicates the command that should be +// executed on the board to be provisioned. +type Command byte + +const ( + SketchInfo Command = iota + 1 + CSR + Locked + GetLocked + WriteCrypto + BeginStorage + SetDeviceID + SetYear + SetMonth + SetDay + SetHour + SetValidity + SetCertSerial + SetAuthKey + SetSignature + EndStorage + ReconstructCert +) + +const ( + timeoutSeconds = 2 +) + +type ProvisioningProtocol struct { + transport *transport.TransportInterface + packetList []frame.Frame +} + +func NewProvisioningProtocol(transport *transport.TransportInterface) *ProvisioningProtocol { + return &ProvisioningProtocol{ + transport: transport, + } +} + +// Send allows to send a provisioning command to a connected arduino device. +func (p *ProvisioningProtocol) Send(ctx context.Context, cmd Command, payload []byte) error { + if p.transport == nil || *p.transport == nil { + return fmt.Errorf("ProvisioningProtocol: transport interface is not initialized") + } + + if !(*p.transport).Connected() { + return fmt.Errorf("ProvisioningProtocol: transport interface is not connected") + } + + if err := ctx.Err(); err != nil { + return fmt.Errorf("context error: %w", err) + } + payload = append([]byte{byte(cmd)}, payload...) + frame := frame.CreateFrame(payload, frame.Cmd) + + err := (*p.transport).Send(frame.ToBytes()) + return err + +} + +// SendReceive allows to send a provisioning command to a connected arduino device. +// Then, it waits for a response from the device and, if any, returns it. +// If no response is received after 2 seconds, an error is returned. +func (p *ProvisioningProtocol) SendReceive(ctx context.Context, cmd Command, payload []byte) ([]byte, error) { + if err := p.Send(ctx, cmd, payload); err != nil { + return nil, err + } + return p.receive(ctx) +} + +// receive allows to wait for a response from an arduino device under provisioning. +// Its timeout is set to 2 seconds. It returns an error if the response is not valid +// or if the timeout expires. +func (p *ProvisioningProtocol) receive(ctx context.Context) ([]byte, error) { + if p.packetList != nil || len(p.packetList) > 0 { + return p.popMsg().GetPayload(), nil + } + + if p.transport == nil || *p.transport == nil { + return nil, errors.New("ProvisioningProtocol: transport interface is not initialized") + } + + if err := ctx.Err(); err != nil { + return nil, err + } + + packets, err := (*p.transport).Receive(timeoutSeconds) + + if err != nil { + return nil, fmt.Errorf("error receiving packets: %w", err) + } + + if len(packets) == 0 { + return nil, nil + } + + for _, packet := range packets { + if !packet.Validate() { + return nil, errors.New("received invalid packet") + } + } + + p.packetList = append(p.packetList, packets...) + + return p.popMsg().GetPayload(), nil +} + +func (p *ProvisioningProtocol) popMsg() *frame.Frame { + if len(p.packetList) == 0 { + return nil + } + msg := p.packetList[0] + p.packetList = p.packetList[1:] + return &msg +} diff --git a/internal/board-protocols/provisioning-protocol/provisioning_protocol_test.go b/internal/board-protocols/provisioning-protocol/provisioning_protocol_test.go new file mode 100644 index 00000000..31d5c496 --- /dev/null +++ b/internal/board-protocols/provisioning-protocol/provisioning_protocol_test.go @@ -0,0 +1,52 @@ +package provisioningprotocol + +import ( + "context" + "testing" + + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/frame" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSend_Success(t *testing.T) { + mockTransportInterface := &mocks.TransportInterface{} + var tr transport.TransportInterface = mockTransportInterface + provProt := NewProvisioningProtocol(&tr) + mockTransportInterface.On("Connected").Return(true) + mockTransportInterface.On("Send", mock.AnythingOfType("[]uint8")).Return(nil) + + payload := []byte{1, 2} + cmd := SetDay + want := []byte{0x55, 0xaa, 1, 0, 5, 10, 1, 2, 143, 124, 0xaa, 0x55} + + err := provProt.Send(context.TODO(), cmd, payload) + assert.NoError(t, err) + mockTransportInterface.AssertCalled(t, "Send", want) +} + +func TestSendReceive_Success(t *testing.T) { + + mockTransportInterface := &mocks.TransportInterface{} + var tr transport.TransportInterface = mockTransportInterface + provProt := NewProvisioningProtocol(&tr) + + want := []byte{1, 2, 3} + rec := frame.CreateFrame(want, frame.Response) + receivedListFrame := []frame.Frame{ + rec, + } + + mockTransportInterface.On("Connected").Return(true) + mockTransportInterface.On("Send", mock.AnythingOfType("[]uint8")).Return(nil) + mockTransportInterface.On("Receive", mock.Anything).Return(receivedListFrame, nil) + + res, err := provProt.SendReceive(context.TODO(), BeginStorage, []byte{1, 2}) + assert.NoError(t, err) + + assert.NotNil(t, res, "Expected non-nil response") + assert.Equal(t, res, want, "Expected %v but received %v", want, res) + +} diff --git a/internal/board-protocols/transport/mocks/Transport.go b/internal/board-protocols/transport/mocks/Transport.go new file mode 100644 index 00000000..d3500e74 --- /dev/null +++ b/internal/board-protocols/transport/mocks/Transport.go @@ -0,0 +1,149 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package mocks + +import ( + frame "github.com/arduino/arduino-cloud-cli/internal/board-protocols/frame" + mock "github.com/stretchr/testify/mock" + + transport "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport" +) + +// TransportInterface is an autogenerated mock type for the TransportInterface type +type TransportInterface struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *TransportInterface) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Connect provides a mock function with given fields: params +func (_m *TransportInterface) Connect(params transport.TransportInterfaceParams) error { + ret := _m.Called(params) + + if len(ret) == 0 { + panic("no return value specified for Connect") + } + + var r0 error + if rf, ok := ret.Get(0).(func(transport.TransportInterfaceParams) error); ok { + r0 = rf(params) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Connected provides a mock function with given fields: +func (_m *TransportInterface) Connected() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Connected") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Receive provides a mock function with given fields: timeoutSeconds +func (_m *TransportInterface) Receive(timeoutSeconds int) ([]frame.Frame, error) { + ret := _m.Called(timeoutSeconds) + + if len(ret) == 0 { + panic("no return value specified for Receive") + } + + var r0 []frame.Frame + var r1 error + if rf, ok := ret.Get(0).(func(int) ([]frame.Frame, error)); ok { + return rf(timeoutSeconds) + } + if rf, ok := ret.Get(0).(func(int) []frame.Frame); ok { + r0 = rf(timeoutSeconds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]frame.Frame) + } + } + + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(timeoutSeconds) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Send provides a mock function with given fields: data +func (_m *TransportInterface) Send(data []byte) error { + ret := _m.Called(data) + + if len(ret) == 0 { + panic("no return value specified for Send") + } + + var r0 error + if rf, ok := ret.Get(0).(func([]byte) error); ok { + r0 = rf(data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Type provides a mock function with given fields: +func (_m *TransportInterface) Type() transport.InterfaceType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Type") + } + + var r0 transport.InterfaceType + if rf, ok := ret.Get(0).(func() transport.InterfaceType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(transport.InterfaceType) + } + + return r0 +} + +// NewTransportInterface creates a new instance of TransportInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTransportInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *TransportInterface { + mock := &TransportInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/board-protocols/transport/transport_controller.go b/internal/board-protocols/transport/transport_controller.go new file mode 100644 index 00000000..e587a536 --- /dev/null +++ b/internal/board-protocols/transport/transport_controller.go @@ -0,0 +1,101 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package transport + +import ( + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/frame" +) + +type TransportController struct { + packetList []frame.Frame + receivingPacket frame.Frame + filledPacket bool + foundStart bool + foundFirstByteStart bool +} + +func NewTransportController() *TransportController { + return &TransportController{ + packetList: make([]frame.Frame, 0), + receivingPacket: frame.Frame{}, + filledPacket: false, + foundStart: false, + foundFirstByteStart: false, + } +} + +func (tc *TransportController) HandleReceivedData(data []byte) []frame.Frame { + // if in the previous iteration the last byte was the beginning of a new packet, + // check if the first byte of the current iteration is the second byte of the begin frame + if tc.foundFirstByteStart { + if len(data) > 0 && data[0] == 0xaa { + tc.foundStart = true + //force fill byte + tc.filledPacket = tc.receivingPacket.FillByte(0x55) + } + tc.foundFirstByteStart = false + } + + n := tc.searchStartPacket(data) + if n != -1 && !tc.foundStart { + tc.foundStart = true + data = data[n:] + } + + if tc.foundStart { + for i := 0; i < len(data); i++ { + if tc.filledPacket { + tc.foundStart = false + n = tc.searchStartPacket(data[i:]) + if n != -1 { + tc.foundStart = true + tc.filledPacket = false + tc.packetList = append(tc.packetList, tc.receivingPacket) + tc.receivingPacket = frame.Frame{} + + i = i + n + } else { + break + } + } + tc.filledPacket = tc.receivingPacket.FillByte(data[i]) + } + } else { + // Discard data + //Check if the last byte is the beginning of a new packet in case the begin is split in two packets + if len(data) > 0 && data[len(data)-1] == 0x55 { + tc.foundFirstByteStart = true + } + } + + if !tc.filledPacket { + return nil + } + + tc.packetList = append(tc.packetList, tc.receivingPacket) + return tc.packetList +} + +func (tc *TransportController) searchStartPacket(data []byte) int { + for i := 0; i < len(data)-1; i++ { + if data[i] == 0x55 && data[i+1] == 0xaa { + return i + } + } + return -1 +} diff --git a/internal/board-protocols/transport/transport_controller_test.go b/internal/board-protocols/transport/transport_controller_test.go new file mode 100644 index 00000000..9a95807c --- /dev/null +++ b/internal/board-protocols/transport/transport_controller_test.go @@ -0,0 +1,152 @@ +package transport + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHandleReceivedData_FullPacket(t *testing.T) { + tc := NewTransportController() + + data := []byte{0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55} + want := []byte{0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55} + got := tc.HandleReceivedData(data) + + assert.NotEmpty(t, got, "expected non-nil packet list") + assert.Equal(t, len(got), 1, "expected 1 packet, got %d", len(got)) + assert.Equal(t, got[0].ToBytes(), want, "expected packet bytes %v, got %v", want, got[0].ToBytes()) +} + +func TestHandleReceivedData_FullPacketWithJunk(t *testing.T) { + tc := NewTransportController() + + data := []byte{0x65, 0x45, 0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55, 0x65, 0x24, 0x67} + want := []byte{0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55} + got := tc.HandleReceivedData(data) + assert.NotEmpty(t, got, "expected non-nil packet list") + assert.Equal(t, len(got), 1, "expected 1 packet, got %d", len(got)) + assert.Equal(t, got[0].ToBytes(), want, "expected packet bytes %v, got %v", want, got[0].ToBytes()) +} + +func TestHandleReceivedData_SplitStartSequence(t *testing.T) { + tc := NewTransportController() + want := []byte{0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55} + // First call: only 0x55 + got := tc.HandleReceivedData([]byte{0x55}) + + assert.Nil(t, got, "expected nil packet list") + + // Second call: 0xaa and 0x02, should complete start and fill + got = tc.HandleReceivedData([]byte{0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55}) + + assert.NotEmpty(t, got, "expected non-nil packet list") + assert.Equal(t, len(got), 1, "expected 1 packet, got %d", len(got)) + assert.Equal(t, got[0].ToBytes(), want, "expected packet bytes %v, got %v", want, got[0].ToBytes()) +} + +func TestHandleReceivedData_SplitStartSequenceWithJunk(t *testing.T) { + tc := NewTransportController() + want := []byte{0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55} + // First call: only 0x55 + got := tc.HandleReceivedData([]byte{0x00, 0x00, 0x12, 0x70, 0x55}) + + assert.Nil(t, got, "expected nil packet list") + // Second call: 0xaa and 0x02, should complete start and fill + got = tc.HandleReceivedData([]byte{0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55}) + + assert.NotEmpty(t, got, "expected non-nil packet list") + assert.Equal(t, len(got), 1, "expected 1 packet, got %d", len(got)) + assert.Equal(t, got[0].ToBytes(), want, "expected packet bytes %v, got %v", want, got[0].ToBytes()) +} + +func TestHandleReceivedData_MultiplePackets(t *testing.T) { + tc := NewTransportController() + // Two packets in one buffer + data := []byte{0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55, 0x55, 0xaa, 0x02, 0x00, 0x03, 0x05, 0xA7, 0xD5, 0xaa, 0x55, 0x55, 0xaa, 0x02, 0x00, 0x03, 0x06, 0x95, 0x4E, 0xaa, 0x55} + want := [][]byte{ + {0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55}, + {0x55, 0xaa, 0x02, 0x00, 0x03, 0x05, 0xA7, 0xD5, 0xaa, 0x55}, + {0x55, 0xaa, 0x02, 0x00, 0x03, 0x06, 0x95, 0x4E, 0xaa, 0x55}, + } + got := tc.HandleReceivedData(data) + assert.NotEmpty(t, got, "expected non-nil packet list") + assert.Equal(t, len(got), 3, "expected 1 packet, got %d", len(got)) + assert.Equal(t, got[0].ToBytes(), want[0], "expected packet bytes %v, got %v", want[0], got[0].ToBytes()) + assert.Equal(t, got[1].ToBytes(), want[1], "expected packet bytes %v, got %v", want[1], got[1].ToBytes()) + assert.Equal(t, got[2].ToBytes(), want[2], "expected packet bytes %v, got %v", want[2], got[2].ToBytes()) +} + +func TestHandleReceivedData_MultiplePacketsWithJunk(t *testing.T) { + tc := NewTransportController() + // Two packets in one buffer + data := []byte{0x00, 0x01, 0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55, + 0x00, 0x34, 0x45, 0x55, 0xaa, 0x02, 0x00, 0x03, 0x05, 0xA7, 0xD5, 0xaa, 0x55, 0x55, 0xaa, 0x02, 0x00, 0x03, 0x06, 0x95, 0x4E, 0xaa, 0x55, 0x00, 0x03} + want := [][]byte{ + {0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55}, + {0x55, 0xaa, 0x02, 0x00, 0x03, 0x05, 0xA7, 0xD5, 0xaa, 0x55}, + {0x55, 0xaa, 0x02, 0x00, 0x03, 0x06, 0x95, 0x4E, 0xaa, 0x55}, + } + got := tc.HandleReceivedData(data) + assert.NotEmpty(t, got, "expected non-nil packet list") + assert.Equal(t, len(got), 3, "expected 1 packet, got %d", len(got)) + assert.Equal(t, got[0].ToBytes(), want[0], "expected packet bytes %v, got %v", want[0], got[0].ToBytes()) + assert.Equal(t, got[1].ToBytes(), want[1], "expected packet bytes %v, got %v", want[1], got[1].ToBytes()) + assert.Equal(t, got[2].ToBytes(), want[2], "expected packet bytes %v, got %v", want[2], got[2].ToBytes()) +} + +func TestHandleReceivedData_NoStartSequence(t *testing.T) { + tc := NewTransportController() + data := []byte{0x01, 0x02, 0x03} + got := tc.HandleReceivedData(data) + assert.Nil(t, got, "expected nil packet list for data without start sequence") + +} + +func TestHandleReceivedData_PartialPacket(t *testing.T) { + tc := NewTransportController() + want := []byte{0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55} + // Only part of the packet arrives + + got := tc.HandleReceivedData([]byte{0x55, 0xaa}) + assert.Nil(t, got, "expected nil packet list for partial packet") + // Second call: more data arrives, but still not complete + got = tc.HandleReceivedData([]byte{0x02, 0x00, 0x03, 0x04, 0xB6}) + assert.Nil(t, got, "expected nil packet list for partial packet") + + // Third call: more data arrives + got = tc.HandleReceivedData([]byte{0x5C, 0xaa}) + assert.Nil(t, got, "expected nil packet list for partial packet") + + // Fourth call: complete packet arrives + got = tc.HandleReceivedData([]byte{0x55}) + assert.NotEmpty(t, got, "expected non-nil packet list") + assert.Equal(t, len(got), 1, "expected 1 packet, got %d", len(got)) + assert.Equal(t, got[0].ToBytes(), want, "expected packet bytes %v, got %v", want, got[0].ToBytes()) +} + +func TestHandleReceivedData_PartialPacketMultiplePacket(t *testing.T) { + tc := NewTransportController() + want := [][]byte{ + {0x55, 0xaa, 0x02, 0x00, 0x03, 0x04, 0xB6, 0x5C, 0xaa, 0x55}, + {0x55, 0xaa, 0x02, 0x00, 0x03, 0x05, 0xA7, 0xD5, 0xaa, 0x55}, + } + // Only part of the packet arrives + + got := tc.HandleReceivedData([]byte{0x55, 0xaa}) + assert.Nil(t, got, "expected nil packet list for partial packet") + // Second call: more data arrives, but still not complete + got = tc.HandleReceivedData([]byte{0x02, 0x00, 0x03, 0x04, 0xB6}) + assert.Nil(t, got, "expected nil packet list for partial packet") + + // Third call: more data arrives + got = tc.HandleReceivedData([]byte{0x5C, 0xaa}) + assert.Nil(t, got, "expected nil packet list for partial packet") + + // Fourth call: complete packet arrives + got = tc.HandleReceivedData([]byte{0x55, 0x55, 0xaa, 0x02, 0x00, 0x03, 0x05, 0xA7, 0xD5, 0xaa, 0x55}) + assert.NotEmpty(t, got, "expected non-nil packet list") + assert.Equal(t, len(got), 2, "expected 2 packet, got %d", len(got)) + assert.Equal(t, got[0].ToBytes(), want[0], "expected packet bytes %v, got %v", want[0], got[0].ToBytes()) + assert.Equal(t, got[1].ToBytes(), want[1], "expected packet bytes %v, got %v", want[1], got[1].ToBytes()) +} diff --git a/internal/board-protocols/transport/transport_interface.go b/internal/board-protocols/transport/transport_interface.go new file mode 100644 index 00000000..06aeb38b --- /dev/null +++ b/internal/board-protocols/transport/transport_interface.go @@ -0,0 +1,44 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package transport + +import ( + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/frame" +) + +type TransportInterfaceParams struct { + Port string + BoundRate int +} + +// MsgType indicates the type of the packet. +type InterfaceType byte + +const ( + Serial InterfaceType = iota + BLE +) + +type TransportInterface interface { + Connect(params TransportInterfaceParams) error + Send(data []byte) error + Receive(timeoutSeconds int) ([]frame.Frame, error) + Connected() bool + Type() InterfaceType + Close() error +} diff --git a/internal/serial/protocol.go b/internal/serial/protocol.go deleted file mode 100644 index d4a95497..00000000 --- a/internal/serial/protocol.go +++ /dev/null @@ -1,70 +0,0 @@ -// This file is part of arduino-cloud-cli. -// -// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package serial - -var ( - // msgStart is the initial byte sequence of every packet. - msgStart = [2]byte{0x55, 0xAA} - // msgEnd is the final byte sequence of every packet. - msgEnd = [2]byte{0xAA, 0x55} -) - -const ( - // payloadField indicates the position of payload field. - payloadField = 5 - // payloadLenField indicates the position of payload length field. - payloadLenField = 3 - // payloadLenFieldLen indicatest the length of payload length field. - payloadLenFieldLen = 2 - // crcFieldLen indicates the length of the signature field. - crcFieldLen = 2 -) - -// MsgType indicates the type of the packet. -type MsgType byte - -const ( - None MsgType = iota - Cmd - Data - Response -) - -// Command indicates the command that should be -// executed on the board to be provisioned. -type Command byte - -const ( - SketchInfo Command = iota + 1 - CSR - Locked - GetLocked - WriteCrypto - BeginStorage - SetDeviceID - SetYear - SetMonth - SetDay - SetHour - SetValidity - SetCertSerial - SetAuthKey - SetSignature - EndStorage - ReconstructCert -) diff --git a/internal/serial/serial.go b/internal/serial/serial.go index abcb4951..aed7c404 100644 --- a/internal/serial/serial.go +++ b/internal/serial/serial.go @@ -18,14 +18,12 @@ package serial import ( - "bytes" - "context" - "encoding/binary" "errors" "fmt" "time" - "github.com/howeyc/crc16" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/frame" + "github.com/arduino/arduino-cloud-cli/internal/board-protocols/transport" "go.bug.st/serial" ) @@ -33,7 +31,8 @@ import ( // features specific functions to send provisioning // commands through the serial port to an arduino device. type Serial struct { - port serial.Port + port serial.Port + connected bool } // NewSerial instantiate and returns a Serial instance. @@ -41,35 +40,28 @@ type Serial struct { // its send/receive functions. func NewSerial() *Serial { s := &Serial{} + s.connected = false return s } // Connect tries to connect Serial to a specific serial port. -func (s *Serial) Connect(address string) error { +func (s *Serial) Connect(params transport.TransportInterfaceParams) error { mode := &serial.Mode{ - BaudRate: 57600, + BaudRate: params.BoundRate, } - port, err := serial.Open(address, mode) + port, err := serial.Open(params.Port, mode) if err != nil { err = fmt.Errorf("%s: %w", "connecting to serial port", err) return err } s.port = port - + s.connected = true s.port.SetReadTimeout(time.Millisecond * 2500) return nil } -// Send allows to send a provisioning command to a connected arduino device. -func (s *Serial) Send(ctx context.Context, cmd Command, payload []byte) error { - if err := ctx.Err(); err != nil { - return err - } - - payload = append([]byte{byte(cmd)}, payload...) - msg := encode(Cmd, payload) - - _, err := s.port.Write(msg) +func (s *Serial) Send(data []byte) error { + _, err := s.port.Write(data) if err != nil { err = fmt.Errorf("%s: %w", "sending message through serial", err) return err @@ -78,103 +70,47 @@ func (s *Serial) Send(ctx context.Context, cmd Command, payload []byte) error { return nil } -// SendReceive allows to send a provisioning command to a connected arduino device. -// Then, it waits for a response from the device and, if any, returns it. -// If no response is received after 2 seconds, an error is returned. -func (s *Serial) SendReceive(ctx context.Context, cmd Command, payload []byte) ([]byte, error) { - if err := s.Send(ctx, cmd, payload); err != nil { - return nil, err - } - return s.receive(ctx) -} - // Close should be used when the Serial connection isn't used anymore. // After that, Serial could Connect again to any port. func (s *Serial) Close() error { + s.connected = false return s.port.Close() } -// receive allows to wait for a response from an arduino device under provisioning. -// Its timeout is set to 2 seconds. It returns an error if the response is not valid -// or if the timeout expires. -// TODO: consider refactoring using a more explicit procedure: -// start := s.Read(buff, MsgStartLength) -// payloadLen := s.Read(buff, payloadFieldLen) -func (s *Serial) receive(ctx context.Context) ([]byte, error) { - buff := make([]byte, 1000) - var resp []byte - - received := 0 - payloadLen := 0 - // Wait to receive the entire packet that is long as the preamble (from msgStart to payload length field) - // plus the actual payload length plus the length of the ending sequence. - for received < (payloadLenField+payloadLenFieldLen)+payloadLen+len(msgEnd) { - if err := ctx.Err(); err != nil { - return nil, err - } +func (s *Serial) Receive(timeoutSeconds int) ([]frame.Frame, error) { + if !s.connected { + return nil, errors.New("serial port not connected") + } - n, err := s.port.Read(buff) + expireTimeout := time.Now().Add(time.Duration(timeoutSeconds) * time.Second) + received := false + transportController := transport.NewTransportController() + packets := []frame.Frame{} + + for !received && time.Now().Before(expireTimeout) { + buffer := make([]byte, 1024) + n, err := s.port.Read(buffer) if err != nil { - err = fmt.Errorf("%s: %w", "receiving from serial", err) return nil, err } - if n == 0 { - break - } - received += n - resp = append(resp, buff[:n]...) - // Update the payload length as soon as it is received. - if payloadLen == 0 && received >= (payloadLenField+payloadLenFieldLen) { - payloadLen = int(binary.BigEndian.Uint16(resp[payloadLenField:(payloadLenField + payloadLenFieldLen)])) - // TODO: return error if payloadLen is too large. + packets = transportController.HandleReceivedData(buffer[:n]) + if len(packets) > 0 { + received = true } } - if received == 0 { - err := errors.New("receiving from serial: timeout, nothing received") - return nil, err - } - - // TODO: check if msgStart is present - - if !bytes.Equal(resp[received-len(msgEnd):], msgEnd[:]) { - err := errors.New("receiving from serial: end of message (0xAA, 0x55) not found") - return nil, err + if !received { + return nil, fmt.Errorf("no response received after %d seconds", timeoutSeconds) } - payload := resp[payloadField : payloadField+payloadLen-crcFieldLen] - ch := crc16.Checksum(payload, crc16.CCITTTable) - // crc is contained in the last bytes of the payload - cp := binary.BigEndian.Uint16(resp[payloadField+payloadLen-crcFieldLen : payloadField+payloadLen]) - if ch != cp { - err := errors.New("receiving from serial: signature of received message is not valid") - return nil, err - } - - return payload, nil + return packets, nil } -// encode is internally used to create a valid provisioning packet. -func encode(mType MsgType, msg []byte) []byte { - // Insert the preamble sequence followed by the message type - packet := append(msgStart[:], byte(mType)) - - // Append the packet length - bLen := make([]byte, payloadLenFieldLen) - binary.BigEndian.PutUint16(bLen, (uint16(len(msg) + crcFieldLen))) - packet = append(packet, bLen...) - - // Append the message payload - packet = append(packet, msg...) - - // Calculate and append the message signature - ch := crc16.Checksum(msg, crc16.CCITTTable) - checksum := make([]byte, crcFieldLen) - binary.BigEndian.PutUint16(checksum, ch) - packet = append(packet, checksum...) +func (s *Serial) Type() transport.InterfaceType { + return transport.Serial +} - // Append final byte sequence - packet = append(packet, msgEnd[:]...) - return packet +func (s *Serial) Connected() bool { + return s.connected } diff --git a/internal/serial/serial_test.go b/internal/serial/serial_test.go deleted file mode 100644 index 1c049393..00000000 --- a/internal/serial/serial_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// This file is part of arduino-cloud-cli. -// -// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package serial - -import ( - "bytes" - "context" - "testing" - - "github.com/arduino/arduino-cloud-cli/internal/serial/mocks" - "github.com/stretchr/testify/mock" -) - -func TestSendReceive(t *testing.T) { - mockPort := &mocks.Port{} - mockSerial := &Serial{mockPort} - - want := []byte{1, 2, 3} - resp := encode(Response, want) - respIdx := 0 - - mockRead := func(msg []uint8) int { - if respIdx >= len(resp) { - return 0 - } - copy(msg, resp[respIdx:respIdx+2]) - respIdx += 2 - return 2 - } - - mockPort.On("Write", mock.AnythingOfType("[]uint8")).Return(0, nil) - mockPort.On("Read", mock.AnythingOfType("[]uint8")).Return(mockRead, nil) - - res, err := mockSerial.SendReceive(context.TODO(), BeginStorage, []byte{1, 2}) - if err != nil { - t.Error(err) - } - - if !bytes.Equal(res, want) { - t.Errorf("Expected %v but received %v", want, res) - } -} - -func TestSend(t *testing.T) { - mockPort := &mocks.Port{} - mockSerial := &Serial{mockPort} - mockPort.On("Write", mock.AnythingOfType("[]uint8")).Return(0, nil) - - payload := []byte{1, 2} - cmd := SetDay - want := []byte{msgStart[0], msgStart[1], 1, 0, 5, 10, 1, 2, 143, 124, msgEnd[0], msgEnd[1]} - - err := mockSerial.Send(context.TODO(), cmd, payload) - if err != nil { - t.Error(err) - } - - mockPort.AssertCalled(t, "Write", want) -} - -func TestEncode(t *testing.T) { - tests := []struct { - name string - msg []byte - want []byte - }{ - { - name: "begin-storage", - msg: []byte{byte(BeginStorage)}, - want: []byte{msgStart[0], msgStart[1], 1, 0, 3, 6, 0x95, 0x4e, msgEnd[0], msgEnd[1]}, - }, - - { - name: "set-year", - msg: append([]byte{byte(SetYear)}, []byte("2021")...), - want: []byte{msgStart[0], msgStart[1], 1, 0, 7, 0x8, 0x32, 0x30, 0x32, 0x31, 0xc3, 0x65, msgEnd[0], msgEnd[1]}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := encode(Cmd, tt.msg) - if !bytes.Equal(tt.want, got) { - t.Errorf("Expected %v, received %v", tt.want, got) - } - }) - } -}