Skip to content

Add ota upload command #31

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 9 commits into from
Sep 13, 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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,12 @@ Extract a template from an existing thing. The template can be saved in two form
Bind a thing to an existing device:

`$ iot-cloud-cli thing bind --id <thingID> --device-id <deviceID>`

## Ota commands

Perform an OTA firmware update. Note that the binary file (`.bin`) should be compiled using an arduino core that supports the specified device.
The default OTA upload should complete in 10 minutes. Use `--deferred` flag to extend this time to one week.

`$ iot-cloud-cli ota upload --device-id <deviceID> --file <sketch-file.ino.bin>`

`$ iot-cloud-cli ota upload --device-id <deviceID> --file <sketch-file.ino.bin> --deferred`
2 changes: 2 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/iot-cloud-cli/cli/config"
"github.com/arduino/iot-cloud-cli/cli/device"
"github.com/arduino/iot-cloud-cli/cli/ota"
"github.com/arduino/iot-cloud-cli/cli/thing"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand All @@ -31,6 +32,7 @@ func Execute() {
cli.AddCommand(config.NewCommand())
cli.AddCommand(device.NewCommand())
cli.AddCommand(thing.NewCommand())
cli.AddCommand(ota.NewCommand())

cli.PersistentFlags().BoolVarP(&cliFlags.verbose, "verbose", "v", false, "Print the logs on the standard output.")
cli.PersistentFlags().StringVar(&cliFlags.outputFormat, "format", "text", "The output format, can be {text|json}.")
Expand Down
17 changes: 17 additions & 0 deletions cli/ota/ota.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ota

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

func NewCommand() *cobra.Command {
otaCommand := &cobra.Command{
Use: "ota",
Short: "Over The Air.",
Long: "Over The Air firmware update.",
}

otaCommand.AddCommand(initUploadCommand())

return otaCommand
}
50 changes: 50 additions & 0 deletions cli/ota/upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ota

import (
"os"

"github.com/arduino/arduino-cli/cli/errorcodes"
"github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/iot-cloud-cli/command/ota"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var uploadFlags struct {
deviceID string
file string
deferred bool
}

func initUploadCommand() *cobra.Command {
uploadCommand := &cobra.Command{
Use: "upload",
Short: "OTA upload",
Long: "OTA upload on a device of Arduino IoT Cloud",
Run: runUploadCommand,
}

uploadCommand.Flags().StringVarP(&uploadFlags.deviceID, "device-id", "d", "", "Device ID")
uploadCommand.Flags().StringVarP(&uploadFlags.file, "file", "", "", "Binary file (.bin) to be uploaded")
uploadCommand.Flags().BoolVar(&uploadFlags.deferred, "deferred", false, "Perform a deferred OTA. It can take up to 1 week.")

uploadCommand.MarkFlagRequired("device-id")
uploadCommand.MarkFlagRequired("file")
return uploadCommand
}

func runUploadCommand(cmd *cobra.Command, args []string) {
logrus.Infof("Uploading binary %s to device %s", uploadFlags.file, uploadFlags.deviceID)

params := &ota.UploadParams{
DeviceID: uploadFlags.deviceID,
File: uploadFlags.file,
}
err := ota.Upload(params)
if err != nil {
feedback.Errorf("Error during ota upload: %v", err)
os.Exit(errorcodes.ErrGeneric)
}

logrus.Info("Upload successfully started")
}
51 changes: 51 additions & 0 deletions command/ota/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package ota

import (
"bytes"
"errors"
"io/ioutil"
"os"

inota "github.com/arduino/iot-cloud-cli/internal/ota"
)

var (
arduinoVendorID = "2341"
fqbnToPID = map[string]string{
"arduino:samd:nano_33_iot": "8057",
"arduino:samd:mkr1000": "804E",
"arduino:samd:mkrgsm1400": "8052",
"arduino:samd:mkrnb1500": "8055",
"arduino:samd:mkrwifi1010": "8054",
"arduino:mbed_nano:nanorp2040connect": "005E",
"arduino:mbed_portenta:envie_m7": "025B",
}
)

// Generate takes a .bin file and generates a .ota file.
func Generate(binFile string, outFile string, fqbn string) error {
productID, ok := fqbnToPID[fqbn]
if !ok {
return errors.New("fqbn not valid")
}

data, err := ioutil.ReadFile(binFile)
if err != nil {
return err
}

var w bytes.Buffer
otaWriter := inota.NewWriter(&w, arduinoVendorID, productID)
_, err = otaWriter.Write(data)
if err != nil {
return err
}
otaWriter.Close()

err = ioutil.WriteFile(outFile, w.Bytes(), os.FileMode(0644))
if err != nil {
return err
}

return nil
}
73 changes: 73 additions & 0 deletions command/ota/upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ota

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"

"github.com/arduino/iot-cloud-cli/internal/config"
"github.com/arduino/iot-cloud-cli/internal/iot"
)

const (
// default ota should complete in 10 mins
otaExpirationMins = 10
// deferred ota can take up to 1 week (equal to 10080 minutes)
otaDeferredExpirationMins = 10080
)

// UploadParams contains the parameters needed to
// perform an OTA upload.
type UploadParams struct {
DeviceID string
File string
Deferred bool
}

// Upload command is used to upload a firmware OTA,
// on a device of Arduino IoT Cloud.
func Upload(params *UploadParams) error {
conf, err := config.Retrieve()
if err != nil {
return err
}
iotClient, err := iot.NewClient(conf.Client, conf.Secret)
if err != nil {
return err
}

dev, err := iotClient.DeviceShow(params.DeviceID)
if err != nil {
return err
}

otaDir, err := ioutil.TempDir("", "")
if err != nil {
return fmt.Errorf("%s: %w", "cannot create temporary folder", err)
}
otaFile := filepath.Join(otaDir, "temp.ota")
defer os.RemoveAll(otaDir)
Copy link
Contributor

@glumia glumia Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we generate the file directly in the generic temporary directory? Also, I think it could be useful to not delete the binary in case we (or some sophisticated user) want to look at it for debugging purposes.

If I'm not wrong it should be regularly deleted by the OS anyway (I know this happens at least on Ubuntu). Or we could give the possibility to decide this behaviour with a --debug flag.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we generate the file directly in the generic temporary directory?

What do you mean? It is generated in the temp directory

I think it could be useful to not delete the binary in case we (or some sophisticated user) want to look at it for debugging purposes.

here I see two reasons for not doing this:

  • it's simpler if the user doesn't know at all that a .ota exists, so leaving a compressed binary would be also dangerous
  • if we want to make the user capable of generating a .ota file, we should add a specific ota generate command
  • allowing the user to manage .ota files is risky. Each ota file is valid only for a specific board, if a user tries to upload it to a different board then the firmware will not be applied and the user is not notified of the causes.
  • .ota files are compressed .bin files with the addition of an header, for debug purposes the .bin file should be analyzed

in conclusion I don't see any benefits in letting the user see or know about a .ota file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? It is generated in the temp directory

I mean replacing lines 37-41 with:

	otaFile, err := ioutil.TempFile("", "temp.ota")
	if err != nil {
		return fmt.Errorf("%s: %w", "cannot create temporary file for ota binary", err)
	}
	defer os.Remove(otaFile.Name())

So that we don't create a temporary directory but just a file.

here I see two reasons ...

Ok, you convinced me 😛 (we or the sophisticated user can always edit the source code to accomplish it)


err = Generate(params.File, otaFile, dev.Fqbn)
if err != nil {
return fmt.Errorf("%s: %w", "cannot generate .ota file", err)
}

file, err := os.Open(otaFile)
if err != nil {
return fmt.Errorf("%s: %w", "cannot open ota file", err)
}

expiration := otaExpirationMins
if params.Deferred {
expiration = otaDeferredExpirationMins
}

err = iotClient.DeviceOTA(params.DeviceID, file, expiration)
if err != nil {
return err
}

return nil
}
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ go 1.15
require (
github.com/antihax/optional v1.0.0
github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3
github.com/arduino/go-paths-helper v1.6.0
github.com/arduino/go-paths-helper v1.6.1
github.com/arduino/iot-client-go v1.3.4-0.20210902151346-1cd63fb0c784
github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.7.0
Expand All @@ -21,4 +22,5 @@ require (
google.golang.org/grpc v1.39.0
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
gotest.tools v2.2.0+incompatible
)
5 changes: 4 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c h1:agh2JT9
github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c/go.mod h1:HK7SpkEax/3P+0w78iRQx1sz1vCDYYw9RXwHjQTB5i8=
github.com/arduino/go-paths-helper v1.0.1/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck=
github.com/arduino/go-paths-helper v1.2.0/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck=
github.com/arduino/go-paths-helper v1.6.0 h1:S7/d7DqB9XlnvF9KrgSiGmo2oWKmYW6O/DTjj3Bijx4=
github.com/arduino/go-paths-helper v1.6.0/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
github.com/arduino/go-paths-helper v1.6.1 h1:lha+/BuuBsx0qTZ3gy6IO1kU23lObWdQ/UItkzVWQ+0=
github.com/arduino/go-paths-helper v1.6.1/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
github.com/arduino/go-properties-orderedmap v1.3.0 h1:4No/vQopB36e7WUIk6H6TxiSEJPiMrVOCZylYmua39o=
github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4lSFMEYCVubfeArCsGPGDwb2yk=
github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b/go.mod h1:uwGy5PpN4lqW97FiLnbcx+xx8jly5YuPMJWfVwwjJiQ=
Expand Down Expand Up @@ -739,6 +740,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
16 changes: 16 additions & 0 deletions internal/iot/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package iot
import (
"context"
"fmt"
"os"

"github.com/antihax/optional"
iotclient "github.com/arduino/iot-client-go"
Expand All @@ -14,6 +15,7 @@ type Client interface {
DeviceDelete(id string) error
DeviceList() ([]iotclient.ArduinoDevicev2, error)
DeviceShow(id string) (*iotclient.ArduinoDevicev2, error)
DeviceOTA(id string, file *os.File, expireMins int) error
CertificateCreate(id, csr string) (*iotclient.ArduinoCompressedv2, error)
ThingCreate(thing *iotclient.Thing, force bool) (*iotclient.ArduinoThing, error)
ThingUpdate(id string, thing *iotclient.Thing, force bool) error
Expand Down Expand Up @@ -89,6 +91,20 @@ func (cl *client) DeviceShow(id string) (*iotclient.ArduinoDevicev2, error) {
return &dev, nil
}

// DeviceOTA performs an OTA upload request to Arduino IoT Cloud, passing
// the ID of the device to be updated and the actual file containing the OTA firmware.
func (cl *client) DeviceOTA(id string, file *os.File, expireMins int) error {
opt := &iotclient.DevicesV2OtaUploadOpts{
ExpireInMins: optional.NewInt32(int32(expireMins)),
}
_, err := cl.api.DevicesV2OtaApi.DevicesV2OtaUpload(cl.ctx, id, file, opt)
if err != nil {
err = fmt.Errorf("uploading device ota: %w", errorDetail(err))
return err
}
return nil
}

// CertificateCreate allows to upload a certificate on Arduino IoT Cloud.
// It returns the certificate parameters populated by the cloud.
func (cl *client) CertificateCreate(id, csr string) (*iotclient.ArduinoCompressedv2, error) {
Expand Down
Empty file added internal/lzss/README.md
Empty file.
Loading