diff --git a/README.md b/README.md index 7c28a89d..b12cd434 100644 --- a/README.md +++ b/README.md @@ -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 --device-id ` + +## 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 --file ` + +`$ iot-cloud-cli ota upload --device-id --file --deferred` diff --git a/cli/cli.go b/cli/cli.go index afb1ddf5..555bce3c 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -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" @@ -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}.") diff --git a/cli/ota/ota.go b/cli/ota/ota.go new file mode 100644 index 00000000..18e6a7c7 --- /dev/null +++ b/cli/ota/ota.go @@ -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 +} diff --git a/cli/ota/upload.go b/cli/ota/upload.go new file mode 100644 index 00000000..3805319e --- /dev/null +++ b/cli/ota/upload.go @@ -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") +} diff --git a/command/ota/generate.go b/command/ota/generate.go new file mode 100644 index 00000000..3f36fae7 --- /dev/null +++ b/command/ota/generate.go @@ -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 +} diff --git a/command/ota/upload.go b/command/ota/upload.go new file mode 100644 index 00000000..00c048c8 --- /dev/null +++ b/command/ota/upload.go @@ -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) + + 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 +} diff --git a/go.mod b/go.mod index d272dbff..b62092bf 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 ) diff --git a/go.sum b/go.sum index ad9e7e37..cd2a3411 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/internal/iot/client.go b/internal/iot/client.go index 8f52f07c..50ec7471 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -3,6 +3,7 @@ package iot import ( "context" "fmt" + "os" "github.com/antihax/optional" iotclient "github.com/arduino/iot-client-go" @@ -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 @@ -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) { diff --git a/internal/lzss/README.md b/internal/lzss/README.md new file mode 100644 index 00000000..e69de29b diff --git a/internal/lzss/lzss.c b/internal/lzss/lzss.c new file mode 100644 index 00000000..782a65e6 --- /dev/null +++ b/internal/lzss/lzss.c @@ -0,0 +1,194 @@ +/* LZSS encoder-decoder (Haruhiko Okumura; public domain) */ + +#include +#include + +#define EI 11 /* typically 10..13 */ +#define EJ 4 /* typically 4..5 */ +#define P 1 /* If match length <= P then output one character */ +#define N (1 << EI) /* buffer size */ +#define F ((1 << EJ) + 1) /* lookahead buffer size */ + +int bit_buffer = 0, bit_mask = 128; +unsigned long codecount = 0, textcount = 0; +unsigned char buffer[N * 2]; +FILE *infile, *outfile; + +void error(void) +{ + printf("Output error\n"); exit(1); +} + +void putbit1(void) +{ + bit_buffer |= bit_mask; + if ((bit_mask >>= 1) == 0) { + if (fputc(bit_buffer, outfile) == EOF) error(); + bit_buffer = 0; bit_mask = 128; codecount++; + } +} + +void putbit0(void) +{ + if ((bit_mask >>= 1) == 0) { + if (fputc(bit_buffer, outfile) == EOF) error(); + bit_buffer = 0; bit_mask = 128; codecount++; + } +} + +void flush_bit_buffer(void) +{ + if (bit_mask != 128) { + if (fputc(bit_buffer, outfile) == EOF) error(); + codecount++; + } +} + +void output1(int c) +{ + int mask; + + putbit1(); + mask = 256; + while (mask >>= 1) { + if (c & mask) putbit1(); + else putbit0(); + } +} + +void output2(int x, int y) +{ + int mask; + + putbit0(); + mask = N; + while (mask >>= 1) { + if (x & mask) putbit1(); + else putbit0(); + } + mask = (1 << EJ); + while (mask >>= 1) { + if (y & mask) putbit1(); + else putbit0(); + } +} + +void encode(void) +{ + int i, j, f1, x, y, r, s, bufferend, c; + + for (i = 0; i < N - F; i++) buffer[i] = ' '; + for (i = N - F; i < N * 2; i++) { + if ((c = fgetc(infile)) == EOF) break; + buffer[i] = c; textcount++; + } + bufferend = i; r = N - F; s = 0; + while (r < bufferend) { + f1 = (F <= bufferend - r) ? F : bufferend - r; + x = 0; y = 1; c = buffer[r]; + for (i = r - 1; i >= s; i--) + if (buffer[i] == c) { + for (j = 1; j < f1; j++) + if (buffer[i + j] != buffer[r + j]) break; + if (j > y) { + x = i; y = j; + } + } + if (y <= P) { y = 1; output1(c); } + else output2(x & (N - 1), y - 2); + r += y; s += y; + if (r >= N * 2 - F) { + for (i = 0; i < N; i++) buffer[i] = buffer[i + N]; + bufferend -= N; r -= N; s -= N; + while (bufferend < N * 2) { + if ((c = fgetc(infile)) == EOF) break; + buffer[bufferend++] = c; textcount++; + } + } + } + flush_bit_buffer(); +// printf("text: %ld bytes\n", textcount); +// printf("code: %ld bytes (%ld%%)\n", +// codecount, (codecount * 100) / textcount); +} + +int getbit(int n) /* get n bits */ +{ + int i, x; + static int buf, mask = 0; + + x = 0; + for (i = 0; i < n; i++) { + if (mask == 0) { + if ((buf = fgetc(infile)) == EOF) return EOF; + mask = 128; + } + x <<= 1; + if (buf & mask) x++; + mask >>= 1; + } + return x; +} + +void decode(void) +{ + int i, j, k, r, c; + + for (i = 0; i < N - F; i++) buffer[i] = ' '; + r = N - F; + while ((c = getbit(1)) != EOF) { + if (c) { + if ((c = getbit(8)) == EOF) break; + fputc(c, outfile); + buffer[r++] = c; r &= (N - 1); + } else { + if ((i = getbit(EI)) == EOF) break; + if ((j = getbit(EJ)) == EOF) break; + for (k = 0; k <= j + 1; k++) { + c = buffer[(i + k) & (N - 1)]; + fputc(c, outfile); + buffer[r++] = c; r &= (N - 1); + } + } + } +} + +int encode_file(char const * in, char const * out) +{ + // reset counters + bit_buffer = 0, bit_mask = 128; + codecount = 0, textcount = 0; + + infile = fopen(in, "rb"); + if (infile == NULL) return 0; + + outfile = fopen(out, "wb"); + if (outfile == NULL) return 0; + + encode(); + + fclose(infile); + fclose(outfile); + + return 0; +} + +int decode_file(char const * in, char const * out) +{ + // reset counters + bit_buffer = 0, bit_mask = 128; + codecount = 0, textcount = 0; + + infile = fopen(in, "rb"); + if (infile == NULL) return 0; + + outfile = fopen(out, "wb"); + if (outfile == NULL) return 0; + + decode(); + + fclose(infile); + fclose(outfile); + + return 0; +} diff --git a/internal/lzss/lzss.go b/internal/lzss/lzss.go new file mode 100644 index 00000000..51ea3c2d --- /dev/null +++ b/internal/lzss/lzss.go @@ -0,0 +1,26 @@ +package lzss + +// #cgo CFLAGS: -g -Wall +// #include +// #include "lzss.h" +import "C" +import ( + // "fmt" + "sync" + "unsafe" +) + +func Encode(source, destination string) { + + var mutex sync.Mutex + + src := C.CString(source) + defer C.free(unsafe.Pointer(src)) + + dst := C.CString(destination) + defer C.free(unsafe.Pointer(dst)) + + mutex.Lock() + C.encode_file(src, dst) + mutex.Unlock() +} diff --git a/internal/lzss/lzss.h b/internal/lzss/lzss.h new file mode 100644 index 00000000..a4a44622 --- /dev/null +++ b/internal/lzss/lzss.h @@ -0,0 +1,6 @@ +#ifndef _LZSS_H +#define _LZSS_H + +int encode_file(char const * in, char const * out); + +#endif \ No newline at end of file diff --git a/internal/lzss/testdata/lorem.lzss b/internal/lzss/testdata/lorem.lzss new file mode 100644 index 00000000..07d0070c Binary files /dev/null and b/internal/lzss/testdata/lorem.lzss differ diff --git a/internal/lzss/testdata/lorem.txt b/internal/lzss/testdata/lorem.txt new file mode 100644 index 00000000..c942efc7 --- /dev/null +++ b/internal/lzss/testdata/lorem.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ligula dui, imperdiet ut vulputate semper, sollicitudin ut eros. Aliquam erat volutpat. In hac habitasse platea dictumst. Nam non tortor sit amet mauris rutrum eleifend. Pellentesque vel justo nibh. Vivamus sem risus, pharetra eget egestas eget, venenatis a ipsum. Cras ultrices aliquam sagittis. Donec lacinia urna ac orci congue ut adipiscing dolor fringilla. Nullam nibh magna, bibendum vulputate ornare id, hendrerit et metus. Nullam dapibus neque quis mi laoreet molestie. Mauris et dui lacus, sit amet egestas purus. + +Donec accumsan elementum accumsan. Nullam gravida dictum diam non semper. Curabitur vel magna in velit accumsan pulvinar eget in lorem. Duis vitae ante velit, at hendrerit nibh. Pellentesque lacus urna, cursus ac semper sagittis, viverra at sem. Quisque ullamcorper odio dolor. In quis pretium lacus. Maecenas lacinia urna id massa congue blandit. Suspendisse dapibus eros sit amet neque fermentum imperdiet. Cras interdum pulvinar eleifend. Suspendisse molestie neque a risus imperdiet convallis. In interdum dignissim pharetra. Morbi lectus tortor, pulvinar quis eleifend in, placerat at risus. Sed aliquam diam at metus adipiscing blandit. + +Integer tristique metus vel ipsum pulvinar dignissim quis vel quam. Donec auctor aliquet bibendum. Morbi aliquet malesuada ultrices. Vivamus ac leo odio. Nam tristique eros non arcu porttitor non volutpat mauris tempus. Proin vestibulum suscipit pretium. Etiam elit tortor, dictum a gravida porta, congue id dolor. Duis eget est vitae elit facilisis blandit. Proin tincidunt felis et ipsum pharetra tempor. Fusce imperdiet vulputate magna, vel lacinia neque volutpat a. Vivamus a elit dolor. Aliquam sollicitudin dui et leo elementum mattis. Quisque suscipit, lorem id eleifend imperdiet, ipsum lorem pharetra purus, vel tempus lectus ligula id tortor. Morbi eget eros vel sapien scelerisque aliquam pellentesque sed turpis. Duis vel lorem non eros semper fringilla vitae vitae erat. + +Vivamus porttitor pulvinar tristique. Proin sed elit ipsum. Phasellus faucibus pulvinar dapibus. Praesent quis sem in purus ultrices imperdiet. Aenean ut nulla urna. In tristique tincidunt urna, nec adipiscing velit laoreet ut. Curabitur et ante sed libero tristique pellentesque. Quisque porttitor sodales ipsum ut rhoncus. Nunc vitae diam gravida orci aliquam cursus vitae ut sapien. Proin ullamcorper felis eu nulla dapibus nec faucibus odio hendrerit. Aenean lorem magna, fermentum in tristique sit amet, accumsan ut massa. Fusce tristique, lectus rhoncus commodo sagittis, ligula felis consequat arcu, id pretium enim dolor id mi. Donec facilisis pulvinar luctus. Pellentesque vitae condimentum risus. Nam quis elit a orci adipiscing bibendum. + +Ut quis felis lorem, dignissim varius turpis. Sed convallis dui semper mauris fermentum porta. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam erat volutpat. Proin lorem felis, scelerisque nec commodo non, porta eu metus. Nulla id augue a turpis mollis pellentesque. Aenean in lectus et leo tincidunt auctor eu interdum est. Nulla varius, lorem congue laoreet laoreet, felis quam ullamcorper ligula, non pellentesque dui ipsum quis odio. Etiam sit amet blandit leo. Aenean venenatis molestie eros, in fermentum ipsum dictum eget. Donec ultricies feugiat nisl, non molestie mi congue quis. Quisque mattis augue nec neque fringilla varius. Proin sollicitudin risus et elit pretium congue. Sed consequat eros sit amet felis pulvinar pulvinar. Morbi in turpis eu nulla cursus venenatis at ut urna. Donec vel lectus quis nisi aliquam varius. \ No newline at end of file diff --git a/internal/ota/encoder.go b/internal/ota/encoder.go new file mode 100644 index 00000000..b91c33df --- /dev/null +++ b/internal/ota/encoder.go @@ -0,0 +1,181 @@ +package ota + +import ( + "bufio" + "encoding/binary" + "hash/crc32" + "io" + "io/ioutil" + "log" + "os" + "strconv" + + "github.com/arduino/iot-cloud-cli/internal/lzss" + "github.com/juju/errors" +) + +// A writer is a buffered, flushable writer. +type writer interface { + io.Writer + Flush() error +} + +// encoder encodes a binary into an .ota file. +type encoder struct { + // w is the writer that compressed bytes are written to. + w writer + + // vendorID is the ID of the board vendor + vendorID string + + // is the ID of the board vendor is the ID of the board model + productID string +} + +// NewWriter creates a new `WriteCloser` for the the given VID/PID. +func NewWriter(w io.Writer, vendorID, productID string) io.WriteCloser { + bw, ok := w.(writer) + if !ok { + bw = bufio.NewWriter(w) + } + return &encoder{ + w: bw, + vendorID: vendorID, + productID: productID, + } +} + +// Write writes a compressed representation of p to e's underlying writer. +func (e *encoder) Write(binaryData []byte) (int, error) { + //log.Println("original binaryData is", len(binaryData), "bytes length") + + // Magic number (VID/PID) + magicNumber := make([]byte, 4) + vid, err := strconv.ParseUint(e.vendorID, 16, 16) + if err != nil { + return 0, errors.Annotate(err, "OTA encoder: failed to parse vendorID") + } + pid, err := strconv.ParseUint(e.productID, 16, 16) + if err != nil { + return 0, errors.Annotate(err, "OTA encoder: failed to parse productID") + } + + binary.LittleEndian.PutUint16(magicNumber[0:2], uint16(pid)) + binary.LittleEndian.PutUint16(magicNumber[2:4], uint16(vid)) + + // Version field (byte array of size 8) + version := Version{ + Compression: true, + } + + // Compress the compiled binary + compressed, err := e.compress(&binaryData) + if err != nil { + return 0, err + } + + // Prepend magic number and version field to payload + var binDataComplete []byte + binDataComplete = append(binDataComplete, magicNumber...) + binDataComplete = append(binDataComplete, version.AsBytes()...) + binDataComplete = append(binDataComplete, compressed...) + //log.Println("binDataComplete is", len(binDataComplete), "bytes length") + + headerSize, err := e.writeHeader(&binDataComplete) + if err != nil { + return headerSize, err + } + + payloadSize, err := e.writePayload(&binDataComplete) + if err != nil { + return payloadSize, err + } + + return headerSize + payloadSize, nil +} + +// Close closes the encoder, flushing any pending output. It does not close or +// flush e's underlying writer. +func (e *encoder) Close() error { + return e.w.Flush() +} + +func (e *encoder) writeHeader(binDataComplete *[]byte) (int, error) { + + // + // Write the length of the content + // + lengthAsBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(lengthAsBytes, uint32(len(*binDataComplete))) + + n, err := e.w.Write(lengthAsBytes) + if err != nil { + return n, err + } + + // + // Calculate the checksum for binDataComplete + // + crc := crc32.ChecksumIEEE(*binDataComplete) + + // encode the checksum uint32 value as 4 bytes + crcAsBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(crcAsBytes, crc) + + n, err = e.w.Write(crcAsBytes) + if err != nil { + return n, err + } + + return len(lengthAsBytes) + len(crcAsBytes), nil +} + +func (e *encoder) writePayload(data *[]byte) (int, error) { + + // write the payload + payloadSize, err := e.w.Write(*data) + if err != nil { + return payloadSize, err + } + + return payloadSize, nil +} + +func (e *encoder) compress(data *[]byte) ([]byte, error) { + + // create a tmp file for input + inputFile, err := ioutil.TempFile("", "ota-lzss-input") + if err != nil { + log.Fatal(err) + return nil, err + } + defer os.Remove(inputFile.Name()) + + // create a tmp file for output + outputFile, err := ioutil.TempFile("", "ota-lzss-output") + if err != nil { + log.Fatal(err) + return nil, err + } + defer os.Remove(outputFile.Name()) + + // write data in the input file + ioutil.WriteFile(inputFile.Name(), *data, 644) + if err != nil { + log.Fatal(err) + return nil, err + } + + // Compress the binary data using LZSS + lzss.Encode(inputFile.Name(), outputFile.Name()) + + // reads compressed data from output file and write it into + // the writer + compressed, err := ioutil.ReadFile(outputFile.Name()) + if err != nil { + log.Fatal(err) + return nil, err + } + + return compressed, nil +} diff --git a/internal/ota/encoder_test.go b/internal/ota/encoder_test.go new file mode 100644 index 00000000..a165c9ad --- /dev/null +++ b/internal/ota/encoder_test.go @@ -0,0 +1,57 @@ +package ota + +import ( + "bytes" + "encoding/hex" + "log" + + "fmt" + "hash/crc32" + "testing" + + "gotest.tools/assert" +) + +func TestComputeCrc32Checksum(t *testing.T) { + + data, _ := hex.DecodeString("DEADBEEF") + crc := crc32.ChecksumIEEE(data) + + assert.Equal(t, crc, uint32(2090640218)) +} + +func TestEncoderWrite(t *testing.T) { + + // Setup test data + data, _ := hex.DecodeString("DEADBEEF") // uncompressed, or 'ef 6b 77 de f0' (compressed w/ LZSS) + + var w bytes.Buffer + vendorID := "2341" // Arduino + productID := "8054" // MRK Wifi 1010 + + otaWriter := NewWriter(&w, vendorID, productID) + defer otaWriter.Close() + + n, err := otaWriter.Write(data) + if err != nil { + t.Error(err) + t.Fail() + } + log.Println("written ota of", n, "bytes length") + + otaWriter.Close() + actual := w.Bytes() + + // You can get the expected result creating an `.ota` file using Alex's tools: + // https://github.com/arduino-libraries/ArduinoIoTCloud/tree/master/extras/tools + expected, _ := hex.DecodeString("11000000a1744bd4548041230000000000000040ef6b77def0") + + res := bytes.Compare(expected, actual) + + if res != 0 { + fmt.Println("expected:", hex.Dump(expected), len(expected), "bytes") + fmt.Println("actual:", hex.Dump(actual), len(actual), "bytes") + } + + assert.Assert(t, res == 0) // 0 means equal +} diff --git a/internal/ota/testdata/lorem.lzss b/internal/ota/testdata/lorem.lzss new file mode 100644 index 00000000..07d0070c Binary files /dev/null and b/internal/ota/testdata/lorem.lzss differ diff --git a/internal/ota/testdata/lorem.txt b/internal/ota/testdata/lorem.txt new file mode 100644 index 00000000..c942efc7 --- /dev/null +++ b/internal/ota/testdata/lorem.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ligula dui, imperdiet ut vulputate semper, sollicitudin ut eros. Aliquam erat volutpat. In hac habitasse platea dictumst. Nam non tortor sit amet mauris rutrum eleifend. Pellentesque vel justo nibh. Vivamus sem risus, pharetra eget egestas eget, venenatis a ipsum. Cras ultrices aliquam sagittis. Donec lacinia urna ac orci congue ut adipiscing dolor fringilla. Nullam nibh magna, bibendum vulputate ornare id, hendrerit et metus. Nullam dapibus neque quis mi laoreet molestie. Mauris et dui lacus, sit amet egestas purus. + +Donec accumsan elementum accumsan. Nullam gravida dictum diam non semper. Curabitur vel magna in velit accumsan pulvinar eget in lorem. Duis vitae ante velit, at hendrerit nibh. Pellentesque lacus urna, cursus ac semper sagittis, viverra at sem. Quisque ullamcorper odio dolor. In quis pretium lacus. Maecenas lacinia urna id massa congue blandit. Suspendisse dapibus eros sit amet neque fermentum imperdiet. Cras interdum pulvinar eleifend. Suspendisse molestie neque a risus imperdiet convallis. In interdum dignissim pharetra. Morbi lectus tortor, pulvinar quis eleifend in, placerat at risus. Sed aliquam diam at metus adipiscing blandit. + +Integer tristique metus vel ipsum pulvinar dignissim quis vel quam. Donec auctor aliquet bibendum. Morbi aliquet malesuada ultrices. Vivamus ac leo odio. Nam tristique eros non arcu porttitor non volutpat mauris tempus. Proin vestibulum suscipit pretium. Etiam elit tortor, dictum a gravida porta, congue id dolor. Duis eget est vitae elit facilisis blandit. Proin tincidunt felis et ipsum pharetra tempor. Fusce imperdiet vulputate magna, vel lacinia neque volutpat a. Vivamus a elit dolor. Aliquam sollicitudin dui et leo elementum mattis. Quisque suscipit, lorem id eleifend imperdiet, ipsum lorem pharetra purus, vel tempus lectus ligula id tortor. Morbi eget eros vel sapien scelerisque aliquam pellentesque sed turpis. Duis vel lorem non eros semper fringilla vitae vitae erat. + +Vivamus porttitor pulvinar tristique. Proin sed elit ipsum. Phasellus faucibus pulvinar dapibus. Praesent quis sem in purus ultrices imperdiet. Aenean ut nulla urna. In tristique tincidunt urna, nec adipiscing velit laoreet ut. Curabitur et ante sed libero tristique pellentesque. Quisque porttitor sodales ipsum ut rhoncus. Nunc vitae diam gravida orci aliquam cursus vitae ut sapien. Proin ullamcorper felis eu nulla dapibus nec faucibus odio hendrerit. Aenean lorem magna, fermentum in tristique sit amet, accumsan ut massa. Fusce tristique, lectus rhoncus commodo sagittis, ligula felis consequat arcu, id pretium enim dolor id mi. Donec facilisis pulvinar luctus. Pellentesque vitae condimentum risus. Nam quis elit a orci adipiscing bibendum. + +Ut quis felis lorem, dignissim varius turpis. Sed convallis dui semper mauris fermentum porta. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam erat volutpat. Proin lorem felis, scelerisque nec commodo non, porta eu metus. Nulla id augue a turpis mollis pellentesque. Aenean in lectus et leo tincidunt auctor eu interdum est. Nulla varius, lorem congue laoreet laoreet, felis quam ullamcorper ligula, non pellentesque dui ipsum quis odio. Etiam sit amet blandit leo. Aenean venenatis molestie eros, in fermentum ipsum dictum eget. Donec ultricies feugiat nisl, non molestie mi congue quis. Quisque mattis augue nec neque fringilla varius. Proin sollicitudin risus et elit pretium congue. Sed consequat eros sit amet felis pulvinar pulvinar. Morbi in turpis eu nulla cursus venenatis at ut urna. Donec vel lectus quis nisi aliquam varius. \ No newline at end of file diff --git a/internal/ota/version.go b/internal/ota/version.go new file mode 100644 index 00000000..8fe68007 --- /dev/null +++ b/internal/ota/version.go @@ -0,0 +1,30 @@ +package ota + +// Version contains all the OTA header information +// Check out https://arduino.atlassian.net/wiki/spaces/RFC/pages/1616871540/OTA+header+structure for more +// information on the OTA header specs. +type Version struct { + HeaderVersion uint8 + Compression bool + Signature bool + Spare uint8 + PayloadTarget uint8 + PayloadMayor uint8 + PayloadMinor uint8 + PayloadPatch uint8 + PayloadBuildNum uint32 +} + +// AsBytes builds a 8 byte length representation of the Version Struct for the OTA update. +func (v *Version) AsBytes() []byte { + version := []byte{0, 0, 0, 0, 0, 0, 0, 0} + + // Set compression + if v.Compression { + version[7] = 0x40 + } + + // Other field are currently not implemented ¯\_(ツ)_/¯ + + return version +} diff --git a/internal/ota/version_test.go b/internal/ota/version_test.go new file mode 100644 index 00000000..5ef6858b --- /dev/null +++ b/internal/ota/version_test.go @@ -0,0 +1,34 @@ +package ota + +import ( + "bytes" + "fmt" + "os" + "testing" + "text/tabwriter" + + "gotest.tools/assert" +) + +func TestVersionWithCompressionEnabled(t *testing.T) { + + version := Version{ + Compression: true, + } + + expected := []byte{0, 0, 0, 0, 0, 0, 0, 0x40} + actual := version.AsBytes() + + // create a tabwriter for formatting the output + w := new(tabwriter.Writer) + + // Format in tab-separated columns with a tab stop of 8. + w.Init(os.Stdout, 0, 8, 0, '\t', 0) + + fmt.Fprintf(w, "Binary:\t%0.8bb (expected)\n", expected) + fmt.Fprintf(w, "Binary:\t%0.8bb (actual)\n", actual) + w.Flush() + + res := bytes.Compare(expected, actual) + assert.Assert(t, res == 0) // 0 means equal +}