Skip to content

Commit a799f96

Browse files
Paolo Calaopolldo
Paolo Calao
authored andcommitted
Add device create-lora command (#64)
This new command enables the provisioning of LoRa devices. The LoRa provisioning process is similar to the standard one (for boards having crypto), but it has relevant differences and it is way simpler.
1 parent 270e126 commit a799f96

File tree

8 files changed

+337
-0
lines changed

8 files changed

+337
-0
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,16 @@ Use this command to provision a device:
6565

6666
####LoRa
6767

68+
LoRa devices should be provisioned using a specific command.
69+
Parameters are the same except for the additional mandatory `--frequency-plan`:
70+
71+
`$ arduino-cloud-cli device create-lora --name <deviceName> --frequency-plan <freqID> --port <port> --fqbn <deviceFqbn>`
72+
6873
The list of supported LoRa frequency plans can be retrieved with:
6974

7075
`$ arduino-cloud-cli device list-frequency-plans`
7176

77+
7278
## Device commands
7379

7480
Devices can be deleted using the device delete command. This command accepts two mutually exclusive flags: `--id` and `--tags`. Only one of them must be passed. When the `--id` is passed, the device having such ID gets deleted:
16.4 KB
Binary file not shown.
Binary file not shown.

cli/device/createlora.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
"fmt"
22+
"os"
23+
24+
"github.com/arduino/arduino-cli/cli/errorcodes"
25+
"github.com/arduino/arduino-cli/cli/feedback"
26+
"github.com/arduino/arduino-cloud-cli/command/device"
27+
"github.com/sirupsen/logrus"
28+
"github.com/spf13/cobra"
29+
)
30+
31+
var createLoraFlags struct {
32+
port string
33+
name string
34+
fqbn string
35+
frequencyPlan string
36+
}
37+
38+
func initCreateLoraCommand() *cobra.Command {
39+
createLoraCommand := &cobra.Command{
40+
Use: "create-lora",
41+
Short: "Create a LoRa device",
42+
Long: "Create a LoRa device for Arduino IoT Cloud",
43+
Run: runCreateLoraCommand,
44+
}
45+
createLoraCommand.Flags().StringVarP(&createLoraFlags.port, "port", "p", "", "Device port")
46+
createLoraCommand.Flags().StringVarP(&createLoraFlags.name, "name", "n", "", "Device name")
47+
createLoraCommand.Flags().StringVarP(&createLoraFlags.fqbn, "fqbn", "b", "", "Device fqbn")
48+
createLoraCommand.Flags().StringVarP(&createLoraFlags.frequencyPlan, "frequency-plan", "f", "",
49+
"ID of the LoRa frequency plan to use. Run the 'device list-frequency-plans' command to obtain a list of valid plans.")
50+
createLoraCommand.MarkFlagRequired("name")
51+
createLoraCommand.MarkFlagRequired("frequency-plan")
52+
return createLoraCommand
53+
}
54+
55+
func runCreateLoraCommand(cmd *cobra.Command, args []string) {
56+
logrus.Infof("Creating LoRa device with name %s", createLoraFlags.name)
57+
58+
params := &device.CreateLoraParams{
59+
CreateParams: device.CreateParams{
60+
Name: createLoraFlags.name,
61+
},
62+
FrequencyPlan: createLoraFlags.frequencyPlan,
63+
}
64+
if createLoraFlags.port != "" {
65+
params.Port = &createLoraFlags.port
66+
}
67+
if createLoraFlags.fqbn != "" {
68+
params.Fqbn = &createLoraFlags.fqbn
69+
}
70+
71+
dev, err := device.CreateLora(params)
72+
if err != nil {
73+
feedback.Errorf("Error during device create-lora: %v", err)
74+
os.Exit(errorcodes.ErrGeneric)
75+
}
76+
77+
feedback.PrintResult(createLoraResult{dev})
78+
}
79+
80+
type createLoraResult struct {
81+
device *device.DeviceLoraInfo
82+
}
83+
84+
func (r createLoraResult) Data() interface{} {
85+
return r.device
86+
}
87+
88+
func (r createLoraResult) String() string {
89+
return fmt.Sprintf(
90+
"name: %s\nid: %s\nboard: %s\nserial-number: %s\nfqbn: %s"+
91+
"\napp-eui: %s\napp-key: %s\neui: %s",
92+
r.device.Name,
93+
r.device.ID,
94+
r.device.Board,
95+
r.device.Serial,
96+
r.device.FQBN,
97+
r.device.AppEUI,
98+
r.device.AppKey,
99+
r.device.EUI,
100+
)
101+
}

cli/device/device.go

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func NewCommand() *cobra.Command {
3535
deviceCommand.AddCommand(tag.InitCreateTagsCommand())
3636
deviceCommand.AddCommand(tag.InitDeleteTagsCommand())
3737
deviceCommand.AddCommand(initListFrequencyPlansCommand())
38+
deviceCommand.AddCommand(initCreateLoraCommand())
3839

3940
return deviceCommand
4041
}

command/device/createlora.go

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package device
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"time"
10+
11+
"github.com/arduino/arduino-cloud-cli/arduino/cli"
12+
"github.com/arduino/arduino-cloud-cli/internal/config"
13+
"github.com/arduino/arduino-cloud-cli/internal/iot"
14+
iotclient "github.com/arduino/iot-client-go"
15+
"github.com/sirupsen/logrus"
16+
"go.bug.st/serial"
17+
)
18+
19+
const (
20+
deveuiUploadAttempts = 3
21+
deveuiUploadWait = 1000
22+
23+
serialEUIAttempts = 4
24+
serialEUIWait = 2000
25+
serialEUITimeout = 3500
26+
serialEUIBaudrate = 9600
27+
28+
// dev-eui is an IEEE EUI64 address, so it must have length of 8 bytes.
29+
// It's retrieved as hexadecimal string, thus 16 chars are expected
30+
deveuiLength = 16
31+
)
32+
33+
// DeviceLoraInfo contains the most interesting
34+
// parameters of an Arduino IoT Cloud LoRa device.
35+
type DeviceLoraInfo struct {
36+
DeviceInfo
37+
AppEUI string `json:"app-eui"`
38+
AppKey string `json:"app-key"`
39+
EUI string `json:"eui"`
40+
}
41+
42+
// CreateLoRaParams contains the parameters needed
43+
// to provision a LoRa device.
44+
type CreateLoraParams struct {
45+
CreateParams
46+
FrequencyPlan string
47+
}
48+
49+
// CreateLora command is used to provision a new LoRa arduino device
50+
// and to add it to Arduino IoT Cloud.
51+
func CreateLora(params *CreateLoraParams) (*DeviceLoraInfo, error) {
52+
comm, err := cli.NewCommander()
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
ports, err := comm.BoardList()
58+
if err != nil {
59+
return nil, err
60+
}
61+
board := boardFromPorts(ports, &params.CreateParams)
62+
if board == nil {
63+
err = errors.New("no board found")
64+
return nil, err
65+
}
66+
67+
bin, err := deveuiBinary(board.fqbn)
68+
if err != nil {
69+
return nil, fmt.Errorf("fqbn not supported for LoRa provisioning: %w", err)
70+
}
71+
72+
logrus.Infof("%s", "Uploading deveui sketch on the LoRa board")
73+
errMsg := "Error while uploading the LoRa provisioning binary"
74+
err = retry(deveuiUploadAttempts, deveuiUploadWait*time.Millisecond, errMsg, func() error {
75+
return comm.UploadBin(board.fqbn, bin, board.port)
76+
})
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to upload LoRa provisioning binary: %w", err)
79+
}
80+
81+
eui, err := extractEUI(board.port)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
conf, err := config.Retrieve()
87+
if err != nil {
88+
return nil, err
89+
}
90+
iotClient, err := iot.NewClient(conf.Client, conf.Secret)
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
logrus.Info("Creating a new device on the cloud")
96+
dev, err := iotClient.DeviceLoraCreate(params.Name, board.serial, board.dType, eui, params.FrequencyPlan)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
devInfo, err := getDeviceLoraInfo(iotClient, dev)
102+
if err != nil {
103+
errDel := iotClient.DeviceDelete(dev.DeviceId)
104+
if errDel != nil { // Oh no
105+
return nil, fmt.Errorf(
106+
"device was successfully provisioned and configured on IoT-API but " +
107+
"now we can't fetch its information nor delete it - please check " +
108+
"it on the web application.\n\nFetch error: " + err.Error() +
109+
"\nDeletion error: " + errDel.Error(),
110+
)
111+
}
112+
return nil, fmt.Errorf("%s: %w", "cannot provision LoRa device", err)
113+
}
114+
return devInfo, nil
115+
}
116+
117+
// deveuiBinary gets the absolute path of the deveui binary corresponding to the
118+
// provisioned board's fqbn. It is contained in the local binaries folder.
119+
func deveuiBinary(fqbn string) (string, error) {
120+
// Use local binaries until they are uploaded online
121+
bin := filepath.Join("./binaries/", "getdeveui."+strings.ReplaceAll(fqbn, ":", ".")+".bin")
122+
bin, err := filepath.Abs(bin)
123+
if err != nil {
124+
return "", fmt.Errorf("getting the deveui binary: %w", err)
125+
}
126+
if _, err := os.Stat(bin); os.IsNotExist(err) {
127+
err = fmt.Errorf("%s: %w", "deveui binary not found", err)
128+
return "", err
129+
}
130+
return bin, nil
131+
}
132+
133+
// extractEUI extracts the EUI from the provisioned lora board.
134+
func extractEUI(port string) (string, error) {
135+
var ser serial.Port
136+
137+
logrus.Infof("%s\n", "Connecting to the board through serial port")
138+
errMsg := "Error while connecting to the board"
139+
err := retry(serialEUIAttempts, serialEUIWait*time.Millisecond, errMsg, func() error {
140+
var err error
141+
ser, err = serial.Open(port, &serial.Mode{BaudRate: serialEUIBaudrate})
142+
return err
143+
})
144+
if err != nil {
145+
return "", fmt.Errorf("failed to extract deveui from the board: %w", err)
146+
}
147+
148+
err = ser.SetReadTimeout(serialEUITimeout * time.Millisecond)
149+
if err != nil {
150+
return "", fmt.Errorf("setting serial read timeout: %w", err)
151+
}
152+
153+
buff := make([]byte, deveuiLength)
154+
n, err := ser.Read(buff)
155+
if err != nil {
156+
return "", fmt.Errorf("reading from serial: %w", err)
157+
}
158+
159+
if n < deveuiLength {
160+
return "", errors.New("cannot read eui from the device")
161+
}
162+
eui := string(buff)
163+
return eui, nil
164+
}
165+
166+
func getDeviceLoraInfo(iotClient iot.Client, loraDev *iotclient.ArduinoLoradevicev1) (*DeviceLoraInfo, error) {
167+
dev, err := iotClient.DeviceShow(loraDev.DeviceId)
168+
if err != nil {
169+
return nil, fmt.Errorf("cannot retrieve device from the cloud: %w", err)
170+
}
171+
172+
devInfo := &DeviceLoraInfo{
173+
DeviceInfo: DeviceInfo{
174+
Name: dev.Name,
175+
ID: dev.Id,
176+
Board: dev.Type,
177+
Serial: dev.Serial,
178+
FQBN: dev.Fqbn,
179+
},
180+
AppEUI: loraDev.AppEui,
181+
AppKey: loraDev.AppKey,
182+
EUI: loraDev.Eui,
183+
}
184+
return devInfo, nil
185+
}

internal/iot/client.go

+21
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
// Client can be used to perform actions on Arduino IoT Cloud.
3030
type Client interface {
3131
DeviceCreate(fqbn, name, serial, devType string) (*iotclient.ArduinoDevicev2, error)
32+
DeviceLoraCreate(name, serial, devType, eui, freq string) (*iotclient.ArduinoLoradevicev1, error)
3233
DeviceDelete(id string) error
3334
DeviceList(tags map[string]string) ([]iotclient.ArduinoDevicev2, error)
3435
DeviceShow(id string) (*iotclient.ArduinoDevicev2, error)
@@ -84,6 +85,26 @@ func (cl *client) DeviceCreate(fqbn, name, serial, dType string) (*iotclient.Ard
8485
return &dev, nil
8586
}
8687

88+
// DeviceLoraCreate allows to create a new LoRa device on Arduino IoT Cloud.
89+
// It returns the LoRa information about the newly created device, and an error.
90+
func (cl *client) DeviceLoraCreate(name, serial, devType, eui, freq string) (*iotclient.ArduinoLoradevicev1, error) {
91+
payload := iotclient.CreateLoraDevicesV1Payload{
92+
App: "defaultApp",
93+
Eui: eui,
94+
FrequencyPlan: freq,
95+
Name: name,
96+
Serial: serial,
97+
Type: devType,
98+
UserId: "me",
99+
}
100+
dev, _, err := cl.api.LoraDevicesV1Api.LoraDevicesV1Create(cl.ctx, payload)
101+
if err != nil {
102+
err = fmt.Errorf("creating lora device: %w", errorDetail(err))
103+
return nil, err
104+
}
105+
return &dev, nil
106+
}
107+
87108
// DeviceDelete deletes the device corresponding to the passed ID
88109
// from Arduino IoT Cloud.
89110
func (cl *client) DeviceDelete(id string) error {

internal/iot/mocks/Client.go

+23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)