diff --git a/README.md b/README.md index 6deeef1e..ca82bf8d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,14 @@ The list of supported LoRa frequency plans can be retrieved with: `$ arduino-cloud-cli device list-frequency-plans` +#### Generic device + +A generic device is like a virtual device that doesn't need to be attached to an actual physical board. +Any actual physical board can connect to Arduino IoT Cloud using the credentials of a generic device. +Generic devices can be created using a specific command. +An optional `--fqbn` flag can be passed to specify the fqbn of the device, otherwise it will be set to `generic:generic:generic`. + +`$ arduino-cloud-cli device create-generic --name --fqbn ` ## Device commands diff --git a/cli/device/creategeneric.go b/cli/device/creategeneric.go new file mode 100644 index 00000000..f69a2bff --- /dev/null +++ b/cli/device/creategeneric.go @@ -0,0 +1,84 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package device + +import ( + "fmt" + "os" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/device" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var createGenericFlags struct { + name string + fqbn string +} + +func initCreateGenericCommand() *cobra.Command { + createGenericCommand := &cobra.Command{ + Use: "create-generic", + Short: "Create a generic device", + Long: "Create a generic device for Arduino IoT Cloud", + Run: runCreateGenericCommand, + } + createGenericCommand.Flags().StringVarP(&createGenericFlags.name, "name", "n", "", "Device name") + createGenericCommand.Flags().StringVarP(&createGenericFlags.fqbn, "fqbn", "b", "generic:generic:generic", "Device fqbn") + createGenericCommand.MarkFlagRequired("name") + return createGenericCommand +} + +func runCreateGenericCommand(cmd *cobra.Command, args []string) { + logrus.Infof("Creating generic device with name %s", createGenericFlags.name) + + params := &device.CreateGenericParams{ + Name: createGenericFlags.name, + FQBN: createGenericFlags.fqbn, + } + + dev, err := device.CreateGeneric(params) + if err != nil { + feedback.Errorf("Error during device create-generic: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + feedback.PrintResult(createGenericResult{dev}) +} + +type createGenericResult struct { + device *device.DeviceGenericInfo +} + +func (r createGenericResult) Data() interface{} { + return r.device +} + +func (r createGenericResult) String() string { + return fmt.Sprintf( + "id: %s\nsecret-key: %s\nname: %s\nboard: %s\nserial-number: %s\nfqbn: %s", + r.device.ID, + r.device.Password, + r.device.Name, + r.device.Board, + r.device.Serial, + r.device.FQBN, + ) +} diff --git a/cli/device/device.go b/cli/device/device.go index e389e974..080a8f5a 100644 --- a/cli/device/device.go +++ b/cli/device/device.go @@ -36,6 +36,7 @@ func NewCommand() *cobra.Command { deviceCommand.AddCommand(tag.InitDeleteTagsCommand()) deviceCommand.AddCommand(initListFrequencyPlansCommand()) deviceCommand.AddCommand(initCreateLoraCommand()) + deviceCommand.AddCommand(initCreateGenericCommand()) return deviceCommand } diff --git a/command/device/creategeneric.go b/command/device/creategeneric.go new file mode 100644 index 00000000..f8418915 --- /dev/null +++ b/command/device/creategeneric.go @@ -0,0 +1,85 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package device + +import ( + "fmt" + + "github.com/arduino/arduino-cloud-cli/internal/config" + "github.com/arduino/arduino-cloud-cli/internal/iot" +) + +const ( + genericDType = "login_and_secretkey_wifi" +) + +// CreateGenericParams contains the parameters needed +// to create a new generic device. +type CreateGenericParams struct { + Name string // Device name + FQBN string // Board FQBN +} + +// DeviceGenericInfo contains the most interesting +// parameters of a generic Arduino IoT Cloud device. +type DeviceGenericInfo struct { + DeviceInfo + Password string `json:"secret-key"` +} + +// CreateGeneric command is used to add a new generic device to Arduino IoT Cloud. +func CreateGeneric(params *CreateGenericParams) (*DeviceGenericInfo, error) { + conf, err := config.Retrieve() + if err != nil { + return nil, err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return nil, err + } + + dev, err := iotClient.DeviceCreate(params.FQBN, params.Name, "", genericDType) + if err != nil { + return nil, err + } + + pass, err := iotClient.DevicePassSet(dev.Id) + if err != nil { + if errDel := iotClient.DeviceDelete(dev.Id); errDel != nil { + return nil, fmt.Errorf( + "device was successfully created on IoT-API but " + + "now we can't set its secret key nor delete it - please check " + + "it on the web application.\n\nFetch error: " + err.Error() + + "\nDeletion error: " + errDel.Error(), + ) + } + return nil, fmt.Errorf("cannot create generic device: %w", err) + } + + devInfo := &DeviceGenericInfo{ + DeviceInfo: DeviceInfo{ + Name: dev.Name, + ID: dev.Id, + Board: dev.Type, + Serial: dev.Serial, + FQBN: dev.Fqbn, + }, + Password: pass.SuggestedPassword, + } + return devInfo, nil +} diff --git a/internal/iot/client.go b/internal/iot/client.go index b5092040..78aba94e 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -34,6 +34,7 @@ type Client interface { DeviceList(tags map[string]string) ([]iotclient.ArduinoDevicev2, error) DeviceShow(id string) (*iotclient.ArduinoDevicev2, error) DeviceOTA(id string, file *os.File, expireMins int) error + DevicePassSet(id string) (*iotclient.ArduinoDevicev2Pass, error) DeviceTagsCreate(id string, tags map[string]string) error DeviceTagsDelete(id string, keys []string) error LoraFrequencyPlansList() ([]iotclient.ArduinoLorafreqplanv1, error) @@ -105,6 +106,26 @@ func (cl *client) DeviceLoraCreate(name, serial, devType, eui, freq string) (*io return &dev, nil } +// DevicePassSet sets the device password to the one suggested by Arduino IoT Cloud. +// Returns the set password. +func (cl *client) DevicePassSet(id string) (*iotclient.ArduinoDevicev2Pass, error) { + // Fetch suggested password + opts := &iotclient.DevicesV2PassGetOpts{SuggestedPassword: optional.NewBool(true)} + pass, _, err := cl.api.DevicesV2PassApi.DevicesV2PassGet(cl.ctx, id, opts) + if err != nil { + err = fmt.Errorf("fetching device suggested password: %w", errorDetail(err)) + return nil, err + } + // Set password to the suggested one + p := iotclient.Devicev2Pass{Password: pass.SuggestedPassword} + pass, _, err = cl.api.DevicesV2PassApi.DevicesV2PassSet(cl.ctx, id, p) + if err != nil { + err = fmt.Errorf("setting device password: %w", errorDetail(err)) + return nil, err + } + return &pass, nil +} + // DeviceDelete deletes the device corresponding to the passed ID // from Arduino IoT Cloud. func (cl *client) DeviceDelete(id string) error { diff --git a/internal/iot/mocks/Client.go b/internal/iot/mocks/Client.go index 07d9f769..0423159c 100644 --- a/internal/iot/mocks/Client.go +++ b/internal/iot/mocks/Client.go @@ -217,6 +217,29 @@ func (_m *Client) DeviceOTA(id string, file *os.File, expireMins int) error { return r0 } +// DevicePassSet provides a mock function with given fields: id +func (_m *Client) DevicePassSet(id string) (*iot.ArduinoDevicev2Pass, error) { + ret := _m.Called(id) + + var r0 *iot.ArduinoDevicev2Pass + if rf, ok := ret.Get(0).(func(string) *iot.ArduinoDevicev2Pass); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*iot.ArduinoDevicev2Pass) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // DeviceShow provides a mock function with given fields: id func (_m *Client) DeviceShow(id string) (*iot.ArduinoDevicev2, error) { ret := _m.Called(id)