From 654f9f5871e60f18beb6c1896c8d197069a3c569 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Mon, 9 Sep 2024 14:27:18 +0200 Subject: [PATCH 1/6] Add documentation for Jaguar defines. (#556) --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 2b653b3..461578a 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,32 @@ like `10s`, `5m`, or `1h` to indicate how many seconds, minutes, or hours the ap jag run -D jag.timeout=10s service.toit ``` +## Defines +Jaguar supports defining values that can be used in your Toit code. This is done through +the `-D` option. Its primary use is to configure Jaguar (see below for temporarily disabling +Jaguar), but it can also be used to pass values to your Toit code. + +All defined values that are available in the `jag.defines` assets, where they are stored as +Tison. The `encoding.tison` library has functions to extract these values from the assets. + +``` sh +jag run -D my-define=499 defines.toit +``` + +``` toit +// defines.toit +import encoding.tison +import system.assets + +main: + defines := assets.decode.get "jag.defines" + --if-present=: tison.decode it + --if-absent=: {:} + if defines is not Map: + throw "defines are malformed" + print defines["my-define"] +``` + ## Temporarily disabling Jaguar You can disable Jaguar while your application runs using the `-D jag.disabled`. This is useful if Jaguar otherwise interferes with your application. As an example, consider an application that uses the WiFi to setup a From 687ec362cd2ec656e3426c3609ef945784a22846 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Thu, 19 Sep 2024 14:01:31 +0200 Subject: [PATCH 2/6] Roll tpkg package. (#543) --- go.mod | 2 +- go.sum | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1e20e73..0915bf4 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/skeema/knownhosts v1.2.1 // indirect github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.17.0 - github.com/toitlang/tpkg v0.0.0-20230928125532-1be02ac647ae + github.com/toitlang/tpkg v0.0.0-20240919112017-273e738f33d0 github.com/toitware/ubjson v0.0.0-20231002110407-71c8fab5e607 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c go.bug.st/serial v1.6.1 diff --git a/go.sum b/go.sum index 17c20f5..2d06c47 100644 --- a/go.sum +++ b/go.sum @@ -1035,6 +1035,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -1418,7 +1420,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -1716,8 +1717,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/toitlang/tpkg v0.0.0-20230928125532-1be02ac647ae h1:vmp2+atDvyFKE6TOSepzmrtNuSy8bXW5WagF3ZuJfq0= -github.com/toitlang/tpkg v0.0.0-20230928125532-1be02ac647ae/go.mod h1:L57D7q11LI+dCu8icjX3Lh2qRidFP5FrbFGdE7c1Bpw= +github.com/toitlang/tpkg v0.0.0-20240919112017-273e738f33d0 h1:XkdGj+nHUvEZYPTe8T5JGDeKEchf8tjcyKPaj/6kIG4= +github.com/toitlang/tpkg v0.0.0-20240919112017-273e738f33d0/go.mod h1:Hjfmv2z6MRbjgaGHVdRizYOHJhxQkQZesO3qx75Qk4w= github.com/toitware/ubjson v0.0.0-20231002110407-71c8fab5e607 h1:0dQXytY4gpPS0LaCFF4FglOhc53jnpFq4RnuEJupJrw= github.com/toitware/ubjson v0.0.0-20231002110407-71c8fab5e607/go.mod h1:LodXJFCaPxCR6kxbtjumQpO85yzrRkFpseNkbd5nXjU= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= From badc76ff3b2f3bfe06ac5a777c0f502151223035 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 20 Sep 2024 10:49:21 +0200 Subject: [PATCH 3/6] Don't show usage if a program can't be compiled. (#555) --- cmd/jag/commands/run.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/jag/commands/run.go b/cmd/jag/commands/run.go index 49b4e3b..c3918a8 100644 --- a/cmd/jag/commands/run.go +++ b/cmd/jag/commands/run.go @@ -280,6 +280,7 @@ func sendCodeFromFile( // We assume the error has been printed. // Mark the command as silent to avoid printing the error twice. cmd.SilenceErrors = true + cmd.SilenceUsage = true return err } } @@ -381,6 +382,7 @@ func sendCodeFromFile( // We assume the error has been printed. // Mark the command as silent to avoid printing the error twice. cmd.SilenceErrors = true + cmd.SilenceUsage = true return err } @@ -389,6 +391,7 @@ func sendCodeFromFile( // We just printed the error. // Mark the command as silent to avoid printing the error twice. cmd.SilenceErrors = true + cmd.SilenceUsage = true return err } fmt.Printf("Success: Sent %dKB code to '%s'\n", len(b)/1024, device.Name) From 18dae10ee0ca6888f11205dcdc79cef1d9436766 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 20 Sep 2024 11:17:24 +0200 Subject: [PATCH 4/6] Make 'Device' an interface. (#548) --- cmd/jag/commands/container.go | 6 +- cmd/jag/commands/device.go | 203 ++++++++++++++++++++++++++++------ cmd/jag/commands/firmware.go | 14 +-- cmd/jag/commands/run.go | 12 +- cmd/jag/commands/scan.go | 38 +++---- cmd/jag/commands/util.go | 4 +- cmd/jag/commands/watch.go | 2 +- 7 files changed, 200 insertions(+), 79 deletions(-) diff --git a/cmd/jag/commands/container.go b/cmd/jag/commands/container.go index f3280d8..b8d0fa4 100644 --- a/cmd/jag/commands/container.go +++ b/cmd/jag/commands/container.go @@ -60,7 +60,7 @@ func ContainerListCmd() *cobra.Command { } // Compute the column lengths for all columns except for the last. - deviceNameLength := max(len("DEVICE"), len(device.Name)) + deviceNameLength := max(len("DEVICE"), len(device.Name())) idLength := len("IMAGE") for id := range containers { idLength = max(idLength, len(id)) @@ -68,7 +68,7 @@ func ContainerListCmd() *cobra.Command { fmt.Println(padded("DEVICE", deviceNameLength) + padded("IMAGE", idLength) + "NAME") for id, name := range containers { - fmt.Println(padded(device.Name, deviceNameLength) + padded(id, idLength) + name) + fmt.Println(padded(device.Name(), deviceNameLength) + padded(id, idLength) + name) } return nil }, @@ -182,7 +182,7 @@ func ContainerUninstallCmd() *cobra.Command { } name := args[0] - fmt.Printf("Uninstalling container '%s' on '%s' ...\n", name, device.Name) + fmt.Printf("Uninstalling container '%s' on '%s' ...\n", name, device.Name()) return device.ContainerUninstall(ctx, sdk, name) }, } diff --git a/cmd/jag/commands/device.go b/cmd/jag/commands/device.go index 7a13ee9..323f9e3 100644 --- a/cmd/jag/commands/device.go +++ b/cmd/jag/commands/device.go @@ -30,8 +30,30 @@ const ( JaguarCRC32Header = "X-Jaguar-CRC32" ) +type Device interface { + ID() string + Name() string + Chip() string + SDKVersion() string + WordSize() int + Address() string + Short() string + String() string + + SetID(string) + SetSDKVersion(string) + + Ping(ctx context.Context, sdk *SDK) bool + SendCode(ctx context.Context, sdk *SDK, request string, b []byte, headersMap map[string]string) error + ContainerList(ctx context.Context, sdk *SDK) (map[string]string, error) + ContainerUninstall(ctx context.Context, sdk *SDK, name string) error + UpdateFirmware(ctx context.Context, sdk *SDK, b []byte) error + + ToJson() map[string]interface{} +} + type Devices struct { - Devices []Device `mapstructure:"devices" yaml:"devices" json:"devices"` + Devices []Device } func (d Devices) Elements() []Short { @@ -42,54 +64,161 @@ func (d Devices) Elements() []Short { return res } -type Device struct { - ID string `mapstructure:"id" yaml:"id" json:"id"` - Name string `mapstructure:"name" yaml:"name" json:"name"` - Chip string `mapstructure:"chip" yaml:"chip" json:"chip"` - Address string `mapstructure:"address" yaml:"address" json:"address"` - SDKVersion string `mapstructure:"sdkVersion" yaml:"sdkVersion" json:"sdkVersion"` - WordSize int `mapstructure:"wordSize" yaml:"wordSize" json:"wordSize"` - Proxied bool `mapstructure:"proxied" yaml:"proxied" json:"proxied"` +type DeviceBase struct { + id string + name string + chip string + sdkVersion string + wordSize int + address string +} + +func (d DeviceBase) ID() string { + return d.id +} + +func (d DeviceBase) Name() string { + return d.name +} + +func (d DeviceBase) Chip() string { + return d.chip +} + +func (d DeviceBase) SDKVersion() string { + return d.sdkVersion +} + +func (d DeviceBase) WordSize() int { + return d.wordSize +} + +func (d DeviceBase) Address() string { + return d.address +} + +func (d DeviceBase) SetID(id string) { + d.id = id +} + +func (d DeviceBase) SetSDKVersion(version string) { + d.sdkVersion = version +} + +func (d DeviceBase) Short() string { + return d.Name() +} + +func (d DeviceBase) String() string { + return fmt.Sprintf("%s (address: %s, %d-bit)", d.Name(), d.Address(), d.WordSize()*8) } -func (d Device) String() string { +type DeviceNetwork struct { + DeviceBase + proxied bool +} + +func NewDeviceFromJson(data map[string]interface{}) (Device, error) { + return NewDeviceNetworkFromJson(data) +} + +func boolOr(data map[string]interface{}, key string, def bool) bool { + if val, ok := data[key].(bool); ok { + return val + } + // Viper converts all keys to lowercase, so we need to check for that as well. + key = strings.ToLower(key) + if val, ok := data[key].(bool); ok { + return val + } + return def +} + +func stringOr(data map[string]interface{}, key string, def string) string { + if val, ok := data[key].(string); ok { + return val + } + // Viper converts all keys to lowercase, so we need to check for that as well. + key = strings.ToLower(key) + if val, ok := data[key].(string); ok { + return val + } + return def +} + +func intOr(data map[string]interface{}, key string, def int) int { + if val, ok := data[key].(float64); ok { + return int(val) + } + // Viper converts all keys to lowercase, so we need to check for that as well. + key = strings.ToLower(key) + if val, ok := data[key].(float64); ok { + return int(val) + } + return def +} + +func NewDeviceNetworkFromJson(data map[string]interface{}) (*DeviceNetwork, error) { + // Print the data object for debugging: + return &DeviceNetwork{ + DeviceBase: DeviceBase{ + id: stringOr(data, "id", ""), + name: stringOr(data, "name", ""), + chip: stringOr(data, "chip", "esp32"), + sdkVersion: stringOr(data, "sdkVersion", ""), + wordSize: intOr(data, "wordSize", 4), + address: stringOr(data, "address", ""), + }, + proxied: boolOr(data, "proxied", false), + }, nil +} + +func (d DeviceNetwork) String() string { proxied := "" - if d.Proxied { + if d.proxied { proxied = ", proxied" } - return fmt.Sprintf("%s (address: %s, %d-bit%s)", d.Name, d.Address, d.WordSize*8, proxied) + return fmt.Sprintf("%s (address: %s, %d-bit%s)", d.Name(), d.Address(), d.WordSize()*8, proxied) } -func (d Device) Short() string { - return d.Name +func (d DeviceNetwork) ToJson() map[string]interface{} { + return map[string]interface{}{ + "id": d.ID(), + "name": d.Name(), + "chip": d.Chip(), + "sdkVersion": d.SDKVersion(), + "wordSize": d.WordSize(), + "address": d.Address(), + "proxied": d.proxied, + } } const ( pingTimeout = 3000 * time.Millisecond ) -func (d Device) newRequest(ctx context.Context, method string, path string, body io.Reader) (*http.Request, error) { +func (d DeviceNetwork) newRequest(ctx context.Context, method string, path string, body io.Reader) (*http.Request, error) { lanIp, err := getLanIp() if err != nil { return nil, err } // If the device is on the same machine (proxied) use "localhost" instead of the // public IP. This is more stable on Windows machines. - address := d.Address + address := d.Address() if strings.HasPrefix(address, "http://"+lanIp+":") { address = "http://localhost:" + strings.TrimPrefix(address, "http://"+lanIp+":") } return http.NewRequestWithContext(ctx, method, address+path, body) } -func (d Device) Ping(ctx context.Context, sdk *SDK) bool { +func (d DeviceNetwork) Ping(ctx context.Context, sdk *SDK) bool { ctx, cancel := context.WithTimeout(ctx, pingTimeout) defer cancel() req, err := d.newRequest(ctx, "GET", "/ping", nil) if err != nil { return false } - req.Header.Set(JaguarDeviceIDHeader, d.ID) + req.Header.Set(JaguarDeviceIDHeader, d.ID()) req.Header.Set(JaguarSDKVersionHeader, sdk.Version) res, err := http.DefaultClient.Do(req) if err != nil { @@ -100,12 +229,12 @@ func (d Device) Ping(ctx context.Context, sdk *SDK) bool { return res.StatusCode == http.StatusOK } -func (d Device) SendCode(ctx context.Context, sdk *SDK, request string, b []byte, headersMap map[string]string) error { +func (d DeviceNetwork) SendCode(ctx context.Context, sdk *SDK, request string, b []byte, headersMap map[string]string) error { req, err := d.newRequest(ctx, "PUT", request, bytes.NewReader(b)) if err != nil { return err } - req.Header.Set(JaguarDeviceIDHeader, d.ID) + req.Header.Set(JaguarDeviceIDHeader, d.ID()) req.Header.Set(JaguarSDKVersionHeader, sdk.Version) for key, value := range headersMap { req.Header.Set(key, value) @@ -126,12 +255,12 @@ func (d Device) SendCode(ctx context.Context, sdk *SDK, request string, b []byte return nil } -func (d Device) ContainerList(ctx context.Context, sdk *SDK) (map[string]string, error) { +func (d DeviceNetwork) ContainerList(ctx context.Context, sdk *SDK) (map[string]string, error) { req, err := d.newRequest(ctx, "GET", "/list", nil) if err != nil { return nil, err } - req.Header.Set(JaguarDeviceIDHeader, d.ID) + req.Header.Set(JaguarDeviceIDHeader, d.ID()) req.Header.Set(JaguarSDKVersionHeader, sdk.Version) res, err := http.DefaultClient.Do(req) if err != nil { @@ -156,12 +285,12 @@ func (d Device) ContainerList(ctx context.Context, sdk *SDK) (map[string]string, return unmarshalled, nil } -func (d Device) ContainerUninstall(ctx context.Context, sdk *SDK, name string) error { +func (d DeviceNetwork) ContainerUninstall(ctx context.Context, sdk *SDK, name string) error { req, err := d.newRequest(ctx, "PUT", "/uninstall", nil) if err != nil { return err } - req.Header.Set(JaguarDeviceIDHeader, d.ID) + req.Header.Set(JaguarDeviceIDHeader, d.ID()) req.Header.Set(JaguarSDKVersionHeader, sdk.Version) req.Header.Set(JaguarContainerNameHeader, name) res, err := http.DefaultClient.Do(req) @@ -232,14 +361,14 @@ func (p *ProgressReader) Read(buffer []byte) (n int, err error) { return copied, nil } -func (d Device) UpdateFirmware(ctx context.Context, sdk *SDK, b []byte) error { +func (d DeviceNetwork) UpdateFirmware(ctx context.Context, sdk *SDK, b []byte) error { var reader = NewProgressReader(b) req, err := d.newRequest(ctx, "PUT", "/firmware", reader) if err != nil { return err } req.ContentLength = int64(len(b)) - req.Header.Set(JaguarDeviceIDHeader, d.ID) + req.Header.Set(JaguarDeviceIDHeader, d.ID()) req.Header.Set(JaguarSDKVersionHeader, sdk.Version) defer fmt.Print("\n\n") res, err := http.DefaultClient.Do(req) @@ -255,21 +384,25 @@ func (d Device) UpdateFirmware(ctx context.Context, sdk *SDK, b []byte) error { return nil } -func GetDevice(ctx context.Context, cfg *viper.Viper, sdk *SDK, checkPing bool, deviceSelect deviceSelect) (*Device, error) { +func GetDevice(ctx context.Context, cfg *viper.Viper, sdk *SDK, checkPing bool, deviceSelect deviceSelect) (Device, error) { manualPick := deviceSelect != nil if cfg.IsSet("device") && !manualPick { - var d Device - if err := cfg.UnmarshalKey("device", &d); err != nil { + var decoded map[string]interface{} + if err := cfg.UnmarshalKey("device", &decoded); err != nil { + return nil, err + } + d, err := NewDeviceFromJson(decoded) + if err != nil { return nil, err } if checkPing { if d.Ping(ctx, sdk) { - return &d, nil + return d, nil } - deviceSelect = deviceIDSelect(d.ID) - fmt.Printf("Failed to ping '%s'.\n", d.Name) + deviceSelect = deviceIDSelect(d.ID()) + fmt.Printf("Failed to ping '%s'.\n", d.Name()) } else { - return &d, nil + return d, nil } } @@ -279,9 +412,9 @@ func GetDevice(ctx context.Context, cfg *viper.Viper, sdk *SDK, checkPing bool, } if !manualPick { if autoSelected { - fmt.Printf("Found device '%s' again\n", d.Name) + fmt.Printf("Found device '%s' again\n", d.Name()) } - cfg.Set("device", d) + cfg.Set("device", d.ToJson()) if err := cfg.WriteConfig(); err != nil { return nil, err } diff --git a/cmd/jag/commands/firmware.go b/cmd/jag/commands/firmware.go index 52b4f97..26904c0 100644 --- a/cmd/jag/commands/firmware.go +++ b/cmd/jag/commands/firmware.go @@ -48,7 +48,7 @@ func FirmwareCmd() *cobra.Command { return err } - fmt.Printf("Device '%s' is running Toit SDK %s\n", device.Name, device.SDKVersion) + fmt.Printf("Device '%s' is running Toit SDK %s\n", device.Name(), device.SDKVersion()) return nil }, } @@ -97,7 +97,7 @@ func FirmwareUpdateCmd() *cobra.Command { } if chip == "auto" || chip == "" { - chip = device.Chip + chip = device.Chip() } wifiSSID, wifiPassword, err := getWifiCredentials(cmd) @@ -107,7 +107,7 @@ func FirmwareUpdateCmd() *cobra.Command { deviceOptions := DeviceOptions{ Id: newID, - Name: device.Name, + Name: device.Name(), Chip: chip, WifiSsid: wifiSSID, WifiPassword: wifiPassword, @@ -162,7 +162,7 @@ func FirmwareUpdateCmd() *cobra.Command { return err } - fmt.Printf("Updating firmware on '%s' to Toit SDK %s\n\n", device.Name, sdk.Version) + fmt.Printf("Updating firmware on '%s' to Toit SDK %s\n\n", device.Name(), sdk.Version) if err := device.UpdateFirmware(ctx, sdk, bin); err != nil { return err } @@ -171,9 +171,9 @@ func FirmwareUpdateCmd() *cobra.Command { // have to scan and ping before they can use the device after the firmware update. // If the update failed or if the device got a new IP address after rebooting, we // will have to ping again. - device.ID = newID - device.SDKVersion = sdk.Version - cfg.Set("device", device) + device.SetID(newID) + device.SetSDKVersion(sdk.Version) + cfg.Set("device", device.ToJson()) return cfg.WriteConfig() }, } diff --git a/cmd/jag/commands/run.go b/cmd/jag/commands/run.go index c3918a8..2a197e6 100644 --- a/cmd/jag/commands/run.go +++ b/cmd/jag/commands/run.go @@ -217,32 +217,32 @@ func runOnHost(ctx context.Context, cmd *cobra.Command, args []string, optimizat func RunFile( cmd *cobra.Command, - device *Device, + device Device, sdk *SDK, path string, defines map[string]interface{}, assetsPath string, optimizationLevel int) error { - fmt.Printf("Running '%s' on '%s' ...\n", path, device.Name) + fmt.Printf("Running '%s' on '%s' ...\n", path, device.Name()) return sendCodeFromFile(cmd, device, sdk, "/run", path, "", defines, assetsPath, optimizationLevel) } func InstallFile( cmd *cobra.Command, - device *Device, + device Device, sdk *SDK, name string, path string, defines map[string]interface{}, assetsPath string, optimizationLevel int) error { - fmt.Printf("Installing container '%s' from '%s' on '%s' ...\n", name, path, device.Name) + fmt.Printf("Installing container '%s' from '%s' on '%s' ...\n", name, path, device.Name()) return sendCodeFromFile(cmd, device, sdk, "/install", path, name, defines, assetsPath, optimizationLevel) } func sendCodeFromFile( cmd *cobra.Command, - device *Device, + device Device, sdk *SDK, request string, path string, @@ -394,7 +394,7 @@ func sendCodeFromFile( cmd.SilenceUsage = true return err } - fmt.Printf("Success: Sent %dKB code to '%s'\n", len(b)/1024, device.Name) + fmt.Printf("Success: Sent %dKB code to '%s'\n", len(b)/1024, device.Name()) return nil } diff --git a/cmd/jag/commands/scan.go b/cmd/jag/commands/scan.go index a7a0fbf..a3df3f3 100644 --- a/cmd/jag/commands/scan.go +++ b/cmd/jag/commands/scan.go @@ -98,7 +98,7 @@ func ScanCmd() *cobra.Command { } } - cfg.Set("device", device) + cfg.Set("device", device.ToJson()) return cfg.WriteConfig() }, } @@ -118,7 +118,7 @@ type deviceSelect interface { type deviceIDSelect string func (s deviceIDSelect) Match(d Device) bool { - return string(s) == d.ID + return string(s) == d.ID() } func (s deviceIDSelect) Address() string { @@ -132,7 +132,7 @@ func (s deviceIDSelect) String() string { type deviceNameSelect string func (s deviceNameSelect) Match(d Device) bool { - return string(s) == d.Name + return string(s) == d.Name() } func (s deviceNameSelect) Address() string { @@ -154,7 +154,7 @@ func (s deviceAddressSelect) Match(d Device) bool { if !strings.Contains(m, ":") { m += ":" } - return strings.HasPrefix(d.Address, m) + return strings.HasPrefix(d.Address(), m) } func (s deviceAddressSelect) Address() string { @@ -169,7 +169,7 @@ func (s deviceAddressSelect) String() string { return fmt.Sprintf("device with address: '%s'", string(s)) } -func scanAndPickDevice(ctx context.Context, scanTimeout time.Duration, port uint, autoSelect deviceSelect, manualPick bool) (*Device, bool, error) { +func scanAndPickDevice(ctx context.Context, scanTimeout time.Duration, port uint, autoSelect deviceSelect, manualPick bool) (Device, bool, error) { if autoSelect == nil { fmt.Println("Scanning ...") } else { @@ -188,7 +188,7 @@ func scanAndPickDevice(ctx context.Context, scanTimeout time.Duration, port uint if autoSelect != nil { for _, d := range devices { if autoSelect.Match(d) { - return &d, true, nil + return d, true, nil } } if manualPick { @@ -208,7 +208,7 @@ func scanAndPickDevice(ctx context.Context, scanTimeout time.Duration, port uint } res := devices[i] - return &res, false, nil + return res, false, nil } func scan(ctx context.Context, ds deviceSelect, port uint) ([]Device, error) { @@ -232,7 +232,7 @@ func scan(ctx context.Context, ds deviceSelect, port uint) ([]Device, error) { if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("got non-OK from device: %s", res.Status) } - dev, err := parseDevice(buf) + dev, err := parseDeviceNetwork(buf) if err != nil { return nil, fmt.Errorf("failed to parse identify. reason %w", err) } else if dev == nil { @@ -274,11 +274,11 @@ looping: return nil, err } - dev, err := parseDevice(buf[:n]) + dev, err := parseDeviceNetwork(buf[:n]) if err != nil { fmt.Println("Failed to parse identify", err) } else if dev != nil { - devices[dev.Address] = *dev + devices[dev.Address()] = *dev } } @@ -286,7 +286,7 @@ looping: for _, d := range devices { res = append(res, d) } - sort.Slice(res, func(i, j int) bool { return res[i].Name < res[j].Name }) + sort.Slice(res, func(i, j int) bool { return res[i].Name() < res[j].Name() }) return res, nil } @@ -295,9 +295,7 @@ type udpMessage struct { Payload map[string]interface{} `json:"payload"` } -func parseDevice(bytes []byte) (*Device, error) { - var device Device - +func parseDeviceNetwork(bytes []byte) (*DeviceNetwork, error) { var msg udpMessage if err := ubjson.Unmarshal(bytes, &msg); err != nil { if err := json.Unmarshal(bytes, &msg); err != nil { @@ -310,17 +308,7 @@ func parseDevice(bytes []byte) (*Device, error) { return nil, nil } - // We marshal the payload into JSON again, so we can use the reflection - // based support in encoding/json to fill in the fields in the device - // struct before returning it. - payload, err := json.Marshal(msg.Payload) - if err != nil { - return nil, fmt.Errorf("failed to re-marshal jaguar.identify: %s. reason: %w", string(bytes), err) - } - if err := json.Unmarshal(payload, &device); err != nil { - return nil, fmt.Errorf("failed to parse payload of jaguar.identify: %s. reason: %w", string(bytes), err) - } - return &device, nil + return NewDeviceNetworkFromJson(msg.Payload) } func isTimeoutError(err error) bool { diff --git a/cmd/jag/commands/util.go b/cmd/jag/commands/util.go index 2bd6f8d..fdc8d13 100644 --- a/cmd/jag/commands/util.go +++ b/cmd/jag/commands/util.go @@ -194,7 +194,7 @@ func (s *SDK) Compile(ctx context.Context, snapshot string, entrypoint string, o return nil } -func (s *SDK) Build(ctx context.Context, device *Device, snapshotPath string, assetsPath string) ([]byte, error) { +func (s *SDK) Build(ctx context.Context, device Device, snapshotPath string, assetsPath string) ([]byte, error) { image, err := os.CreateTemp("", "*.image") if err != nil { return nil, err @@ -203,7 +203,7 @@ func (s *SDK) Build(ctx context.Context, device *Device, snapshotPath string, as defer os.Remove(image.Name()) bits := "-m32" - if device.WordSize == 8 { + if device.WordSize() == 8 { bits = "-m64" } diff --git a/cmd/jag/commands/watch.go b/cmd/jag/commands/watch.go index 5b7837b..3bbcfce 100644 --- a/cmd/jag/commands/watch.go +++ b/cmd/jag/commands/watch.go @@ -181,7 +181,7 @@ func parseDependeniesToDirs(b []byte) []string { func onWatchChanges( cmd *cobra.Command, watcher *watcher, - device *Device, + device Device, sdk *SDK, entrypoint string, assetsPath string, From ba7f0ab5e71651b0115352015660b4ddf461babf Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 20 Sep 2024 11:18:13 +0200 Subject: [PATCH 5/6] Feedback. --- cmd/jag/commands/device_network.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/jag/commands/device_network.go b/cmd/jag/commands/device_network.go index 10febaa..fe65314 100644 --- a/cmd/jag/commands/device_network.go +++ b/cmd/jag/commands/device_network.go @@ -1,4 +1,4 @@ -// Copyright (C) 2021 Toitware ApS. All rights reserved. +// Copyright (C) 2024 Toitware ApS. All rights reserved. // Use of this source code is governed by an MIT-style license that can be // found in the LICENSE file. From 536140c88e1c793fd887ad1dfd9afb14e1a052ad Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 20 Sep 2024 11:22:43 +0200 Subject: [PATCH 6/6] Move network-device code into its own file. (#549) No functional change. --- cmd/jag/commands/device.go | 241 --------------------------- cmd/jag/commands/device_network.go | 252 +++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 241 deletions(-) create mode 100644 cmd/jag/commands/device_network.go diff --git a/cmd/jag/commands/device.go b/cmd/jag/commands/device.go index 323f9e3..98bae14 100644 --- a/cmd/jag/commands/device.go +++ b/cmd/jag/commands/device.go @@ -5,20 +5,11 @@ package commands import ( - "bytes" "context" - "encoding/json" "fmt" - "hash/crc32" - "io" - "net/http" - "os" "strings" - "time" - "unicode/utf8" "github.com/spf13/viper" - "github.com/toitware/ubjson" ) const ( @@ -113,15 +104,9 @@ func (d DeviceBase) String() string { return fmt.Sprintf("%s (address: %s, %d-bit)", d.Name(), d.Address(), d.WordSize()*8) } -type DeviceNetwork struct { - DeviceBase - proxied bool -} - func NewDeviceFromJson(data map[string]interface{}) (Device, error) { return NewDeviceNetworkFromJson(data) } - func boolOr(data map[string]interface{}, key string, def bool) bool { if val, ok := data[key].(bool); ok { return val @@ -158,232 +143,6 @@ func intOr(data map[string]interface{}, key string, def int) int { return def } -func NewDeviceNetworkFromJson(data map[string]interface{}) (*DeviceNetwork, error) { - // Print the data object for debugging: - return &DeviceNetwork{ - DeviceBase: DeviceBase{ - id: stringOr(data, "id", ""), - name: stringOr(data, "name", ""), - chip: stringOr(data, "chip", "esp32"), - sdkVersion: stringOr(data, "sdkVersion", ""), - wordSize: intOr(data, "wordSize", 4), - address: stringOr(data, "address", ""), - }, - proxied: boolOr(data, "proxied", false), - }, nil -} - -func (d DeviceNetwork) String() string { - proxied := "" - if d.proxied { - proxied = ", proxied" - } - return fmt.Sprintf("%s (address: %s, %d-bit%s)", d.Name(), d.Address(), d.WordSize()*8, proxied) -} - -func (d DeviceNetwork) ToJson() map[string]interface{} { - return map[string]interface{}{ - "id": d.ID(), - "name": d.Name(), - "chip": d.Chip(), - "sdkVersion": d.SDKVersion(), - "wordSize": d.WordSize(), - "address": d.Address(), - "proxied": d.proxied, - } -} - -const ( - pingTimeout = 3000 * time.Millisecond -) - -func (d DeviceNetwork) newRequest(ctx context.Context, method string, path string, body io.Reader) (*http.Request, error) { - lanIp, err := getLanIp() - if err != nil { - return nil, err - } - // If the device is on the same machine (proxied) use "localhost" instead of the - // public IP. This is more stable on Windows machines. - address := d.Address() - if strings.HasPrefix(address, "http://"+lanIp+":") { - address = "http://localhost:" + strings.TrimPrefix(address, "http://"+lanIp+":") - } - return http.NewRequestWithContext(ctx, method, address+path, body) -} - -func (d DeviceNetwork) Ping(ctx context.Context, sdk *SDK) bool { - ctx, cancel := context.WithTimeout(ctx, pingTimeout) - defer cancel() - req, err := d.newRequest(ctx, "GET", "/ping", nil) - if err != nil { - return false - } - req.Header.Set(JaguarDeviceIDHeader, d.ID()) - req.Header.Set(JaguarSDKVersionHeader, sdk.Version) - res, err := http.DefaultClient.Do(req) - if err != nil { - return false - } - - io.ReadAll(res.Body) // Avoid closing connection prematurely. - return res.StatusCode == http.StatusOK -} - -func (d DeviceNetwork) SendCode(ctx context.Context, sdk *SDK, request string, b []byte, headersMap map[string]string) error { - req, err := d.newRequest(ctx, "PUT", request, bytes.NewReader(b)) - if err != nil { - return err - } - req.Header.Set(JaguarDeviceIDHeader, d.ID()) - req.Header.Set(JaguarSDKVersionHeader, sdk.Version) - for key, value := range headersMap { - req.Header.Set(key, value) - } - // Set a crc32 header of the bytes. - req.Header.Set(JaguarCRC32Header, fmt.Sprintf("%d", crc32.ChecksumIEEE(b))) - - res, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - io.ReadAll(res.Body) // Avoid closing connection prematurely. - if res.StatusCode != http.StatusOK { - return fmt.Errorf("got non-OK from device: %s", res.Status) - } - - return nil -} - -func (d DeviceNetwork) ContainerList(ctx context.Context, sdk *SDK) (map[string]string, error) { - req, err := d.newRequest(ctx, "GET", "/list", nil) - if err != nil { - return nil, err - } - req.Header.Set(JaguarDeviceIDHeader, d.ID()) - req.Header.Set(JaguarSDKVersionHeader, sdk.Version) - res, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("got non-OK from device: %s", res.Status) - } - - var unmarshalled map[string]string - if err = ubjson.Unmarshal(body, &unmarshalled); err != nil { - if err = json.Unmarshal(body, &unmarshalled); err != nil { - return nil, err - } - } - - return unmarshalled, nil -} - -func (d DeviceNetwork) ContainerUninstall(ctx context.Context, sdk *SDK, name string) error { - req, err := d.newRequest(ctx, "PUT", "/uninstall", nil) - if err != nil { - return err - } - req.Header.Set(JaguarDeviceIDHeader, d.ID()) - req.Header.Set(JaguarSDKVersionHeader, sdk.Version) - req.Header.Set(JaguarContainerNameHeader, name) - res, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - io.ReadAll(res.Body) // Avoid closing connection prematurely. - if err != nil { - return err - } - if res.StatusCode != http.StatusOK { - return fmt.Errorf("got non-OK from device: %s", res.Status) - } - return nil -} - -// A Reader based on a byte array that prints a progress bar. -type ProgressReader struct { - b []byte - index int - spinState int -} - -func NewProgressReader(b []byte) *ProgressReader { - return &ProgressReader{b, 0, 0} -} - -func (p *ProgressReader) Read(buffer []byte) (n int, err error) { - if p.index == len(p.b) { - return 0, io.EOF - } - copied := copy(buffer, p.b[p.index:]) - p.index += copied - percent := (p.index * 100) / len(p.b) - fmt.Print("\r") - // The strings must contain characters with the same UTF-8 length so that - // they can be chopped up. The emoji generally are 4-byte characters. - // Braille are 3-byte characters, and or course ASCII is 1-byte characters. - spin := "⠁⠂⠄⡀⢀⠠⠐⠈" - done := "🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱" - todo := "--------------------------------------------------" - if os.PathSeparator == '\\' { // Windows. - spin = "/-\\|" - done = "################### Jaguar #######################" - } - - parts := utf8.RuneCountInString(done) - todoParts := utf8.RuneCountInString(todo) - if todoParts < parts { - parts = todoParts - } - spinStates := utf8.RuneCountInString(spin) - doneBytesPerPart := len(done) / parts - todoBytesPerPart := len(todo) / parts - spinBytesPerPart := len(spin) / spinStates - - pos := percent / (100 / parts) - p.spinState += spinBytesPerPart - if p.spinState == len(spin) { - p.spinState = 0 - } - spinChar := spin[p.spinState : p.spinState+spinBytesPerPart] - fmt.Printf(" %3d%% %4dk %s [", percent, p.index>>10, spinChar) - fmt.Print(done[len(done)-pos*doneBytesPerPart:]) - fmt.Print(todo[:len(todo)-pos*todoBytesPerPart]) - fmt.Print("] ") - return copied, nil -} - -func (d DeviceNetwork) UpdateFirmware(ctx context.Context, sdk *SDK, b []byte) error { - var reader = NewProgressReader(b) - req, err := d.newRequest(ctx, "PUT", "/firmware", reader) - if err != nil { - return err - } - req.ContentLength = int64(len(b)) - req.Header.Set(JaguarDeviceIDHeader, d.ID()) - req.Header.Set(JaguarSDKVersionHeader, sdk.Version) - defer fmt.Print("\n\n") - res, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - io.ReadAll(res.Body) // Avoid closing connection prematurely. - if res.StatusCode != http.StatusOK { - return fmt.Errorf("got non-OK from device: %s", res.Status) - } - - return nil -} - func GetDevice(ctx context.Context, cfg *viper.Viper, sdk *SDK, checkPing bool, deviceSelect deviceSelect) (Device, error) { manualPick := deviceSelect != nil if cfg.IsSet("device") && !manualPick { diff --git a/cmd/jag/commands/device_network.go b/cmd/jag/commands/device_network.go new file mode 100644 index 0000000..fe65314 --- /dev/null +++ b/cmd/jag/commands/device_network.go @@ -0,0 +1,252 @@ +// Copyright (C) 2024 Toitware ApS. All rights reserved. +// Use of this source code is governed by an MIT-style license that can be +// found in the LICENSE file. + +package commands + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "hash/crc32" + "io" + "net/http" + "os" + "strings" + "time" + "unicode/utf8" + + "github.com/toitware/ubjson" +) + +type DeviceNetwork struct { + DeviceBase + proxied bool +} + +func NewDeviceNetworkFromJson(data map[string]interface{}) (*DeviceNetwork, error) { + // Print the data object for debugging: + return &DeviceNetwork{ + DeviceBase: DeviceBase{ + id: stringOr(data, "id", ""), + name: stringOr(data, "name", ""), + chip: stringOr(data, "chip", "esp32"), + sdkVersion: stringOr(data, "sdkVersion", ""), + wordSize: intOr(data, "wordSize", 4), + address: stringOr(data, "address", ""), + }, + proxied: boolOr(data, "proxied", false), + }, nil +} + +func (d DeviceNetwork) String() string { + proxied := "" + if d.proxied { + proxied = ", proxied" + } + return fmt.Sprintf("%s (address: %s, %d-bit%s)", d.Name(), d.Address(), d.WordSize()*8, proxied) +} + +func (d DeviceNetwork) ToJson() map[string]interface{} { + return map[string]interface{}{ + "id": d.ID(), + "name": d.Name(), + "chip": d.Chip(), + "sdkVersion": d.SDKVersion(), + "wordSize": d.WordSize(), + "address": d.Address(), + "proxied": d.proxied, + } +} + +const ( + pingTimeout = 3000 * time.Millisecond +) + +func (d DeviceNetwork) newRequest(ctx context.Context, method string, path string, body io.Reader) (*http.Request, error) { + lanIp, err := getLanIp() + if err != nil { + return nil, err + } + // If the device is on the same machine (proxied) use "localhost" instead of the + // public IP. This is more stable on Windows machines. + address := d.Address() + if strings.HasPrefix(address, "http://"+lanIp+":") { + address = "http://localhost:" + strings.TrimPrefix(address, "http://"+lanIp+":") + } + return http.NewRequestWithContext(ctx, method, address+path, body) +} + +func (d DeviceNetwork) Ping(ctx context.Context, sdk *SDK) bool { + ctx, cancel := context.WithTimeout(ctx, pingTimeout) + defer cancel() + req, err := d.newRequest(ctx, "GET", "/ping", nil) + if err != nil { + return false + } + req.Header.Set(JaguarDeviceIDHeader, d.ID()) + req.Header.Set(JaguarSDKVersionHeader, sdk.Version) + res, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + + io.ReadAll(res.Body) // Avoid closing connection prematurely. + return res.StatusCode == http.StatusOK +} + +func (d DeviceNetwork) SendCode(ctx context.Context, sdk *SDK, request string, b []byte, headersMap map[string]string) error { + req, err := d.newRequest(ctx, "PUT", request, bytes.NewReader(b)) + if err != nil { + return err + } + req.Header.Set(JaguarDeviceIDHeader, d.ID()) + req.Header.Set(JaguarSDKVersionHeader, sdk.Version) + for key, value := range headersMap { + req.Header.Set(key, value) + } + // Set a crc32 header of the bytes. + req.Header.Set(JaguarCRC32Header, fmt.Sprintf("%d", crc32.ChecksumIEEE(b))) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + io.ReadAll(res.Body) // Avoid closing connection prematurely. + if res.StatusCode != http.StatusOK { + return fmt.Errorf("got non-OK from device: %s", res.Status) + } + + return nil +} + +func (d DeviceNetwork) ContainerList(ctx context.Context, sdk *SDK) (map[string]string, error) { + req, err := d.newRequest(ctx, "GET", "/list", nil) + if err != nil { + return nil, err + } + req.Header.Set(JaguarDeviceIDHeader, d.ID()) + req.Header.Set(JaguarSDKVersionHeader, sdk.Version) + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("got non-OK from device: %s", res.Status) + } + + var unmarshalled map[string]string + if err = ubjson.Unmarshal(body, &unmarshalled); err != nil { + if err = json.Unmarshal(body, &unmarshalled); err != nil { + return nil, err + } + } + + return unmarshalled, nil +} + +func (d DeviceNetwork) ContainerUninstall(ctx context.Context, sdk *SDK, name string) error { + req, err := d.newRequest(ctx, "PUT", "/uninstall", nil) + if err != nil { + return err + } + req.Header.Set(JaguarDeviceIDHeader, d.ID()) + req.Header.Set(JaguarSDKVersionHeader, sdk.Version) + req.Header.Set(JaguarContainerNameHeader, name) + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + io.ReadAll(res.Body) // Avoid closing connection prematurely. + if err != nil { + return err + } + if res.StatusCode != http.StatusOK { + return fmt.Errorf("got non-OK from device: %s", res.Status) + } + return nil +} + +// A Reader based on a byte array that prints a progress bar. +type ProgressReader struct { + b []byte + index int + spinState int +} + +func NewProgressReader(b []byte) *ProgressReader { + return &ProgressReader{b, 0, 0} +} + +func (p *ProgressReader) Read(buffer []byte) (n int, err error) { + if p.index == len(p.b) { + return 0, io.EOF + } + copied := copy(buffer, p.b[p.index:]) + p.index += copied + percent := (p.index * 100) / len(p.b) + fmt.Print("\r") + // The strings must contain characters with the same UTF-8 length so that + // they can be chopped up. The emoji generally are 4-byte characters. + // Braille are 3-byte characters, and or course ASCII is 1-byte characters. + spin := "⠁⠂⠄⡀⢀⠠⠐⠈" + done := "🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱🐱" + todo := "--------------------------------------------------" + if os.PathSeparator == '\\' { // Windows. + spin = "/-\\|" + done = "################### Jaguar #######################" + } + + parts := utf8.RuneCountInString(done) + todoParts := utf8.RuneCountInString(todo) + if todoParts < parts { + parts = todoParts + } + spinStates := utf8.RuneCountInString(spin) + doneBytesPerPart := len(done) / parts + todoBytesPerPart := len(todo) / parts + spinBytesPerPart := len(spin) / spinStates + + pos := percent / (100 / parts) + p.spinState += spinBytesPerPart + if p.spinState == len(spin) { + p.spinState = 0 + } + spinChar := spin[p.spinState : p.spinState+spinBytesPerPart] + fmt.Printf(" %3d%% %4dk %s [", percent, p.index>>10, spinChar) + fmt.Print(done[len(done)-pos*doneBytesPerPart:]) + fmt.Print(todo[:len(todo)-pos*todoBytesPerPart]) + fmt.Print("] ") + return copied, nil +} + +func (d DeviceNetwork) UpdateFirmware(ctx context.Context, sdk *SDK, b []byte) error { + var reader = NewProgressReader(b) + req, err := d.newRequest(ctx, "PUT", "/firmware", reader) + if err != nil { + return err + } + req.ContentLength = int64(len(b)) + req.Header.Set(JaguarDeviceIDHeader, d.ID()) + req.Header.Set(JaguarSDKVersionHeader, sdk.Version) + defer fmt.Print("\n\n") + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + io.ReadAll(res.Body) // Avoid closing connection prematurely. + if res.StatusCode != http.StatusOK { + return fmt.Errorf("got non-OK from device: %s", res.Status) + } + + return nil +}