Skip to content

Commit 9a1cf13

Browse files
committed
Add ota upload command (#31)
The ota flow is the following: the device to be updated should be already provisioned and online. The user should have a sketch locally. This sketch must be compiled through arduino-cli or arduino-ide1/2. Then, this command should be used in order to start the upload: $./iot-cloud-cli ota upload --device-id <deviceID> --file <sketchName.ino.bin> * Import ota generator * Implement ota generation * Add ota upload command * Adapt to format-output changes * Fix typos * Improve ota upload help * Add deferred option * Improve deferred ota * Update readme
1 parent dc78e19 commit 9a1cf13

21 files changed

+772
-5
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,12 @@ Extract a template from an existing thing. The template can be saved in two form
7979
Bind a thing to an existing device:
8080

8181
`$ iot-cloud-cli thing bind --id <thingID> --device-id <deviceID>`
82+
83+
## Ota commands
84+
85+
Perform an OTA firmware update. Note that the binary file (`.bin`) should be compiled using an arduino core that supports the specified device.
86+
The default OTA upload should complete in 10 minutes. Use `--deferred` flag to extend this time to one week.
87+
88+
`$ iot-cloud-cli ota upload --device-id <deviceID> --file <sketch-file.ino.bin>`
89+
90+
`$ iot-cloud-cli ota upload --device-id <deviceID> --file <sketch-file.ino.bin> --deferred`

cli/cli.go

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/arduino/arduino-cli/cli/feedback"
1111
"github.com/arduino/iot-cloud-cli/cli/config"
1212
"github.com/arduino/iot-cloud-cli/cli/device"
13+
"github.com/arduino/iot-cloud-cli/cli/ota"
1314
"github.com/arduino/iot-cloud-cli/cli/thing"
1415
"github.com/sirupsen/logrus"
1516
"github.com/spf13/cobra"
@@ -31,6 +32,7 @@ func Execute() {
3132
cli.AddCommand(config.NewCommand())
3233
cli.AddCommand(device.NewCommand())
3334
cli.AddCommand(thing.NewCommand())
35+
cli.AddCommand(ota.NewCommand())
3436

3537
cli.PersistentFlags().BoolVarP(&cliFlags.verbose, "verbose", "v", false, "Print the logs on the standard output.")
3638
cli.PersistentFlags().StringVar(&cliFlags.outputFormat, "format", "text", "The output format, can be {text|json}.")

cli/ota/ota.go

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

cli/ota/upload.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package ota
2+
3+
import (
4+
"os"
5+
6+
"github.com/arduino/arduino-cli/cli/errorcodes"
7+
"github.com/arduino/arduino-cli/cli/feedback"
8+
"github.com/arduino/iot-cloud-cli/command/ota"
9+
"github.com/sirupsen/logrus"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var uploadFlags struct {
14+
deviceID string
15+
file string
16+
deferred bool
17+
}
18+
19+
func initUploadCommand() *cobra.Command {
20+
uploadCommand := &cobra.Command{
21+
Use: "upload",
22+
Short: "OTA upload",
23+
Long: "OTA upload on a device of Arduino IoT Cloud",
24+
Run: runUploadCommand,
25+
}
26+
27+
uploadCommand.Flags().StringVarP(&uploadFlags.deviceID, "device-id", "d", "", "Device ID")
28+
uploadCommand.Flags().StringVarP(&uploadFlags.file, "file", "", "", "Binary file (.bin) to be uploaded")
29+
uploadCommand.Flags().BoolVar(&uploadFlags.deferred, "deferred", false, "Perform a deferred OTA. It can take up to 1 week.")
30+
31+
uploadCommand.MarkFlagRequired("device-id")
32+
uploadCommand.MarkFlagRequired("file")
33+
return uploadCommand
34+
}
35+
36+
func runUploadCommand(cmd *cobra.Command, args []string) {
37+
logrus.Infof("Uploading binary %s to device %s", uploadFlags.file, uploadFlags.deviceID)
38+
39+
params := &ota.UploadParams{
40+
DeviceID: uploadFlags.deviceID,
41+
File: uploadFlags.file,
42+
}
43+
err := ota.Upload(params)
44+
if err != nil {
45+
feedback.Errorf("Error during ota upload: %v", err)
46+
os.Exit(errorcodes.ErrGeneric)
47+
}
48+
49+
logrus.Info("Upload successfully started")
50+
}

command/ota/generate.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package ota
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io/ioutil"
7+
"os"
8+
9+
inota "github.com/arduino/iot-cloud-cli/internal/ota"
10+
)
11+
12+
var (
13+
arduinoVendorID = "2341"
14+
fqbnToPID = map[string]string{
15+
"arduino:samd:nano_33_iot": "8057",
16+
"arduino:samd:mkr1000": "804E",
17+
"arduino:samd:mkrgsm1400": "8052",
18+
"arduino:samd:mkrnb1500": "8055",
19+
"arduino:samd:mkrwifi1010": "8054",
20+
"arduino:mbed_nano:nanorp2040connect": "005E",
21+
"arduino:mbed_portenta:envie_m7": "025B",
22+
}
23+
)
24+
25+
// Generate takes a .bin file and generates a .ota file.
26+
func Generate(binFile string, outFile string, fqbn string) error {
27+
productID, ok := fqbnToPID[fqbn]
28+
if !ok {
29+
return errors.New("fqbn not valid")
30+
}
31+
32+
data, err := ioutil.ReadFile(binFile)
33+
if err != nil {
34+
return err
35+
}
36+
37+
var w bytes.Buffer
38+
otaWriter := inota.NewWriter(&w, arduinoVendorID, productID)
39+
_, err = otaWriter.Write(data)
40+
if err != nil {
41+
return err
42+
}
43+
otaWriter.Close()
44+
45+
err = ioutil.WriteFile(outFile, w.Bytes(), os.FileMode(0644))
46+
if err != nil {
47+
return err
48+
}
49+
50+
return nil
51+
}

command/ota/upload.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package ota
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/arduino/iot-cloud-cli/internal/config"
10+
"github.com/arduino/iot-cloud-cli/internal/iot"
11+
)
12+
13+
const (
14+
// default ota should complete in 10 mins
15+
otaExpirationMins = 10
16+
// deferred ota can take up to 1 week (equal to 10080 minutes)
17+
otaDeferredExpirationMins = 10080
18+
)
19+
20+
// UploadParams contains the parameters needed to
21+
// perform an OTA upload.
22+
type UploadParams struct {
23+
DeviceID string
24+
File string
25+
Deferred bool
26+
}
27+
28+
// Upload command is used to upload a firmware OTA,
29+
// on a device of Arduino IoT Cloud.
30+
func Upload(params *UploadParams) error {
31+
conf, err := config.Retrieve()
32+
if err != nil {
33+
return err
34+
}
35+
iotClient, err := iot.NewClient(conf.Client, conf.Secret)
36+
if err != nil {
37+
return err
38+
}
39+
40+
dev, err := iotClient.DeviceShow(params.DeviceID)
41+
if err != nil {
42+
return err
43+
}
44+
45+
otaDir, err := ioutil.TempDir("", "")
46+
if err != nil {
47+
return fmt.Errorf("%s: %w", "cannot create temporary folder", err)
48+
}
49+
otaFile := filepath.Join(otaDir, "temp.ota")
50+
defer os.RemoveAll(otaDir)
51+
52+
err = Generate(params.File, otaFile, dev.Fqbn)
53+
if err != nil {
54+
return fmt.Errorf("%s: %w", "cannot generate .ota file", err)
55+
}
56+
57+
file, err := os.Open(otaFile)
58+
if err != nil {
59+
return fmt.Errorf("%s: %w", "cannot open ota file", err)
60+
}
61+
62+
expiration := otaExpirationMins
63+
if params.Deferred {
64+
expiration = otaDeferredExpirationMins
65+
}
66+
67+
err = iotClient.DeviceOTA(params.DeviceID, file, expiration)
68+
if err != nil {
69+
return err
70+
}
71+
72+
return nil
73+
}

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ go 1.15
55
require (
66
github.com/antihax/optional v1.0.0
77
github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3
8-
github.com/arduino/go-paths-helper v1.6.0
8+
github.com/arduino/go-paths-helper v1.6.1
99
github.com/arduino/iot-client-go v1.3.4-0.20210902151346-1cd63fb0c784
1010
github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6
11-
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f // indirect
11+
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5
1212
github.com/sirupsen/logrus v1.8.1
1313
github.com/spf13/cobra v1.1.3
1414
github.com/spf13/viper v1.7.0
@@ -22,4 +22,5 @@ require (
2222
google.golang.org/grpc v1.39.0
2323
google.golang.org/protobuf v1.27.1 // indirect
2424
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
25+
gotest.tools v2.2.0+incompatible
2526
)

go.sum

+5-3
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c h1:agh2JT9
5252
github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c/go.mod h1:HK7SpkEax/3P+0w78iRQx1sz1vCDYYw9RXwHjQTB5i8=
5353
github.com/arduino/go-paths-helper v1.0.1/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck=
5454
github.com/arduino/go-paths-helper v1.2.0/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck=
55-
github.com/arduino/go-paths-helper v1.6.0 h1:S7/d7DqB9XlnvF9KrgSiGmo2oWKmYW6O/DTjj3Bijx4=
5655
github.com/arduino/go-paths-helper v1.6.0/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
56+
github.com/arduino/go-paths-helper v1.6.1 h1:lha+/BuuBsx0qTZ3gy6IO1kU23lObWdQ/UItkzVWQ+0=
57+
github.com/arduino/go-paths-helper v1.6.1/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
5758
github.com/arduino/go-properties-orderedmap v1.3.0 h1:4No/vQopB36e7WUIk6H6TxiSEJPiMrVOCZylYmua39o=
5859
github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4lSFMEYCVubfeArCsGPGDwb2yk=
5960
github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b/go.mod h1:uwGy5PpN4lqW97FiLnbcx+xx8jly5YuPMJWfVwwjJiQ=
@@ -240,9 +241,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
240241
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
241242
github.com/juju/clock v0.0.0-20180524022203-d293bb356ca4/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
242243
github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
244+
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 h1:rhqTjzJlm7EbkELJDKMTU7udov+Se0xZkWmugr6zGok=
243245
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
244-
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f h1:MCOvExGLpaSIzLYB4iQXEHP4jYVU6vmzLNQPdMVrxnM=
245-
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
246246
github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
247247
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI=
248248
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
@@ -740,6 +740,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
740740
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
741741
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
742742
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
743+
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
744+
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
743745
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
744746
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
745747
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

internal/iot/client.go

+16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package iot
33
import (
44
"context"
55
"fmt"
6+
"os"
67

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

94+
// DeviceOTA performs an OTA upload request to Arduino IoT Cloud, passing
95+
// the ID of the device to be updated and the actual file containing the OTA firmware.
96+
func (cl *client) DeviceOTA(id string, file *os.File, expireMins int) error {
97+
opt := &iotclient.DevicesV2OtaUploadOpts{
98+
ExpireInMins: optional.NewInt32(int32(expireMins)),
99+
}
100+
_, err := cl.api.DevicesV2OtaApi.DevicesV2OtaUpload(cl.ctx, id, file, opt)
101+
if err != nil {
102+
err = fmt.Errorf("uploading device ota: %w", errorDetail(err))
103+
return err
104+
}
105+
return nil
106+
}
107+
92108
// CertificateCreate allows to upload a certificate on Arduino IoT Cloud.
93109
// It returns the certificate parameters populated by the cloud.
94110
func (cl *client) CertificateCreate(id, csr string) (*iotclient.ArduinoCompressedv2, error) {

internal/lzss/README.md

Whitespace-only changes.

0 commit comments

Comments
 (0)