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)
- }
- })
- }
-}