Skip to content

Commit 0989562

Browse files
committed
Add device create-lora command
1 parent 8034348 commit 0989562

File tree

4 files changed

+301
-0
lines changed

4 files changed

+301
-0
lines changed

cli/device/createlora.go

Lines changed: 101 additions & 0 deletions
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 '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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func NewCommand() *cobra.Command {
3434
deviceCommand.AddCommand(initDeleteCommand())
3535
deviceCommand.AddCommand(tag.InitCreateTagsCommand())
3636
deviceCommand.AddCommand(tag.InitDeleteTagsCommand())
37+
deviceCommand.AddCommand(initCreateLoraCommand())
3738

3839
return deviceCommand
3940
}

command/device/createlora.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
iotClient.DeviceDelete(dev.DeviceId)
104+
err = fmt.Errorf("%s: %w", "cannot provision LoRa device", err)
105+
return nil, err
106+
}
107+
return devInfo, nil
108+
}
109+
110+
// deveuiBinary gets the absolute path of the deveui binary corresponding to the
111+
// provisioned board's fqbn. It is contained in the local binaries folder.
112+
func deveuiBinary(fqbn string) (string, error) {
113+
// Use local binaries until they are uploaded online
114+
bin := filepath.Join("./binaries/", "getdeveui."+strings.ReplaceAll(fqbn, ":", ".")+".bin")
115+
bin, err := filepath.Abs(bin)
116+
if err != nil {
117+
return "", fmt.Errorf("getting the deveui binary: %w", err)
118+
}
119+
if _, err := os.Stat(bin); os.IsNotExist(err) {
120+
err = fmt.Errorf("%s: %w", "deveui binary not found", err)
121+
return "", err
122+
}
123+
return bin, nil
124+
}
125+
126+
// extractEUI extracts the EUI from the provisioned lora board
127+
func extractEUI(port string) (string, error) {
128+
var ser serial.Port
129+
130+
logrus.Infof("%s\n", "Connecting to the board through serial port")
131+
errMsg := "Error while connecting to the board"
132+
err := retry(serialEUIAttempts, serialEUIWait*time.Millisecond, errMsg, func() error {
133+
var err error
134+
ser, err = serial.Open(port, &serial.Mode{BaudRate: serialEUIBaudrate})
135+
return err
136+
})
137+
if err != nil {
138+
return "", fmt.Errorf("failed to extract deveui from the board: %w", err)
139+
}
140+
141+
err = ser.SetReadTimeout(serialEUITimeout * time.Millisecond)
142+
if err != nil {
143+
return "", fmt.Errorf("setting serial read timeout: %w", err)
144+
}
145+
146+
buff := make([]byte, deveuiLength)
147+
n, err := ser.Read(buff)
148+
if err != nil {
149+
return "", fmt.Errorf("reading from serial: %w", err)
150+
}
151+
152+
if n < deveuiLength {
153+
return "", errors.New("cannot read eui from the device")
154+
}
155+
eui := string(buff)
156+
return eui, nil
157+
}
158+
159+
func getDeviceLoraInfo(iotClient iot.Client, loraDev *iotclient.ArduinoLoradevicev1) (*DeviceLoraInfo, error) {
160+
dev, err := iotClient.DeviceShow(loraDev.DeviceId)
161+
if err != nil {
162+
return nil, fmt.Errorf("cannot retrieve device from the cloud: %w", err)
163+
}
164+
165+
devInfo := &DeviceLoraInfo{
166+
DeviceInfo: DeviceInfo{
167+
Name: dev.Name,
168+
ID: dev.Id,
169+
Board: dev.Type,
170+
Serial: dev.Serial,
171+
FQBN: dev.Fqbn,
172+
},
173+
AppEUI: loraDev.AppEui,
174+
AppKey: loraDev.AppKey,
175+
EUI: loraDev.Eui,
176+
}
177+
return devInfo, nil
178+
}

internal/iot/client.go

Lines changed: 21 additions & 0 deletions
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)
@@ -83,6 +84,26 @@ func (cl *client) DeviceCreate(fqbn, name, serial, dType string) (*iotclient.Ard
8384
return &dev, nil
8485
}
8586

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

0 commit comments

Comments
 (0)