Skip to content

Commit 56a1e79

Browse files
Paolo Calaopolldo
Paolo Calao
authored andcommitted
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 214ae0a commit 56a1e79

14 files changed

+526
-17
lines changed

Diff for: README.md

+72-12
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,92 @@
1-
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.
1+
# iot-cloud-cli
2+
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.
24

35
### Requirements
46

57
This is all you need to use iot-cloud-cli for device **provisioning**:
68
* A client ID and a secret ID, retrievable from the [cloud](https://create.arduino.cc/iot/integrations) by creating a new API key
7-
* The folder containing the precompiled provisioning firmwares (`binaries`) needs to be in the same location you run the command from
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+
815

916
## Set a configuration
1017

11-
iot-cloud-cli needs to be configured before being used. In particular a client ID and the corresponding secret ID should be set.
18+
iot-cloud-cli should be configured before being used. In particular a client ID and the corresponding secret ID should be set.
1219
You can retrieve them from the [cloud](https://create.arduino.cc/iot/integrations) by creating a new API key.
1320

1421
Once you have the IDs, call this command with your parameters:
1522

1623
`$ iot-cloud-cli config -c <clientID> -s <secretID>`
1724

18-
A file named `config.yaml` will be created in the Current Working Directory containing the login credentials.
19-
Example
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.
2036

21-
```yaml
22-
client: 00112233445566778899aabbccddeeff
23-
secret: 00112233445566778899aabbccddeeffffeeddccbbaa99887766554433221100
37+
```
38+
$ iot-cloud-cli ping -u "<deviceId>" -p "<secret>" -t <thing ID>>
39+
Connected to Arduino IoT Cloud
40+
Subscribed true
41+
Property value sent successfully 81
42+
Property value sent successfully 87
2443
```
2544

26-
## Device provisioning
45+
### How to set up the device and thing in IoT Cloud
2746

28-
When provisioning a device, you can optionally specify the port to which the device is connected to and its fqbn. If they are not given, then the first device found will be provisioned.
47+
#### Device
2948

30-
Use this command to provision a device:
49+
* Visit https://create.arduino.cc/iot/devices and select "Add device".
50+
* Select "Set up a 3rd party device".
51+
* Select "ESP8266".
52+
* From the drop down select "Generic ESP8266 Module", and click "Continue".
53+
* Pick a nice and friendly device name.
54+
* Save the "Device ID" and "Secret Key" is a safe place, because you will not be able to see them anymore.
55+
56+
#### Thing ID
3157

32-
`$ iot-cloud-cli device create --name <deviceName> --port <port> --fqbn <deviceFqbn>`
58+
* Visit https://create.arduino.cc/iot/things and select "Create Thing".
59+
* Select "Add Variable".
60+
* Give the variable the name "counter", type "Integer Number" and leave the variable permission the value "Read & Write".
61+
* Press the "Add Variable" button to confirm.
62+
* Copy the "Thing ID" from the bottom right of the page.
63+
64+
#### Connect the device and the thing
65+
66+
You should connect the new device to the new thing.
67+
68+
#### Testing
69+
70+
##### Connect to the PROD environment
71+
72+
```shell
73+
$ iot-cloud-cli ping -u "<Device ID>" -p "<Secret Key>" -t <Thing ID>>
74+
```
75+
76+
If every works as expected you should see something similar to this output:
77+
```
78+
Connected to Arduino IoT Cloud
79+
Subscribed true
80+
Property value sent successfully 81
81+
Property value sent successfully 87
82+
```
83+
84+
If you visit https://create.arduino.cc/iot/devices the "Generic ESP8266 Module" device status should be "Online".
85+
86+
##### Connect to the DEV environment
87+
88+
The DEV environment is using a different broker, so you need to add the option `--host`:
89+
90+
```shell
91+
$ iot-cloud-cli ping --host tcps://mqtts-sa.iot.oniudra.cc:8884 -u "<Device ID>" -p "<Secret Key>" -t "<thing-id>"
92+
```

Diff for: 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

Diff for: binaries/arduino.mbed_nano.nanorp2040connect.elf

3.91 MB
Binary file not shown.

Diff for: binaries/arduino.mbed_portenta.envie_m7.bin

533 KB
Binary file not shown.

Diff for: binaries/arduino.samd.mkr1000.bin

111 KB
Binary file not shown.

Diff for: binaries/arduino.samd.mkrgsm1400.bin

143 KB
Binary file not shown.

Diff for: binaries/arduino.samd.mkrnb1500.bin

111 KB
Binary file not shown.

Diff for: binaries/arduino.samd.mkrwifi1010.bin

80.1 KB
Binary file not shown.

Diff for: binaries/arduino.samd.nano_33_iot.bin

79.9 KB
Binary file not shown.

Diff for: 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+
}

Diff for: 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+
}

Diff for: cli/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ 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/spf13/cobra"
910
)
1011

1112
func Execute() {
1213
rootCmd := &cobra.Command{}
1314
rootCmd.AddCommand(config.NewCommand())
15+
rootCmd.AddCommand(device.NewCommand())
1416

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

Diff for: 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)