github.com/arduino/arduino-cloud-cli@v0.0.0-20240517070944-e7a449561083/command/device/createlora.go (about) 1 // This file is part of arduino-cloud-cli. 2 // 3 // Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published 7 // by the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 package device 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 "time" 25 26 "github.com/arduino/arduino-cloud-cli/arduino/cli" 27 "github.com/arduino/arduino-cloud-cli/config" 28 "github.com/arduino/arduino-cloud-cli/internal/iot" 29 iotclient "github.com/arduino/iot-client-go" 30 "github.com/sirupsen/logrus" 31 "go.bug.st/serial" 32 ) 33 34 const ( 35 deveuiUploadAttempts = 3 36 deveuiUploadWait = 1000 37 38 serialEUIAttempts = 4 39 serialEUIWait = 2000 40 serialEUITimeout = 3500 41 serialEUIBaudrate = 9600 42 43 // dev-eui is an IEEE EUI64 address, so it must have length of 8 bytes. 44 // It's retrieved as hexadecimal string, thus 16 chars are expected. 45 deveuiLength = 16 46 ) 47 48 // DeviceLoraInfo contains the most interesting 49 // parameters of an Arduino IoT Cloud LoRa device. 50 type DeviceLoraInfo struct { 51 DeviceInfo 52 AppEUI string `json:"app_eui"` 53 AppKey string `json:"app_key"` 54 EUI string `json:"eui"` 55 } 56 57 // CreateLoRaParams contains the parameters needed 58 // to provision a LoRa device. 59 type CreateLoraParams struct { 60 CreateParams 61 FrequencyPlan string 62 } 63 64 // CreateLora command is used to provision a new LoRa arduino device 65 // and to add it to Arduino IoT Cloud. 66 func CreateLora(ctx context.Context, params *CreateLoraParams, cred *config.Credentials) (*DeviceLoraInfo, error) { 67 comm, err := cli.NewCommander() 68 if err != nil { 69 return nil, err 70 } 71 72 ports, err := comm.BoardList(ctx) 73 if err != nil { 74 return nil, err 75 } 76 board := boardFromPorts(ports, ¶ms.CreateParams) 77 if board == nil { 78 err = errors.New("no board found") 79 return nil, err 80 } 81 82 if !board.isLora() { 83 return nil, fmt.Errorf( 84 "board with fqbn %s found at port %s is not a LoRa device."+ 85 " Try the 'create' command instead if it's a device with a supported crypto-chip"+ 86 " or 'create-generic' otherwise", 87 board.fqbn, 88 board.address, 89 ) 90 } 91 92 bin, err := downloadProvisioningFile(ctx, board.fqbn) 93 if err != nil { 94 return nil, err 95 } 96 97 logrus.Infof("%s", "Uploading deveui sketch on the LoRa board") 98 errMsg := "Error while uploading the LoRa provisioning binary" 99 err = retry(ctx, deveuiUploadAttempts, deveuiUploadWait*time.Millisecond, errMsg, func() error { 100 return comm.UploadBin(ctx, board.fqbn, bin, board.address, board.protocol) 101 }) 102 if err != nil { 103 return nil, fmt.Errorf("failed to upload LoRa provisioning binary: %w", err) 104 } 105 106 eui, err := extractEUI(ctx, board.address) 107 if err != nil { 108 return nil, err 109 } 110 111 iotClient, err := iot.NewClient(cred) 112 if err != nil { 113 return nil, err 114 } 115 116 logrus.Info("Creating a new device on the cloud") 117 dev, err := iotClient.DeviceLoraCreate(ctx, params.Name, board.serial, board.dType, eui, params.FrequencyPlan) 118 if err != nil { 119 return nil, err 120 } 121 122 devInfo, err := getDeviceLoraInfo(ctx, iotClient, dev) 123 if err != nil { 124 // Don't use the passed context for the cleanup because it could be cancelled. 125 errDel := iotClient.DeviceDelete(context.Background(), dev.DeviceId) 126 if errDel != nil { // Oh no 127 return nil, fmt.Errorf( 128 "device was successfully provisioned and configured on IoT-API but " + 129 "now we can't fetch its information nor delete it - please check " + 130 "it on the web application.\n\nFetch error: " + err.Error() + 131 "\nDeletion error: " + errDel.Error(), 132 ) 133 } 134 return nil, fmt.Errorf("%s: %w", "cannot provision LoRa device", err) 135 } 136 return devInfo, nil 137 } 138 139 // extractEUI extracts the EUI from the provisioned lora board. 140 func extractEUI(ctx context.Context, port string) (string, error) { 141 var ser serial.Port 142 143 logrus.Infof("%s\n", "Connecting to the board through serial port") 144 errMsg := "Error while connecting to the board" 145 err := retry(ctx, serialEUIAttempts, serialEUIWait*time.Millisecond, errMsg, func() error { 146 var err error 147 ser, err = serial.Open(port, &serial.Mode{BaudRate: serialEUIBaudrate}) 148 return err 149 }) 150 if err != nil { 151 return "", fmt.Errorf("failed to extract deveui from the board: %w", err) 152 } 153 154 err = ser.SetReadTimeout(serialEUITimeout * time.Millisecond) 155 if err != nil { 156 return "", fmt.Errorf("setting serial read timeout: %w", err) 157 } 158 159 buff := make([]byte, deveuiLength) 160 n, err := ser.Read(buff) 161 if err != nil { 162 return "", fmt.Errorf("reading from serial: %w", err) 163 } 164 165 if n < deveuiLength { 166 return "", errors.New("cannot read eui from the device") 167 } 168 eui := string(buff) 169 return eui, nil 170 } 171 172 func getDeviceLoraInfo(ctx context.Context, iotClient *iot.Client, loraDev *iotclient.ArduinoLoradevicev1) (*DeviceLoraInfo, error) { 173 dev, err := iotClient.DeviceShow(ctx, loraDev.DeviceId) 174 if err != nil { 175 return nil, fmt.Errorf("cannot retrieve device from the cloud: %w", err) 176 } 177 178 devInfo := &DeviceLoraInfo{ 179 DeviceInfo: DeviceInfo{ 180 Name: dev.Name, 181 ID: dev.Id, 182 Board: dev.Type, 183 Serial: dev.Serial, 184 FQBN: dev.Fqbn, 185 }, 186 AppEUI: loraDev.AppEui, 187 AppKey: loraDev.AppKey, 188 EUI: loraDev.Eui, 189 } 190 return devInfo, nil 191 }