diff --git a/cli/ota/status.go b/cli/ota/status.go index 89d67081..4c3ce90e 100644 --- a/cli/ota/status.go +++ b/cli/ota/status.go @@ -38,28 +38,29 @@ type statusFlags struct { func initOtaStatusCommand() *cobra.Command { flags := &statusFlags{} - uploadCommand := &cobra.Command{ + statusCommand := &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 := runPrintOtaStatusCommand(flags); err != nil { - feedback.Errorf("Error during ota get status: %v", err) + if err := runPrintOtaStatusCommand(flags, cmd); err != nil { + feedback.Errorf("\nError 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)") + statusCommand.Flags().StringVarP(&flags.otaID, "ota-id", "o", "", "OTA ID") + statusCommand.Flags().StringVarP(&flags.otaIDs, "ota-ids", "", "", "OTA IDs (comma separated)") + statusCommand.Flags().StringVarP(&flags.deviceId, "device-id", "d", "", "Device ID") + statusCommand.Flags().Int16VarP(&flags.limit, "limit", "l", 10, "Output limit (default: 10)") + statusCommand.Flags().StringVarP(&flags.sort, "sort", "s", "desc", "Sorting (default: desc)") - return uploadCommand + return statusCommand } -func runPrintOtaStatusCommand(flags *statusFlags) error { +func runPrintOtaStatusCommand(flags *statusFlags, command *cobra.Command) error { if flags.otaID == "" && flags.deviceId == "" && flags.otaIDs == "" { + command.Help() return fmt.Errorf("required flag(s) \"ota-id\" or \"device-id\" or \"ota-ids\" not set") } diff --git a/command/ota/status.go b/command/ota/status.go index 6913209d..f13ec5dd 100644 --- a/command/ota/status.go +++ b/command/ota/status.go @@ -27,8 +27,9 @@ func PrintOtaStatus(otaid, otaids, device string, cred *config.Credentials, limi res, err := otapi.GetOtaStatusByOtaID(otaid, limit, order) if err == nil && res != nil { feedback.PrintResult(otaapi.OtaStatusDetail{ - Ota: res.Ota, - Details: res.States, + FirmwareSize: res.FirmwareSize, + Ota: res.Ota, + Details: res.States, }) } else if err != nil { return err diff --git a/internal/ota-api/dto.go b/internal/ota-api/dto.go index eaf09424..04184ed8 100644 --- a/internal/ota-api/dto.go +++ b/internal/ota-api/dto.go @@ -18,6 +18,7 @@ package otaapi import ( + "strconv" "strings" "time" @@ -26,10 +27,13 @@ import ( "github.com/arduino/arduino-cli/table" ) +const progressBarMultiplier = 2 + type ( OtaStatusResponse struct { - Ota Ota `json:"ota"` - States []State `json:"states,omitempty"` + FirmwareSize *int64 `json:"firmware_size,omitempty"` + Ota Ota `json:"ota"` + States []State `json:"states,omitempty"` } OtaStatusList struct { @@ -53,8 +57,9 @@ type ( } OtaStatusDetail struct { - Ota Ota `json:"ota"` - Details []State `json:"details,omitempty"` + FirmwareSize *int64 `json:"firmware_size,omitempty"` + Ota Ota `json:"ota"` + Details []State `json:"details,omitempty"` } ) @@ -154,8 +159,13 @@ func (r OtaStatusDetail) String() string { if len(r.Details) > 0 { t = table.New() t.SetHeader("Time", "Status", "Detail") + fwSize := int64(0) + if r.FirmwareSize != nil { + fwSize = *r.FirmwareSize + } for _, s := range r.Details { - t.AddRow(formatHumanReadableTs(s.Timestamp), upperCaseFirst(s.State), s.StateData) + stateData := formatStateData(s.State, s.StateData, fwSize, hasReachedFlashState(r.Details)) + t.AddRow(formatHumanReadableTs(s.Timestamp), upperCaseFirst(s.State), stateData) } output += "\nDetails:\n" + t.Render() } @@ -163,6 +173,48 @@ func (r OtaStatusDetail) String() string { return output } +func hasReachedFlashState(states []State) bool { + for _, s := range states { + if s.State == "flash" || s.State == "reboot" { + return true + } + } + return false +} + +func formatStateData(state, data string, firmware_size int64, hasReceivedFlashState bool) string { + if data == "" || data == "Unknown" { + return "" + } + if state == "fetch" { + // This is the state 'fetch' of OTA progress. This contains a number that represents the number of bytes fetched + actualDownloadedData, err := strconv.Atoi(data) + if err != nil || actualDownloadedData <= 0 || firmware_size <= 0 { // Sanitize and avoid division by zero + return data + } + if hasReceivedFlashState { + return buildSimpleProgressBar(float64(100)) + } + percentage := (float64(actualDownloadedData) / float64(firmware_size)) * 100 + return buildSimpleProgressBar(percentage) + } + return data +} + +func buildSimpleProgressBar(progress float64) string { + progressInt := int(progress) / 10 + progressInt = progressInt * progressBarMultiplier + maxProgress := 10 * progressBarMultiplier + var bar strings.Builder + bar.WriteString("[") + bar.WriteString(strings.Repeat("=", progressInt)) + bar.WriteString(strings.Repeat(" ", maxProgress-progressInt)) + bar.WriteString("] ") + bar.WriteString(strconv.FormatFloat(progress, 'f', 2, 64)) + bar.WriteString("%") + return bar.String() +} + func upperCaseFirst(s string) string { if len(s) > 0 { s = strings.ReplaceAll(s, "_", " ") diff --git a/internal/ota-api/dto_test.go b/internal/ota-api/dto_test.go new file mode 100644 index 00000000..bb91225f --- /dev/null +++ b/internal/ota-api/dto_test.go @@ -0,0 +1,19 @@ +package otaapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProgressBar_notCompletePct(t *testing.T) { + firmwareSize := int64(25665 * 2) + bar := formatStateData("fetch", "25665", firmwareSize, false) + assert.Equal(t, "[========== ] 50.00%", bar) +} + +func TestProgressBar_ifFlashState_goTo100Pct(t *testing.T) { + firmwareSize := int64(25665 * 2) + bar := formatStateData("fetch", "25665", firmwareSize, true) // If in flash status, go to 100% + assert.Equal(t, "[====================] 100.00%", bar) +}
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: