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, &params.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  }