Skip to content

Add device create command #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 40 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
# iot-cloud-cli

The iot-cloud-cli is a virtual device for Arduino IoT Cloud for testing.
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.

### Requirements

This is all you need to use iot-cloud-cli for device **provisioning**:
* A client ID and a secret ID, retrievable from the [cloud](https://create.arduino.cc/iot/integrations) by creating a new API key
* arduino-cli in daemon mode, use the command 'arduino-cli daemon'

This is all you need to use iot-cloud-cli as a **virtual device**:
* A "Generic ESP8266 Module" or "Generic ESP32 Module" device in IoT Cloud (requires a Maker plan)
* A thing with a `counter` property connected to the "Generic ESP8266/ESP32 Module" device


## Set a configuration

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

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

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

## Device provisioning

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.
Use this command to provision a device:

`$ iot-cloud-cli device create --name <deviceName> --port <port> --fqbn <deviceFqbn>`


## Use iot-cloud-cli as a virtual device

The iot-cloud-cli can be used as a virtual device for Arduino IoT Cloud for testing.

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

## Requirements

This is all you need to use iot-cloud-cli:
* A "Generic ESP8266 Module" device in IoT Cloud (requires a Maker plan)
* A thing with a `counter` property connected to the "Generic ESP8266 Module" device

## How to set up the device and thing in IoT Cloud
### How to set up the device and thing in IoT Cloud

### Device
#### Device

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

### Thing ID
#### Thing ID

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

### Connect the device and the thing
#### Connect the device and the thing

You should connect the new device to the new thing.

### Testing
#### Testing

#### Connect to the PROD environment
##### Connect to the PROD environment

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

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

#### Connect to the DEV environment
##### Connect to the DEV environment

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

Expand Down
6 changes: 1 addition & 5 deletions arduino/grpc/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,14 @@ func (c compileHandler) UploadBin(fqbn, bin, port string) error {

// Wait for the upload to complete
for {
resp, err := stream.Recv()
_, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
err = fmt.Errorf("%s: %w", "errors during upload", err)
return err
}
if resp.ErrStream != nil {
err = fmt.Errorf("%s: %w", "errors during upload", err)
return err
}
}

return nil
Expand Down
Binary file added binaries/arduino.mbed_nano.nanorp2040connect.elf
Binary file not shown.
Binary file added binaries/arduino.mbed_portenta.envie_m7.bin
Binary file not shown.
Binary file added binaries/arduino.samd.mkr1000.bin
Binary file not shown.
Binary file added binaries/arduino.samd.mkrgsm1400.bin
Binary file not shown.
Binary file added binaries/arduino.samd.mkrnb1500.bin
Binary file not shown.
Binary file added binaries/arduino.samd.mkrwifi1010.bin
Binary file not shown.
Binary file added binaries/arduino.samd.nano_33_iot.bin
Binary file not shown.
46 changes: 46 additions & 0 deletions cli/device/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package device

import (
"fmt"

"github.com/bcmi-labs/iot-cloud-cli/command/device"
"github.com/spf13/cobra"
)

var createFlags struct {
port string
name string
fqbn string
}

func initCreateCommand() *cobra.Command {
createCommand := &cobra.Command{
Use: "create",
Short: "Create a device",
Long: "Create a device for Arduino IoT Cloud",
RunE: runCreateCommand,
}
createCommand.Flags().StringVarP(&createFlags.port, "port", "p", "", "Device port")
createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Device name")
createCommand.Flags().StringVarP(&createFlags.fqbn, "fqbn", "b", "", "Device fqbn")
createCommand.MarkFlagRequired("name")
return createCommand
}

func runCreateCommand(cmd *cobra.Command, args []string) error {
fmt.Printf("Creating device with name %s\n", createFlags.name)

params := &device.CreateParams{
Port: createFlags.port,
Name: createFlags.name,
Fqbn: createFlags.fqbn,
}

devID, err := device.Create(params)
if err != nil {
return err
}

fmt.Printf("IoT Cloud device created with ID: %s\n", devID)
return nil
}
17 changes: 17 additions & 0 deletions cli/device/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package device

import (
"github.com/spf13/cobra"
)

func NewCommand() *cobra.Command {
deviceCommand := &cobra.Command{
Use: "device",
Short: "Device commands.",
Long: "Device commands.",
}

deviceCommand.AddCommand(initCreateCommand())

return deviceCommand
}
2 changes: 2 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/bcmi-labs/iot-cloud-cli/cli/config"
"github.com/bcmi-labs/iot-cloud-cli/cli/device"
"github.com/bcmi-labs/iot-cloud-cli/cli/ping"
"github.com/spf13/cobra"
)
Expand All @@ -13,6 +14,7 @@ func Execute() {
rootCmd := &cobra.Command{}
rootCmd.AddCommand(ping.NewCommand())
rootCmd.AddCommand(config.NewCommand())
rootCmd.AddCommand(device.NewCommand())

if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down
127 changes: 127 additions & 0 deletions command/device/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package device

import (
"errors"
"fmt"
"strings"

rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/bcmi-labs/iot-cloud-cli/arduino/grpc"
"github.com/bcmi-labs/iot-cloud-cli/internal/config"
"github.com/bcmi-labs/iot-cloud-cli/internal/iot"
)

// CreateParams contains the paramters needed
// to find the device to be provisioned.
// If Port is an empty string, then each serial port is analyzed.
// If Fqbn is an empty string, then the first device found gets selected.
type CreateParams struct {
Port string
Name string
Fqbn string
}

type device struct {
fqbn string
serial string
dType string
port string
}

// Create command is used to provision a new arduino device
// and to add it to the arduino iot cloud.
func Create(params *CreateParams) (string, error) {
rpcComm, rpcClose, err := grpc.NewClient()
if err != nil {
return "", err
}
defer rpcClose()

ports, err := rpcComm.BoardList()
if err != nil {
return "", err
}
dev := deviceFromPorts(ports, params)
if dev == nil {
err = errors.New("no device found")
return "", err
}

conf, err := config.Retrieve()
if err != nil {
return "", err
}
iotClient, err := iot.NewClient(conf.Client, conf.Secret)
if err != nil {
return "", err
}

fmt.Println("Creating a new device on the cloud")
devID, err := iotClient.AddDevice(dev.fqbn, params.Name, dev.serial, dev.dType)
if err != nil {
return "", err
}

prov := &provision{
Commander: rpcComm,
Client: iotClient,
dev: dev,
id: devID}
err = prov.run()
if err != nil {
// TODO: delete the device on iot cloud
err = fmt.Errorf("%s: %w", "cannot provision device", err)
return "", err
}

return devID, nil
}

// deviceFromPorts returns a board that matches all the criteria
// passed in. If no criteria are passed, it returns the first device found.
func deviceFromPorts(ports []*rpc.DetectedPort, params *CreateParams) *device {
for _, port := range ports {
if portFilter(port, params) {
continue
}
board := boardFilter(port.Boards, params)
if board != nil {
t := strings.Split(board.Fqbn, ":")[2]
dev := &device{board.Fqbn, port.SerialNumber, t, port.Address}
return dev
}
}

return nil
}

// portFilter filters out the given port if the port parameter is not an empty string
// and if they do not match.
// It returns:
// true -> to skip the port
// false -> to keep the port
func portFilter(port *rpc.DetectedPort, params *CreateParams) bool {
if len(port.Boards) == 0 {
return true
}
if params.Port != "" && params.Port != port.Address {
return true
}
return false
}

// boardFilter looks for a board which has the same fqbn passed as parameter.
// It returns:
// - a board if it is found.
// - nil if no board matching the fqbn parameter is found.
func boardFilter(boards []*rpc.BoardListItem, params *CreateParams) (board *rpc.BoardListItem) {
if params.Fqbn == "" {
return boards[0]
}
for _, b := range boards {
if b.Fqbn == params.Fqbn {
return b
}
}
return
}
Loading