From b0fd012c858083cb448622a535a7aba653d7496b Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Wed, 3 Apr 2024 15:08:32 +0200 Subject: [PATCH 1/9] Added support for ota v2 --- cli/cli.go | 4 +- cli/device/list.go | 6 +- cli/device/tag/create.go | 32 ++++- cli/device/tag/delete.go | 35 ++++- cli/ota/massupload.go | 45 +++--- cli/ota/ota.go | 1 + cli/ota/status.go | 72 ++++++++++ command/device/list.go | 15 +- command/ota/massupload.go | 27 +++- command/ota/massupload_test.go | 19 ++- command/ota/status.go | 46 ++++++ command/ota/upload.go | 14 +- example/tools/ota/ota_mass_upload.sh | 153 ++++++++++++++++++++ internal/iot/client.go | 11 +- internal/iot/token.go | 12 +- internal/ota-api/client.go | 207 +++++++++++++++++++++++++++ internal/ota-api/dto.go | 186 ++++++++++++++++++++++++ 17 files changed, 832 insertions(+), 53 deletions(-) create mode 100644 cli/ota/status.go create mode 100644 command/ota/status.go create mode 100755 example/tools/ota/ota_mass_upload.sh create mode 100644 internal/ota-api/client.go create mode 100644 internal/ota-api/dto.go diff --git a/cli/cli.go b/cli/cli.go index 9f1e4899..4ddeb0ea 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -19,7 +19,7 @@ package cli import ( "fmt" - "io/ioutil" + "io" "os" "strings" @@ -83,7 +83,7 @@ func parseFormatString(arg string) (feedback.OutputFormat, bool) { } func preRun(flags *cliFlags) error { - logrus.SetOutput(ioutil.Discard) + logrus.SetOutput(io.Discard) // enable log only if verbose flag is passed if flags.verbose { logrus.SetLevel(logrus.InfoLevel) diff --git a/cli/device/list.go b/cli/device/list.go index a76a7412..a853dfc7 100644 --- a/cli/device/list.go +++ b/cli/device/list.go @@ -33,7 +33,8 @@ import ( ) type listFlags struct { - tags map[string]string + tags map[string]string + deviceIds string } func initListCommand() *cobra.Command { @@ -56,6 +57,7 @@ func initListCommand() *cobra.Command { "Comma-separated list of tags with format =.\n"+ "List only devices that match the provided tags.", ) + listCommand.Flags().StringVarP(&flags.deviceIds, "device-ids", "d", "", "Comma separated list of Device IDs") return listCommand } @@ -67,7 +69,7 @@ func runListCommand(flags *listFlags) error { return fmt.Errorf("retrieving credentials: %w", err) } - params := &device.ListParams{Tags: flags.tags} + params := &device.ListParams{Tags: flags.tags, DeviceIds: flags.deviceIds} devs, err := device.List(context.TODO(), params, cred) if err != nil { return err diff --git a/cli/device/tag/create.go b/cli/device/tag/create.go index 22b62a30..6303a6ea 100644 --- a/cli/device/tag/create.go +++ b/cli/device/tag/create.go @@ -21,6 +21,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/feedback" @@ -32,6 +33,7 @@ import ( type createTagsFlags struct { id string + ids string tags map[string]string } @@ -49,23 +51,45 @@ func InitCreateTagsCommand() *cobra.Command { }, } createTagsCommand.Flags().StringVarP(&flags.id, "id", "i", "", "Device ID") + createTagsCommand.Flags().StringVarP(&flags.ids, "ids", "", "", "Comma-separated list of Device IDs") createTagsCommand.Flags().StringToStringVar( &flags.tags, "tags", nil, "Comma-separated list of tags with format =.", ) - createTagsCommand.MarkFlagRequired("id") createTagsCommand.MarkFlagRequired("tags") return createTagsCommand } func runCreateTagsCommand(flags *createTagsFlags) error { - logrus.Infof("Creating tags on device %s", flags.id) + if flags.id == "" && flags.ids == "" { + return fmt.Errorf("missing required flag(s) \"id\" or \"ids\"") + } + + if flags.id != "" { + if err := creteTag(flags.id, flags.tags); err != nil { + return err + } + } + if flags.ids != "" { + idsArray := strings.Split(flags.ids, ",") + for _, id := range idsArray { + id = strings.TrimSpace(id) + if err := creteTag(id, flags.tags); err != nil { + return err + } + } + } + return nil +} + +func creteTag(id string, tags map[string]string) error { + logrus.Infof("Creating tags on device %s", id) params := &tag.CreateTagsParams{ - ID: flags.id, - Tags: flags.tags, + ID: id, + Tags: tags, Resource: tag.Device, } diff --git a/cli/device/tag/delete.go b/cli/device/tag/delete.go index 0779f2d1..1721a688 100644 --- a/cli/device/tag/delete.go +++ b/cli/device/tag/delete.go @@ -21,6 +21,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/feedback" @@ -32,6 +33,7 @@ import ( type deleteTagsFlags struct { id string + ids string keys []string } @@ -49,14 +51,39 @@ func InitDeleteTagsCommand() *cobra.Command { }, } deleteTagsCommand.Flags().StringVarP(&flags.id, "id", "i", "", "Device ID") + deleteTagsCommand.Flags().StringVarP(&flags.id, "ids", "", "", "Comma-separated list of Device IDs") deleteTagsCommand.Flags().StringSliceVarP(&flags.keys, "keys", "k", nil, "Comma-separated list of keys of tags to delete") - deleteTagsCommand.MarkFlagRequired("id") deleteTagsCommand.MarkFlagRequired("keys") return deleteTagsCommand } func runDeleteTagsCommand(flags *deleteTagsFlags) error { - logrus.Infof("Deleting tags with keys %s", flags.keys) + if flags.id == "" && flags.ids == "" { + return fmt.Errorf("missing required flag(s) \"id\" or \"ids\"") + } + + if flags.id != "" { + err := deleteTags(flags.id, flags.keys) + if err != nil { + return err + } + } + if flags.ids != "" { + ids := strings.Split(flags.ids, ",") + for _, id := range ids { + id = strings.TrimSpace(id) + err := deleteTags(id, flags.keys) + if err != nil { + return err + } + } + } + + return nil +} + +func deleteTags(id string, keys []string) error { + logrus.Infof("Deleting tags with keys %s", keys) cred, err := config.RetrieveCredentials() if err != nil { @@ -64,8 +91,8 @@ func runDeleteTagsCommand(flags *deleteTagsFlags) error { } params := &tag.DeleteTagsParams{ - ID: flags.id, - Keys: flags.keys, + ID: id, + Keys: keys, Resource: tag.Device, } diff --git a/cli/ota/massupload.go b/cli/ota/massupload.go index 10675780..baa4d4e9 100644 --- a/cli/ota/massupload.go +++ b/cli/ota/massupload.go @@ -22,7 +22,6 @@ import ( "fmt" "os" "sort" - "strings" "github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/feedback" @@ -96,22 +95,6 @@ func runMassUploadCommand(flags *massUploadFlags) error { }) feedback.PrintResult(massUploadResult{resp}) - - var failed []string - for _, r := range resp { - if r.Err != nil { - failed = append(failed, r.ID) - } - } - if len(failed) == 0 { - return nil - } - failDevs := strings.Join(failed, ",") - feedback.Printf( - "You can try to perform the OTA again on the failed devices using the following command:\n"+ - "$ arduino-cloud-cli ota mass-upload --file %s --fqbn %s -d %s", - params.File, params.FQBN, failDevs, - ) return nil } @@ -128,17 +111,35 @@ func (r massUploadResult) String() string { return "No OTA done." } t := table.New() - t.SetHeader("ID", "Result") + hasErrorReason := false + for _, r := range r.res { + if r.OtaStatus.ErrorReason != "" { + hasErrorReason = true + break + } + } + + if hasErrorReason { + t.SetHeader("Device ID", "Ota ID", "Result", "Error Reason") + } else { + t.SetHeader("Device ID", "Ota ID", "Result") + } + + // Now print the table for _, r := range r.res { outcome := "Success" if r.Err != nil { outcome = fmt.Sprintf("Fail: %s", r.Err.Error()) } + if r.OtaStatus.Status != "" { + outcome = r.OtaStatus.MapStatus() + } - t.AddRow( - r.ID, - outcome, - ) + line := []interface{}{r.ID, r.OtaStatus.ID, outcome} + if hasErrorReason { + line = append(line, r.OtaStatus.ErrorReason) + } + t.AddRow(line...) } return t.Render() } diff --git a/cli/ota/ota.go b/cli/ota/ota.go index 757c8272..f6b944e1 100644 --- a/cli/ota/ota.go +++ b/cli/ota/ota.go @@ -30,6 +30,7 @@ func NewCommand() *cobra.Command { otaCommand.AddCommand(initUploadCommand()) otaCommand.AddCommand(initMassUploadCommand()) + otaCommand.AddCommand(initOtaStatusCommand()) return otaCommand } diff --git a/cli/ota/status.go b/cli/ota/status.go new file mode 100644 index 00000000..ebcce353 --- /dev/null +++ b/cli/ota/status.go @@ -0,0 +1,72 @@ +// 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 ota + +import ( + "fmt" + "os" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/ota" + "github.com/arduino/arduino-cloud-cli/config" + "github.com/spf13/cobra" +) + +type statusFlags struct { + otaID string + otaIDs string + deviceId string + limit int16 + sort string +} + +func initOtaStatusCommand() *cobra.Command { + flags := &statusFlags{} + uploadCommand := &cobra.Command{ + Use: "status", + Short: "OTA status", + Long: "Get OTA status by OTA or device ID", + Run: func(cmd *cobra.Command, args []string) { + if err := runOtaStatusCommand(flags); err != nil { + feedback.Errorf("Error during ota get status: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + }, + } + uploadCommand.Flags().StringVarP(&flags.otaID, "ota-id", "o", "", "OTA ID") + uploadCommand.Flags().StringVarP(&flags.otaIDs, "ota-ids", "", "", "OTA IDs (comma separated)") + uploadCommand.Flags().StringVarP(&flags.deviceId, "device-id", "d", "", "Device ID") + uploadCommand.Flags().Int16VarP(&flags.limit, "limit", "l", 10, "Output limit (default: 10)") + uploadCommand.Flags().StringVarP(&flags.sort, "sort", "s", "desc", "Sorting (default: desc)") + + return uploadCommand +} + +func runOtaStatusCommand(flags *statusFlags) error { + if flags.otaID == "" && flags.deviceId == "" && flags.otaIDs == "" { + return fmt.Errorf("required flag(s) \"ota-id\" or \"device-id\" or \"ota-ids\" not set") + } + + cred, err := config.RetrieveCredentials() + if err != nil { + return fmt.Errorf("retrieving credentials: %w", err) + } + + return ota.PrintOtaStatus(flags.otaID, flags.otaIDs, flags.deviceId, cred, int(flags.limit), flags.sort) +} diff --git a/command/device/list.go b/command/device/list.go index d1f6af0f..96d08ed3 100644 --- a/command/device/list.go +++ b/command/device/list.go @@ -20,6 +20,8 @@ package device import ( "context" "fmt" + "slices" + "strings" "github.com/arduino/arduino-cloud-cli/config" "github.com/arduino/arduino-cloud-cli/internal/iot" @@ -28,7 +30,8 @@ import ( // ListParams contains the optional parameters needed // to filter the devices to be listed. type ListParams struct { - Tags map[string]string // If tags are provided, only devices that have all these tags are listed. + Tags map[string]string // If tags are provided, only devices that have all these tags are listed. + DeviceIds string // If ids are provided, only devices with these ids are listed. } // List command is used to list @@ -43,9 +46,19 @@ func List(ctx context.Context, params *ListParams, cred *config.Credentials) ([] if err != nil { return nil, err } + var deviceIdFilter []string + if params.DeviceIds != "" { + deviceIdFilter = strings.Split(params.DeviceIds, ",") + for i := range deviceIdFilter { + deviceIdFilter[i] = strings.TrimSpace(deviceIdFilter[i]) + } + } var devices []DeviceInfo for _, foundDev := range foundDevices { + if len(deviceIdFilter) > 0 && !slices.Contains(deviceIdFilter, foundDev.Id) { + continue + } dev, err := getDeviceInfo(&foundDev) if err != nil { return nil, fmt.Errorf("parsing device %s from cloud: %w", foundDev.Id, err) diff --git a/command/ota/massupload.go b/command/ota/massupload.go index 3cef9909..80bf5120 100644 --- a/command/ota/massupload.go +++ b/command/ota/massupload.go @@ -21,12 +21,12 @@ import ( "context" "errors" "fmt" - "io/ioutil" "os" "path/filepath" "github.com/arduino/arduino-cloud-cli/config" "github.com/arduino/arduino-cloud-cli/internal/iot" + otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" iotclient "github.com/arduino/iot-client-go" ) @@ -46,8 +46,9 @@ type MassUploadParams struct { // Result of an ota upload on a device. type Result struct { - ID string - Err error + ID string + Err error + OtaStatus otaapi.Ota } // MassUpload command is used to mass upload a firmware OTA, @@ -60,7 +61,7 @@ func MassUpload(ctx context.Context, params *MassUploadParams, cred *config.Cred } // Generate .ota file - otaDir, err := ioutil.TempDir("", "") + otaDir, err := os.MkdirTemp("", "") if err != nil { return nil, fmt.Errorf("%s: %w", "cannot create temporary folder", err) } @@ -76,6 +77,7 @@ func MassUpload(ctx context.Context, params *MassUploadParams, cred *config.Cred if err != nil { return nil, err } + otapi := otaapi.NewClient(cred) // Prepare the list of device-ids to update d, err := idsGivenTags(ctx, iotClient, params.Tags) @@ -96,7 +98,7 @@ func MassUpload(ctx context.Context, params *MassUploadParams, cred *config.Cred expiration = otaDeferredExpirationMins } - res := run(ctx, iotClient, valid, otaFile, expiration) + res := run(ctx, iotClient, otapi, valid, otaFile, expiration) res = append(res, invalid...) return res, nil } @@ -155,7 +157,11 @@ type otaUploader interface { DeviceOTA(ctx context.Context, id string, file *os.File, expireMins int) error } -func run(ctx context.Context, uploader otaUploader, ids []string, otaFile string, expiration int) []Result { +type otaStatusGetter interface { + GetOtaLastStatusByDeviceID(deviceID string) (*otaapi.OtaStatusList, error) +} + +func run(ctx context.Context, uploader otaUploader, otapi otaStatusGetter, ids []string, otaFile string, expiration int) []Result { type job struct { id string file *os.File @@ -181,7 +187,14 @@ func run(ctx context.Context, uploader otaUploader, ids []string, otaFile string go func() { for job := range jobs { err := uploader.DeviceOTA(ctx, job.id, job.file, expiration) - resCh <- Result{ID: job.id, Err: err} + otaResult := Result{ID: job.id, Err: err} + + otaID, otaapierr := otapi.GetOtaLastStatusByDeviceID(job.id) + if otaapierr == nil && otaID != nil && len(otaID.Ota) > 0 { + otaResult.OtaStatus = otaID.Ota[0] + } + + resCh <- otaResult } }() } diff --git a/command/ota/massupload_test.go b/command/ota/massupload_test.go index f3cec3c3..bc2856da 100644 --- a/command/ota/massupload_test.go +++ b/command/ota/massupload_test.go @@ -7,7 +7,9 @@ import ( "strings" "testing" + otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" iotclient "github.com/arduino/iot-client-go" + "github.com/gofrs/uuid" ) const testFilename = "testdata/empty.bin" @@ -20,6 +22,20 @@ func (d *deviceUploaderTest) DeviceOTA(ctx context.Context, id string, file *os. return d.deviceOTA(ctx, id, file, expireMins) } +type otaStatusGetterTest struct{} + +func (s *otaStatusGetterTest) GetOtaLastStatusByDeviceID(deviceID string) (*otaapi.OtaStatusList, error) { + ota := otaapi.Ota{ + ID: uuid.Must(uuid.NewV4()).String(), + Status: "in_progress", + StartedAt: "2021-09-01T12:00:00Z", + } + response := &otaapi.OtaStatusList{ + Ota: []otaapi.Ota{ota}, + } + return response, nil +} + func TestRun(t *testing.T) { var ( failPrefix = "00000000" @@ -38,9 +54,10 @@ func TestRun(t *testing.T) { return nil }, } + mockStatusClient := &otaStatusGetterTest{} devs := []string{okID1, failID1, okID2, failID2, okID3} - res := run(context.TODO(), mockClient, devs, testFilename, 0) + res := run(context.TODO(), mockClient, mockStatusClient, devs, testFilename, 0) if len(res) != len(devs) { t.Errorf("expected %d results, got %d", len(devs), len(res)) } diff --git a/command/ota/status.go b/command/ota/status.go new file mode 100644 index 00000000..6913209d --- /dev/null +++ b/command/ota/status.go @@ -0,0 +1,46 @@ +package ota + +import ( + "fmt" + + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/config" + otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" +) + +func PrintOtaStatus(otaid, otaids, device string, cred *config.Credentials, limit int, order string) error { + + if feedback.GetFormat() == feedback.JSONMini { + return fmt.Errorf("jsonmini format is not supported for this command") + } + + otapi := otaapi.NewClient(cred) + + if otaids != "" { + res, err := otapi.GetOtaStatusByOtaIDs(otaids) + if err == nil && res != nil { + feedback.PrintResult(res) + } else if err != nil { + return err + } + } else if otaid != "" { + res, err := otapi.GetOtaStatusByOtaID(otaid, limit, order) + if err == nil && res != nil { + feedback.PrintResult(otaapi.OtaStatusDetail{ + Ota: res.Ota, + Details: res.States, + }) + } else if err != nil { + return err + } + } else if device != "" { + res, err := otapi.GetOtaStatusByDeviceID(device, limit, order) + if err == nil && res != nil { + feedback.PrintResult(res) + } else if err != nil { + return err + } + } + + return nil +} diff --git a/command/ota/upload.go b/command/ota/upload.go index a98b6e44..c6c7dfc2 100644 --- a/command/ota/upload.go +++ b/command/ota/upload.go @@ -20,12 +20,13 @@ package ota import ( "context" "fmt" - "io/ioutil" "os" "path/filepath" + "github.com/arduino/arduino-cli/cli/feedback" "github.com/arduino/arduino-cloud-cli/config" "github.com/arduino/arduino-cloud-cli/internal/iot" + otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" ) const ( @@ -50,13 +51,14 @@ func Upload(ctx context.Context, params *UploadParams, cred *config.Credentials) if err != nil { return err } + otapi := otaapi.NewClient(cred) dev, err := iotClient.DeviceShow(ctx, params.DeviceID) if err != nil { return err } - otaDir, err := ioutil.TempDir("", "") + otaDir, err := os.MkdirTemp("", "") if err != nil { return fmt.Errorf("%s: %w", "cannot create temporary folder", err) } @@ -83,6 +85,14 @@ func Upload(ctx context.Context, params *UploadParams, cred *config.Credentials) if err != nil { return err } + // Try to get ota-id from API + otaID, err := otapi.GetOtaLastStatusByDeviceID(params.DeviceID) + if err != nil { + return err + } + if otaID != nil && len(otaID.Ota) > 0 { + feedback.PrintResult(otaID.Ota[0]) + } return nil } diff --git a/example/tools/ota/ota_mass_upload.sh b/example/tools/ota/ota_mass_upload.sh new file mode 100755 index 00000000..eae7018a --- /dev/null +++ b/example/tools/ota/ota_mass_upload.sh @@ -0,0 +1,153 @@ +# !/bin/bash + +# This script is used to upload the firmware to the device using the OTA service. + +export PATH=$PATH:. + +checkExecutable () { + if ! command -v $1 &> /dev/null + then + echo "$1 could not be found in PATH" + exit 1 + fi +} + +printHelp () { + echo "" + echo "Usage: $0 [-t | -d ] -f [-o ] [-v ]" + echo "" + echo "Examples -----------------" + echo " perform ota on devices with firmware=v1 tag" + echo " $0 -t firmware=v1 -f myfirmware.bin" + echo " perform ota on devices with firmware=v1 tag and apply new tag firmware=v2 to updated devices, waiting for 1200 seconds" + echo " $0 -t firmware=v1 -f myfirmware.bin -v firmware=v2 -w 1200" + echo " perform ota on two specified devices" + echo " $0 -d 261ec96a-38ba-4520-96e6-2447c4163e9b,8b10acdb-b722-4068-8e4d-d1c1b7302df4 -f myfirmware.bin" + echo "" + exit 1 +} + +# Check dependencies... +checkExecutable "arduino-cloud-cli" +checkExecutable "jq" +checkExecutable "sort" +checkExecutable "uniq" +checkExecutable "paste" + +# Default wait time for OTA process to complete +waittime=600 +newtagversion="" + +while getopts t:v:f:o:d: flag +do + case "${flag}" in + t) tag=${OPTARG};; + v) newtagversion=${OPTARG};; + f) firmwarefile=${OPTARG};; + o) waittime=${OPTARG};; + d) deviceids=${OPTARG};; + esac +done + +if [[ "$firmwarefile" == "" || "$waittime" == "" ]]; then + printHelp +fi +if [[ "$tag" == "" && "$deviceids" == "" ]]; then + printHelp +fi + +if [[ "$deviceids" == "" ]]; then + echo "Starting OTA process for devices with tag \"$tag\" using firmware \"$firmwarefile\"" + echo "" + + devicelistjson=$(arduino-cloud-cli device list --tags $tag --format json) +else + echo "Starting OTA process for devices \"$deviceids\" using firmware \"$firmwarefile\"" + echo "" + + devicelistjson=$(arduino-cloud-cli device list -d $deviceids --format json) +fi + +if [[ "$devicelistjson" == "" || "$devicelistjson" == "null" ]]; then + echo "No device found" + exit 1 +fi + +devicecount=$(echo $devicelistjson | jq '.[] | .id' | wc -l) + +if [ "$devicecount" -gt 0 ]; then + echo "Found $devicecount devices" + echo "" + if [[ "$deviceids" == "" ]]; then + arduino-cloud-cli device list --tags $tag + else + arduino-cloud-cli device list -d $deviceids + fi +else + echo "No device found" + exit 1 +fi + +fqbncount=$(echo $devicelistjson | jq '.[] | .fqbn' | sort | uniq | wc -l) + +if [ "$fqbncount" -gt 1 ]; then + echo "Mixed FQBNs detected. Please ensure all devices have the same FQBN." + fqbns=$(echo $devicelistjson | jq '.[] | .fqbn' | sort | uniq) + echo "Detected FQBNs:" + echo "$fqbns" + exit 1 +fi + +fqbn=$(echo $devicelistjson | jq -r '.[] | .fqbn' | sort | uniq | head -n 1) + +echo "Sending OTA request to detected boards of type $fqbn..." +if [[ "$deviceids" == "" ]]; then + otastartedout=$(arduino-cloud-cli ota mass-upload --device-tags $tag --file $firmwarefile -b $fqbn --format json) +else + otastartedout=$(arduino-cloud-cli ota mass-upload -d $deviceids --file $firmwarefile -b $fqbn --format json) +fi +if [ $? -ne 0 ]; then + echo "Detected error during OTA process. Exiting..." + exit 1 +fi + +otaids=$(echo $otastartedout | jq -r '.[] | .OtaStatus | .id' | uniq | paste -sd "," -) + +if [ $otaids == "null" ]; then + echo "No OTA processes to monitor. This could be due to an upgrade from previous ArduinoIotLibrary versions. Exiting..." + exit 0 +fi + +correctlyfinished=0 +while [ $waittime -gt 0 ]; do + echo "Waiting for $waittime seconds for OTA process to complete..." + sleep 15 + waittime=$((waittime-15)) + # Check status of running processess... + otastatuslines=$(arduino-cloud-cli ota status --ota-ids $otaids --format json) + otastatusinpcnt=$(echo $otastatuslines | grep in_progress | wc -l) + otastatuspencnt=$(echo $otastatuslines | grep pending | wc -l) + otasucceeded=$(echo $otastatuslines | jq -r '.[] | select (.status | contains("succeeded")) | .device_id' | uniq | paste -sd "," -) + if [[ $otastatusinpcnt -eq 0 && $otastatuspencnt -eq 0 ]]; then + correctlyfinished=1 + break + fi +done + +echo "" +echo "Status report:" +arduino-cloud-cli ota status --ota-ids $otaids + +if [ $correctlyfinished -eq 0 ]; then + echo "OTA process did not complete within the specified time for some boards" + exit 1 +else + echo "OTA process completed successfully for all boards" + if [ "$newtagversion" != "" ]; then + echo "Tagging updated devices with tag: $newtagversion" + arduino-cloud-cli device create-tags --ids $otasucceeded --tags $newtagversion + fi + exit 0 +fi + +exit 0 diff --git a/internal/iot/client.go b/internal/iot/client.go index 3a438b27..55170caa 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -195,8 +195,8 @@ func (cl *Client) DeviceOTA(ctx context.Context, id string, file *os.File, expir ExpireInMins: optional.NewInt32(int32(expireMins)), Async: optional.NewBool(true), } - _, err = cl.api.DevicesV2OtaApi.DevicesV2OtaUpload(ctx, id, file, opt) - if err != nil { + resp, err := cl.api.DevicesV2OtaApi.DevicesV2OtaUpload(ctx, id, file, opt) + if err != nil && resp.StatusCode != 409 { // 409 (Conflict) is the status code for an already existing OTA for the same SHA/device, so ignoring it. err = fmt.Errorf("uploading device ota: %w", errorDetail(err)) return err } @@ -471,13 +471,10 @@ func (cl *Client) DashboardDelete(ctx context.Context, id string) error { } func (cl *Client) setup(client, secret, organization string) error { - baseURL := "https://api2.arduino.cc" - if url := os.Getenv("IOT_API_URL"); url != "" { - baseURL = url - } + baseURL := GetArduinoAPIBaseURL() // Configure a token source given the user's credentials. - cl.token = token(client, secret, baseURL) + cl.token = NewUserTokenSource(client, secret, baseURL) config := iotclient.NewConfiguration() if organization != "" { diff --git a/internal/iot/token.go b/internal/iot/token.go index 2f4b1ead..717ef342 100644 --- a/internal/iot/token.go +++ b/internal/iot/token.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "net/url" + "os" "strings" iotclient "github.com/arduino/iot-client-go" @@ -29,7 +30,16 @@ import ( cc "golang.org/x/oauth2/clientcredentials" ) -func token(client, secret, baseURL string) oauth2.TokenSource { +func GetArduinoAPIBaseURL() string { + baseURL := "https://api2.arduino.cc" + if url := os.Getenv("IOT_API_URL"); url != "" { + baseURL = url + } + return baseURL +} + +// Build a new token source to forge api JWT tokens based on provided credentials +func NewUserTokenSource(client, secret, baseURL string) oauth2.TokenSource { // We need to pass the additional "audience" var to request an access token. additionalValues := url.Values{} additionalValues.Add("audience", "https://api2.arduino.cc/iot") diff --git a/internal/ota-api/client.go b/internal/ota-api/client.go new file mode 100644 index 00000000..dc647406 --- /dev/null +++ b/internal/ota-api/client.go @@ -0,0 +1,207 @@ +// 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 otaapi + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sort" + "strings" + "time" + + "github.com/arduino/arduino-cloud-cli/config" + "github.com/arduino/arduino-cloud-cli/internal/iot" + "golang.org/x/oauth2" +) + +const ( + OrderDesc = "desc" + OrderAsc = "asc" +) + +var ErrAlreadyInProgress = fmt.Errorf("already in progress") + +type OtaApiClient struct { + client *http.Client + host string + src oauth2.TokenSource + organization string +} + +func NewClient(credentials *config.Credentials) *OtaApiClient { + host := iot.GetArduinoAPIBaseURL() + tokenSource := iot.NewUserTokenSource(credentials.Client, credentials.Secret, host) + return &OtaApiClient{ + client: &http.Client{}, + src: tokenSource, + host: host, + organization: credentials.Organization, + } +} + +func (c *OtaApiClient) performGetRequest(endpoint, token string) (*http.Response, error) { + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+token) + req.Header.Add("Content-Type", "application/json") + if c.organization != "" { + req.Header.Add("X-Organization", c.organization) + } + res, err := c.client.Do(req) + if err != nil { + return nil, err + } + return res, nil +} + +func (c *OtaApiClient) GetOtaStatusByOtaID(otaid string, limit int, order string) (*OtaStatusResponse, error) { + + if otaid == "" { + return nil, fmt.Errorf("invalid ota-id: empty") + } + + userRequestToken, err := c.src.Token() + if err != nil { + if strings.Contains(err.Error(), "401") { + return nil, errors.New("wrong credentials") + } + return nil, fmt.Errorf("cannot retrieve a valid token: %w", err) + } + + endpoint := c.host + "/ota/v1/ota/" + otaid + res, err := c.performGetRequest(endpoint, userRequestToken.AccessToken) + if err != nil { + return nil, err + } + defer res.Body.Close() + bodyb, err := io.ReadAll(res.Body) + + if res.StatusCode == 200 { + var otaResponse OtaStatusResponse + if err == nil && bodyb != nil { + err = json.Unmarshal(bodyb, &otaResponse) + if err != nil { + return nil, err + } + } + + if len(otaResponse.States) > 0 { + // Sort output by StartedAt + sort.Slice(otaResponse.States, func(i, j int) bool { + t1, err := time.Parse(time.RFC3339, otaResponse.States[i].Timestamp) + if err != nil { + return false + } + t2, err := time.Parse(time.RFC3339, otaResponse.States[j].Timestamp) + if err != nil { + return false + } + if order == "asc" { + return t1.Before(t2) + } + return t1.After(t2) + }) + if limit > 0 && len(otaResponse.States) > limit { + otaResponse.States = otaResponse.States[:limit] + } + } + + return &otaResponse, nil + } else if res.StatusCode == 404 || res.StatusCode == 400 { + return nil, fmt.Errorf("ota-id %s not found", otaid) + } + + return nil, err +} + +func (c *OtaApiClient) GetOtaStatusByOtaIDs(otaids string) (*OtaStatusList, error) { + + ids := strings.Split(otaids, ",") + if len(ids) == 0 { + return nil, fmt.Errorf("invalid ota-ids: empty") + } + + returnStatus := OtaStatusList{} + for _, id := range ids { + if id != "" { + resp, err := c.GetOtaStatusByOtaID(id, 1, OrderDesc) + if err != nil { + return nil, err + } + returnStatus.Ota = append(returnStatus.Ota, resp.Ota) + } + + } + + return &returnStatus, nil +} + +func (c *OtaApiClient) GetOtaLastStatusByDeviceID(deviceID string) (*OtaStatusList, error) { + return c.GetOtaStatusByDeviceID(deviceID, 1, OrderDesc) +} + +func (c *OtaApiClient) GetOtaStatusByDeviceID(deviceID string, limit int, order string) (*OtaStatusList, error) { + + if deviceID == "" { + return nil, fmt.Errorf("invalid device-id: empty") + } + + userRequestToken, err := c.src.Token() + if err != nil { + if strings.Contains(err.Error(), "401") { + return nil, errors.New("wrong credentials") + } + return nil, fmt.Errorf("cannot retrieve a valid token: %w", err) + } + + endpoint := c.host + "/ota/v1/ota?device_id=" + deviceID + if limit > 0 { + endpoint += "&limit=" + fmt.Sprintf("%d", limit) + } + if order != "" && (order == "asc" || order == "desc") { + endpoint += "&order=" + order + } + res, err := c.performGetRequest(endpoint, userRequestToken.AccessToken) + if err != nil { + return nil, err + } + defer res.Body.Close() + bodyb, err := io.ReadAll(res.Body) + + if res.StatusCode == 200 { + var otaResponse OtaStatusList + if err == nil && bodyb != nil { + err = json.Unmarshal(bodyb, &otaResponse) + if err != nil { + return nil, err + } + } + return &otaResponse, nil + } else if res.StatusCode == 404 || res.StatusCode == 400 { + return nil, fmt.Errorf("device-id %s not found", deviceID) + } else if res.StatusCode == 409 { + return nil, ErrAlreadyInProgress + } + + return nil, err +} diff --git a/internal/ota-api/dto.go b/internal/ota-api/dto.go new file mode 100644 index 00000000..1ffeb707 --- /dev/null +++ b/internal/ota-api/dto.go @@ -0,0 +1,186 @@ +// 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 otaapi + +import ( + "strings" + "time" + + "unicode" + + "github.com/arduino/arduino-cli/table" +) + +type ( + OtaStatusResponse struct { + Ota Ota `json:"ota"` + States []State `json:"states,omitempty"` + } + + OtaStatusList struct { + Ota []Ota `json:"ota"` + } + + Ota struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + DeviceID string `json:"device_id,omitempty" yaml:"device_id,omitempty"` + Status string `json:"status" yaml:"status"` + StartedAt string `json:"started_at" yaml:"started_at"` + EndedAt string `json:"ended_at,omitempty" yaml:"ended_at,omitempty"` + ErrorReason string `json:"error_reason,omitempty" yaml:"error_reason,omitempty"` + Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"` + } + + State struct { + OtaID string `json:"ota_id"` + State string `json:"state"` + StateData string `json:"state_data,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + } + + OtaStatusDetail struct { + Ota Ota `json:"ota"` + Details []State `json:"details,omitempty"` + } +) + +func (r OtaStatusList) Data() interface{} { + return r.Ota +} + +func (r OtaStatusList) String() string { + if len(r.Ota) == 0 { + return "" + } + t := table.New() + hasErrorReason := false + for _, r := range r.Ota { + if r.ErrorReason != "" { + hasErrorReason = true + break + } + } + + if hasErrorReason { + t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At", "Error Reason") + } else { + t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At") + } + + // Now print the table + for _, r := range r.Ota { + line := []any{r.DeviceID, r.ID, r.MapStatus(), formatHumanReadableTs(r.StartedAt), formatHumanReadableTs(r.EndedAt)} + if hasErrorReason { + line = append(line, r.ErrorReason) + } + t.AddRow(line...) + } + + return t.Render() +} + +func (o Ota) MapStatus() string { + return upperCaseFirst(o.Status) +} + +func (r Ota) Data() interface{} { + return r +} + +func (r Ota) String() string { + if len(r.ID) == 0 { + return "" + } + t := table.New() + hasErrorReason := r.ErrorReason != "" + + if hasErrorReason { + t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At", "Error Reason") + } else { + t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At") + } + + // Now print the table + line := []any{r.DeviceID, r.DeviceID, r.MapStatus(), formatHumanReadableTs(r.StartedAt), formatHumanReadableTs(r.EndedAt)} + if hasErrorReason { + line = append(line, r.ErrorReason) + } + t.AddRow(line...) + + return t.Render() +} + +func (r OtaStatusDetail) Data() interface{} { + return r.Ota +} + +func (r OtaStatusDetail) String() string { + if r.Ota.ID == "" { + return "No OTA found" + } + t := table.New() + hasErrorReason := r.Ota.ErrorReason != "" + + if hasErrorReason { + t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At", "Error Reason") + } else { + t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At") + } + + // Now print the table + line := []any{r.Ota.DeviceID, r.Ota.ID, r.Ota.MapStatus(), formatHumanReadableTs(r.Ota.StartedAt), formatHumanReadableTs(r.Ota.EndedAt)} + if hasErrorReason { + line = append(line, r.Ota.ErrorReason) + } + t.AddRow(line...) + + output := t.Render() + + // Add details + if len(r.Details) > 0 { + t = table.New() + t.SetHeader("Time", "Status", "Detail") + for _, s := range r.Details { + t.AddRow(formatHumanReadableTs(s.Timestamp), upperCaseFirst(s.State), s.StateData) + } + output += "\nDetails:\n" + t.Render() + } + + return output +} + +func upperCaseFirst(s string) string { + if len(s) > 0 { + s = strings.ReplaceAll(s, "_", " ") + for i, v := range s { + return string(unicode.ToUpper(v)) + s[i+1:] + } + } + return "" +} + +func formatHumanReadableTs(ts string) string { + if ts == "" { + return "" + } + parsed, err := time.Parse(time.RFC3339Nano, ts) + if err != nil { + return ts + } + return parsed.Format(time.RFC3339) +} From 44935b5b11a268beb0d7d3afd7910a1cb092cde2 Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Thu, 18 Apr 2024 11:40:10 +0200 Subject: [PATCH 2/9] Fixed go compatibility --- command/device/list.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/command/device/list.go b/command/device/list.go index 96d08ed3..ba1ffd43 100644 --- a/command/device/list.go +++ b/command/device/list.go @@ -20,7 +20,6 @@ package device import ( "context" "fmt" - "slices" "strings" "github.com/arduino/arduino-cloud-cli/config" @@ -56,7 +55,7 @@ func List(ctx context.Context, params *ListParams, cred *config.Credentials) ([] var devices []DeviceInfo for _, foundDev := range foundDevices { - if len(deviceIdFilter) > 0 && !slices.Contains(deviceIdFilter, foundDev.Id) { + if len(deviceIdFilter) > 0 && !sliceContains(deviceIdFilter, foundDev.Id) { continue } dev, err := getDeviceInfo(&foundDev) @@ -68,3 +67,12 @@ func List(ctx context.Context, params *ListParams, cred *config.Credentials) ([] return devices, nil } + +func sliceContains(s []string, v string) bool { + for i := range s { + if v == s[i] { + return true + } + } + return false +} From 046b6a01609bdfad9c88d000415117f00e24dbd0 Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Thu, 18 Apr 2024 12:03:00 +0200 Subject: [PATCH 3/9] Fixed output for ota status --- internal/ota-api/dto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ota-api/dto.go b/internal/ota-api/dto.go index 1ffeb707..06b401bb 100644 --- a/internal/ota-api/dto.go +++ b/internal/ota-api/dto.go @@ -116,7 +116,7 @@ func (r Ota) String() string { } // Now print the table - line := []any{r.DeviceID, r.DeviceID, r.MapStatus(), formatHumanReadableTs(r.StartedAt), formatHumanReadableTs(r.EndedAt)} + line := []any{r.DeviceID, r.ID, r.MapStatus(), formatHumanReadableTs(r.StartedAt), formatHumanReadableTs(r.EndedAt)} if hasErrorReason { line = append(line, r.ErrorReason) } From aeef30c13033ae240e1d8b3bef097f9794fbda37 Mon Sep 17 00:00:00 2001 From: Roberto Gazia Date: Thu, 18 Apr 2024 14:39:14 +0200 Subject: [PATCH 4/9] refactor: fix typos --- example/tools/ota/ota_mass_upload.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/tools/ota/ota_mass_upload.sh b/example/tools/ota/ota_mass_upload.sh index eae7018a..f6ac16d7 100755 --- a/example/tools/ota/ota_mass_upload.sh +++ b/example/tools/ota/ota_mass_upload.sh @@ -1,4 +1,4 @@ -# !/bin/bash +#!/bin/bash # This script is used to upload the firmware to the device using the OTA service. @@ -20,7 +20,7 @@ printHelp () { echo " perform ota on devices with firmware=v1 tag" echo " $0 -t firmware=v1 -f myfirmware.bin" echo " perform ota on devices with firmware=v1 tag and apply new tag firmware=v2 to updated devices, waiting for 1200 seconds" - echo " $0 -t firmware=v1 -f myfirmware.bin -v firmware=v2 -w 1200" + echo " $0 -t firmware=v1 -f myfirmware.bin -v firmware=v2 -o 1200" echo " perform ota on two specified devices" echo " $0 -d 261ec96a-38ba-4520-96e6-2447c4163e9b,8b10acdb-b722-4068-8e4d-d1c1b7302df4 -f myfirmware.bin" echo "" From 02860e60c81772616187689a530658aa5d373fce Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Fri, 19 Apr 2024 09:59:55 +0200 Subject: [PATCH 5/9] Tagging improvements --- example/tools/ota/ota_mass_upload.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/example/tools/ota/ota_mass_upload.sh b/example/tools/ota/ota_mass_upload.sh index f6ac16d7..dfb3fc25 100755 --- a/example/tools/ota/ota_mass_upload.sh +++ b/example/tools/ota/ota_mass_upload.sh @@ -138,16 +138,20 @@ echo "" echo "Status report:" arduino-cloud-cli ota status --ota-ids $otaids +exitcode=0 if [ $correctlyfinished -eq 0 ]; then echo "OTA process did not complete within the specified time for some boards" - exit 1 + exitcode=1 else echo "OTA process completed successfully for all boards" - if [ "$newtagversion" != "" ]; then - echo "Tagging updated devices with tag: $newtagversion" - arduino-cloud-cli device create-tags --ids $otasucceeded --tags $newtagversion - fi - exit 0 fi -exit 0 +if [ "$newtagversion" != "" ]; then + echo "" + echo "Tag updated devices as \"$newtagversion\"" + arduino-cloud-cli device create-tags --ids $otasucceeded --tags $newtagversion + echo "" + arduino-cloud-cli device list --tags $newtagversion +fi + +exit $exitcode From cc9dc77db8bc6329070874a2ee64a57ded4a3829 Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Fri, 19 Apr 2024 15:33:27 +0200 Subject: [PATCH 6/9] Tag in case of v1.x lib update --- example/tools/ota/ota_mass_upload.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/example/tools/ota/ota_mass_upload.sh b/example/tools/ota/ota_mass_upload.sh index dfb3fc25..0ea0706f 100755 --- a/example/tools/ota/ota_mass_upload.sh +++ b/example/tools/ota/ota_mass_upload.sh @@ -115,6 +115,14 @@ otaids=$(echo $otastartedout | jq -r '.[] | .OtaStatus | .id' | uniq | paste -sd if [ $otaids == "null" ]; then echo "No OTA processes to monitor. This could be due to an upgrade from previous ArduinoIotLibrary versions. Exiting..." + if [ "$newtagversion" != "" ]; then + otasucceeded=$(echo $devicelistjson | jq -r '.[] | .id' | uniq | paste -sd "," -) + echo "" + echo "Tag updated devices as \"$newtagversion\"" + arduino-cloud-cli device create-tags --ids $otasucceeded --tags $newtagversion + echo "" + arduino-cloud-cli device list --tags $newtagversion + fi exit 0 fi From 5e29112f4b8b04edfe17e05360c4aface11b49ad Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Mon, 29 Apr 2024 12:31:08 +0200 Subject: [PATCH 7/9] Fixed compile --- command/ota/massupload.go | 7 +------ command/ota/upload.go | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/command/ota/massupload.go b/command/ota/massupload.go index 2904a303..0d81ae2e 100644 --- a/command/ota/massupload.go +++ b/command/ota/massupload.go @@ -26,8 +26,8 @@ import ( "github.com/arduino/arduino-cloud-cli/config" "github.com/arduino/arduino-cloud-cli/internal/iot" - otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" "github.com/arduino/arduino-cloud-cli/internal/ota" + otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" iotclient "github.com/arduino/iot-client-go" ) @@ -64,11 +64,6 @@ func MassUpload(ctx context.Context, params *MassUploadParams, cred *config.Cred } // Generate .ota file - otaDir, err := os.MkdirTemp("", "") - if err != nil { - return nil, fmt.Errorf("error generating temp directory: %w", err) - } - _, err := os.Stat(params.File) if err != nil { return nil, fmt.Errorf("file %s does not exists: %w", params.File, err) diff --git a/command/ota/upload.go b/command/ota/upload.go index d31430bd..fcb32267 100644 --- a/command/ota/upload.go +++ b/command/ota/upload.go @@ -26,8 +26,8 @@ import ( "github.com/arduino/arduino-cli/cli/feedback" "github.com/arduino/arduino-cloud-cli/config" "github.com/arduino/arduino-cloud-cli/internal/iot" - otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" "github.com/arduino/arduino-cloud-cli/internal/ota" + otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" ) const ( @@ -65,11 +65,6 @@ func Upload(ctx context.Context, params *UploadParams, cred *config.Credentials) return err } - otaDir, err := os.MkdirTemp("", "") - if err != nil { - return fmt.Errorf("%s: %w", "cannot create temporary folder", err) - } - if !params.DoNotApplyHeader { //Verify if file has already an OTA header header, _ := ota.DecodeOtaFirmwareHeaderFromFile(params.File) From fe99c531dfc2702d3497330aa580f861aa98eaad Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Thu, 2 May 2024 09:09:52 +0200 Subject: [PATCH 8/9] Removed not used param --- internal/ota-api/dto.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/ota-api/dto.go b/internal/ota-api/dto.go index 06b401bb..eaf09424 100644 --- a/internal/ota-api/dto.go +++ b/internal/ota-api/dto.go @@ -43,7 +43,6 @@ type ( StartedAt string `json:"started_at" yaml:"started_at"` EndedAt string `json:"ended_at,omitempty" yaml:"ended_at,omitempty"` ErrorReason string `json:"error_reason,omitempty" yaml:"error_reason,omitempty"` - Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"` } State struct { From a63774fee003ab60cd383a20e75cfe220c466e1c Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Thu, 2 May 2024 09:40:02 +0200 Subject: [PATCH 9/9] Increased timeout --- example/tools/ota/ota_mass_upload.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/tools/ota/ota_mass_upload.sh b/example/tools/ota/ota_mass_upload.sh index 0ea0706f..77127235 100755 --- a/example/tools/ota/ota_mass_upload.sh +++ b/example/tools/ota/ota_mass_upload.sh @@ -35,7 +35,7 @@ checkExecutable "uniq" checkExecutable "paste" # Default wait time for OTA process to complete -waittime=600 +waittime=900 newtagversion="" while getopts t:v:f:o:d: flag