Skip to content

Commit 7b83b62

Browse files
author
Paolo Calao
authored
Add device create command (#11)
This PR introduces a command to provision arduino devices. It must be executed on a computer with an arduino device connected via USB. This command does everything needed to provision a device, including creating the device in Cloud, but also running the initial provisioning sketch that generates proper certificates to store in secure element. When using this command, you can optionally explicit the port which the device is connected to and its fqbn. If they are not given, then the first device found is provisioned. Usage: $ iot-cloud-cli device create --name <deviceName> --port <port> --fqbn <deviceFqbn> * Add local provisioning binaries * Add device command * Implement provisioning Fix log format Fix - add delay Fix - use same flag as arduinocli * Hack - don't check errstream from cli Problem: Uploading the provisioning bin to a portenta Description: even if the upload gets completed, the errstream from the rpc client of arduino-cli is not empty. It contains this: %!w(<nil>) Temporary solution (hacky): don't check the errstream * Split device create into cli and command Fix * Update readme
1 parent e173453 commit 7b83b62

14 files changed

+494
-19
lines changed

README.md

+40-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
11
# iot-cloud-cli
22

3-
The iot-cloud-cli is a virtual device for Arduino IoT Cloud for testing.
3+
iot-cloud-cli is a command line interface that allows to exploit the features of Arduino IoT Cloud. As of now, it is possible to provision a device and to simulate a device to be connected to the cloud using MQTT for thoubleshooting purpose.
4+
5+
### Requirements
6+
7+
This is all you need to use iot-cloud-cli for device **provisioning**:
8+
* A client ID and a secret ID, retrievable from the [cloud](https://create.arduino.cc/iot/integrations) by creating a new API key
9+
* arduino-cli in daemon mode, use the command 'arduino-cli daemon'
10+
11+
This is all you need to use iot-cloud-cli as a **virtual device**:
12+
* A "Generic ESP8266 Module" or "Generic ESP32 Module" device in IoT Cloud (requires a Maker plan)
13+
* A thing with a `counter` property connected to the "Generic ESP8266/ESP32 Module" device
14+
15+
16+
## Set a configuration
17+
18+
iot-cloud-cli should be configured before being used. In particular a client ID and the corresponding secret ID should be set.
19+
You can retrieve them from the [cloud](https://create.arduino.cc/iot/integrations) by creating a new API key.
20+
21+
Once you have the IDs, call this command with your parameters:
22+
23+
`$ iot-cloud-cli config -c <clientID> -s <secretID>`
24+
25+
## Device provisioning
26+
27+
When provisioning a device, you can optionally explicit the port which the device is connected to and its fqbn. If they are not given, then the first device found is provisioned.
28+
Use this command to provision a device:
29+
30+
`$ iot-cloud-cli device create --name <deviceName> --port <port> --fqbn <deviceFqbn>`
31+
32+
33+
## Use iot-cloud-cli as a virtual device
34+
35+
The iot-cloud-cli can be used as a virtual device for Arduino IoT Cloud for testing.
436

537
```
638
$ iot-cloud-cli ping -u "<deviceId>" -p "<secret>" -t <thing ID>>
@@ -10,15 +42,9 @@ $ iot-cloud-cli ping -u "<deviceId>" -p "<secret>" -t <thing ID>>
1042
Property value sent successfully 87
1143
```
1244

13-
## Requirements
14-
15-
This is all you need to use iot-cloud-cli:
16-
* A "Generic ESP8266 Module" device in IoT Cloud (requires a Maker plan)
17-
* A thing with a `counter` property connected to the "Generic ESP8266 Module" device
18-
19-
## How to set up the device and thing in IoT Cloud
45+
### How to set up the device and thing in IoT Cloud
2046

21-
### Device
47+
#### Device
2248

2349
* Visit https://create.arduino.cc/iot/devices and select "Add device".
2450
* Select "Set up a 3rd party device".
@@ -27,21 +53,21 @@ This is all you need to use iot-cloud-cli:
2753
* Pick a nice and friendly device name.
2854
* Save the "Device ID" and "Secret Key" is a safe place, because you will not be able to see them anymore.
2955

30-
### Thing ID
56+
#### Thing ID
3157

3258
* Visit https://create.arduino.cc/iot/things and select "Create Thing".
3359
* Select "Add Variable".
3460
* Give the variable the name "counter", type "Integer Number" and leave the variable permission the value "Read & Write".
3561
* Press the "Add Variable" button to confirm.
3662
* Copy the "Thing ID" from the bottom right of the page.
3763

38-
### Connect the device and the thing
64+
#### Connect the device and the thing
3965

4066
You should connect the new device to the new thing.
4167

42-
### Testing
68+
#### Testing
4369

44-
#### Connect to the PROD environment
70+
##### Connect to the PROD environment
4571

4672
```shell
4773
$ iot-cloud-cli ping -u "<Device ID>" -p "<Secret Key>" -t <Thing ID>>
@@ -57,7 +83,7 @@ Property value sent successfully 87
5783

5884
If you visit https://create.arduino.cc/iot/devices the "Generic ESP8266 Module" device status should be "Online".
5985

60-
#### Connect to the DEV environment
86+
##### Connect to the DEV environment
6187

6288
The DEV environment is using a different broker, so you need to add the option `--host`:
6389

arduino/grpc/compile.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,14 @@ func (c compileHandler) UploadBin(fqbn, bin, port string) error {
3939

4040
// Wait for the upload to complete
4141
for {
42-
resp, err := stream.Recv()
42+
_, err := stream.Recv()
4343
if err != nil {
4444
if err == io.EOF {
4545
break
4646
}
4747
err = fmt.Errorf("%s: %w", "errors during upload", err)
4848
return err
4949
}
50-
if resp.ErrStream != nil {
51-
err = fmt.Errorf("%s: %w", "errors during upload", err)
52-
return err
53-
}
5450
}
5551

5652
return nil
3.91 MB
Binary file not shown.
533 KB
Binary file not shown.

binaries/arduino.samd.mkr1000.bin

111 KB
Binary file not shown.

binaries/arduino.samd.mkrgsm1400.bin

143 KB
Binary file not shown.

binaries/arduino.samd.mkrnb1500.bin

111 KB
Binary file not shown.

binaries/arduino.samd.mkrwifi1010.bin

80.1 KB
Binary file not shown.

binaries/arduino.samd.nano_33_iot.bin

79.9 KB
Binary file not shown.

cli/device/create.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package device
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/bcmi-labs/iot-cloud-cli/command/device"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var createFlags struct {
11+
port string
12+
name string
13+
fqbn string
14+
}
15+
16+
func initCreateCommand() *cobra.Command {
17+
createCommand := &cobra.Command{
18+
Use: "create",
19+
Short: "Create a device",
20+
Long: "Create a device for Arduino IoT Cloud",
21+
RunE: runCreateCommand,
22+
}
23+
createCommand.Flags().StringVarP(&createFlags.port, "port", "p", "", "Device port")
24+
createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Device name")
25+
createCommand.Flags().StringVarP(&createFlags.fqbn, "fqbn", "b", "", "Device fqbn")
26+
createCommand.MarkFlagRequired("name")
27+
return createCommand
28+
}
29+
30+
func runCreateCommand(cmd *cobra.Command, args []string) error {
31+
fmt.Printf("Creating device with name %s\n", createFlags.name)
32+
33+
params := &device.CreateParams{
34+
Port: createFlags.port,
35+
Name: createFlags.name,
36+
Fqbn: createFlags.fqbn,
37+
}
38+
39+
devID, err := device.Create(params)
40+
if err != nil {
41+
return err
42+
}
43+
44+
fmt.Printf("IoT Cloud device created with ID: %s\n", devID)
45+
return nil
46+
}

cli/device/device.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package device
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
func NewCommand() *cobra.Command {
8+
deviceCommand := &cobra.Command{
9+
Use: "device",
10+
Short: "Device commands.",
11+
Long: "Device commands.",
12+
}
13+
14+
deviceCommand.AddCommand(initCreateCommand())
15+
16+
return deviceCommand
17+
}

cli/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
"github.com/bcmi-labs/iot-cloud-cli/cli/config"
8+
"github.com/bcmi-labs/iot-cloud-cli/cli/device"
89
"github.com/bcmi-labs/iot-cloud-cli/cli/ping"
910
"github.com/spf13/cobra"
1011
)
@@ -13,6 +14,7 @@ func Execute() {
1314
rootCmd := &cobra.Command{}
1415
rootCmd.AddCommand(ping.NewCommand())
1516
rootCmd.AddCommand(config.NewCommand())
17+
rootCmd.AddCommand(device.NewCommand())
1618

1719
if err := rootCmd.Execute(); err != nil {
1820
fmt.Fprintln(os.Stderr, err)

command/device/create.go

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package device
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
9+
"github.com/bcmi-labs/iot-cloud-cli/arduino/grpc"
10+
"github.com/bcmi-labs/iot-cloud-cli/internal/config"
11+
"github.com/bcmi-labs/iot-cloud-cli/internal/iot"
12+
)
13+
14+
// CreateParams contains the paramters needed
15+
// to find the device to be provisioned.
16+
// If Port is an empty string, then each serial port is analyzed.
17+
// If Fqbn is an empty string, then the first device found gets selected.
18+
type CreateParams struct {
19+
Port string
20+
Name string
21+
Fqbn string
22+
}
23+
24+
type device struct {
25+
fqbn string
26+
serial string
27+
dType string
28+
port string
29+
}
30+
31+
// Create command is used to provision a new arduino device
32+
// and to add it to the arduino iot cloud.
33+
func Create(params *CreateParams) (string, error) {
34+
rpcComm, rpcClose, err := grpc.NewClient()
35+
if err != nil {
36+
return "", err
37+
}
38+
defer rpcClose()
39+
40+
ports, err := rpcComm.BoardList()
41+
if err != nil {
42+
return "", err
43+
}
44+
dev := deviceFromPorts(ports, params)
45+
if dev == nil {
46+
err = errors.New("no device found")
47+
return "", err
48+
}
49+
50+
conf, err := config.Retrieve()
51+
if err != nil {
52+
return "", err
53+
}
54+
iotClient, err := iot.NewClient(conf.Client, conf.Secret)
55+
if err != nil {
56+
return "", err
57+
}
58+
59+
fmt.Println("Creating a new device on the cloud")
60+
devID, err := iotClient.AddDevice(dev.fqbn, params.Name, dev.serial, dev.dType)
61+
if err != nil {
62+
return "", err
63+
}
64+
65+
prov := &provision{
66+
Commander: rpcComm,
67+
Client: iotClient,
68+
dev: dev,
69+
id: devID}
70+
err = prov.run()
71+
if err != nil {
72+
// TODO: delete the device on iot cloud
73+
err = fmt.Errorf("%s: %w", "cannot provision device", err)
74+
return "", err
75+
}
76+
77+
return devID, nil
78+
}
79+
80+
// deviceFromPorts returns a board that matches all the criteria
81+
// passed in. If no criteria are passed, it returns the first device found.
82+
func deviceFromPorts(ports []*rpc.DetectedPort, params *CreateParams) *device {
83+
for _, port := range ports {
84+
if portFilter(port, params) {
85+
continue
86+
}
87+
board := boardFilter(port.Boards, params)
88+
if board != nil {
89+
t := strings.Split(board.Fqbn, ":")[2]
90+
dev := &device{board.Fqbn, port.SerialNumber, t, port.Address}
91+
return dev
92+
}
93+
}
94+
95+
return nil
96+
}
97+
98+
// portFilter filters out the given port if the port parameter is not an empty string
99+
// and if they do not match.
100+
// It returns:
101+
// true -> to skip the port
102+
// false -> to keep the port
103+
func portFilter(port *rpc.DetectedPort, params *CreateParams) bool {
104+
if len(port.Boards) == 0 {
105+
return true
106+
}
107+
if params.Port != "" && params.Port != port.Address {
108+
return true
109+
}
110+
return false
111+
}
112+
113+
// boardFilter looks for a board which has the same fqbn passed as parameter.
114+
// It returns:
115+
// - a board if it is found.
116+
// - nil if no board matching the fqbn parameter is found.
117+
func boardFilter(boards []*rpc.BoardListItem, params *CreateParams) (board *rpc.BoardListItem) {
118+
if params.Fqbn == "" {
119+
return boards[0]
120+
}
121+
for _, b := range boards {
122+
if b.Fqbn == params.Fqbn {
123+
return b
124+
}
125+
}
126+
return
127+
}

0 commit comments

Comments
 (0)