Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 6 additions & 17 deletions public/docs/fleet/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ $schema: https://toit.io/schemas/artemis/pod-specification/v1.json
name: my-pod
sdk-version: SDK-VERSION
artemis-version: ARTEMIS-VERSION
max-offline: 0s
max-offline: 5m
connections:
- type: wifi
ssid: YOUR WIFI SSID
Expand Down Expand Up @@ -263,15 +263,11 @@ these changes are lost at the next firmware upgrade.
Devices managed by Artemis need to connect to the cloud to receive updated information.
The frequency at which they synchronize can be configured by the user.

Devices that connect frequently or all the time are easy
to interact with and manage, but they spend a lot of power on staying connected
all the time. The pod specification in the example `my-pod.yaml` does not specify
how often to connect, so Artemis assumes that you want an interactive device.

If you don't need your device to stay connected all the time, you can
give it a 'max-offline' setting. This tells the Artemis service that it is
okay to be offline for 5m, 1h30m, or 24h without necessarily connecting to the
Internet.
Devices that connect frequently are easy to interact with and manage, but they
spend more power and place more load on the broker. The optional `max-offline`
setting tells the Artemis service that it is okay to be offline for 5m, 1h30m,
or 24h without necessarily connecting to the Internet. It defaults to five
minutes.

You can set this through:

Expand All @@ -282,13 +278,6 @@ artemis device set-max-offline 1m19s
If you monitor the output of your device, you'll see that the device goes
to sleep between its cloud synchronizations.

You can go back to the original setting, where the device tries to
stay online all the time by giving it a 'max-offline' setting of 0s:

``` sh
artemis device set-max-offline 0s
```

### Installing code

Artemis makes it easy to install and uninstall new code on your devices.
Expand Down
6 changes: 3 additions & 3 deletions public/docs/fleet/pods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ $schema: https://toit.io/schemas/artemis/pod-specification/v1.json
name: example
sdk-version: SDK-VERSION
artemis-version: ARTEMIS-VERSION
max-offline: 0s
max-offline: 5m
connections:
- type: wifi
ssid: YOUR-WIFI-SSID
Expand All @@ -129,8 +129,8 @@ The two version entries are for the SDK and Artemis versions. Be aware that not
all combinations of those are supported. Use `artemis sdk list` to see the
valid combinations.

The `max-offline` entry is optional and defaults to 0s. Use it to control for how
long your device is allowed to stay offline.
The optional `max-offline` entry controls how long your device is allowed to
stay offline. It must be greater than zero and defaults to five minutes.

The `connections` section contains a prioritized list of ways to connect to the
Internet. You can have multiple `wifi` entries and Artemis will attempt to
Expand Down
7 changes: 3 additions & 4 deletions public/docs/fleet/reliability.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ guards against the Artemis service itself getting into a bad state.
## Max offline

The frequency at which Artemis contacts the broker can be configured using the
`max-offline` setting in the pod specification. A value of `0s` means that the
device stays connected to the broker and polls it continuously (but at most
every 20 seconds). A value of `1h` means that the device can be offline for
up to an hour before it tries to reconnect.
`max-offline` setting in the pod specification. A value of `1h` means that the
device can be offline for up to an hour before it tries to reconnect. The value
must be greater than zero.

Here is a pod specification with a `max-offline` value of `1h`:

Expand Down
2 changes: 1 addition & 1 deletion public/examples/specification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ $schema: https://toit.io/schemas/artemis/pod-specification/v1.json
name: example
sdk-version: SDK-VERSION
artemis-version: ARTEMIS-VERSION
max-offline: 0s
max-offline: 5m
connections:
- type: wifi
ssid: YOUR-WIFI-SSID
Expand Down
2 changes: 1 addition & 1 deletion public/schemas/pod-specification/v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"type": "string"
},
"max-offline": {
"description": "Maximum duration before Artemis should attempt to synchronize. For example '30m'.",
"description": "Strictly positive maximum duration before Artemis should attempt to synchronize. Defaults to '5m'.",
"type": "string"
},
"extends": {
Expand Down
11 changes: 2 additions & 9 deletions src/cli/broker.toit
Original file line number Diff line number Diff line change
Expand Up @@ -818,10 +818,7 @@ class Broker:
throw "No known firmware information for device."
new-goal := device.goal or device.reported-state-firmware
cli_.ui.emit --info "Setting max-offline to $(Duration --s=max-offline-seconds)."
if max-offline-seconds > 0:
new-goal["max-offline"] = max-offline-seconds
else:
new-goal.remove "max-offline"
new-goal["max-offline"] = max-offline-seconds
new-goal

static has-implicit-network_ chip-family/string -> bool:
Expand Down Expand Up @@ -866,12 +863,8 @@ class Broker:
"sdk-version": sdk-version,
}

// Add the max-offline setting if is non-zero. The device service
// handles the absence of the max-offline setting differently, so
// we cannot just add zero seconds to the config. This matches what
// we do in $config_set_max_offline.
max-offline-seconds := specification.max-offline-seconds
if max-offline-seconds > 0: device-config["max-offline"] = max-offline-seconds
device-config["max-offline"] = max-offline-seconds

if specification.connections.is-empty and not has-implicit-network_ envelope-chip-family:
cli_.ui.emit --warning "No network connections configured."
Expand Down
2 changes: 2 additions & 0 deletions src/cli/cmds/device.toit
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ set-max-offline invocation/Invocation:
duration := parse-duration max-offline --if-error=:
ui.abort "Invalid max-offline duration: $max-offline."
duration.in-s
if max-offline-seconds <= 0:
ui.abort "Max-offline must be greater than zero."

fleet.broker.config-set-max-offline --device-id=device.id
--max-offline-seconds=max-offline-seconds
Expand Down
2 changes: 1 addition & 1 deletion src/cli/cmds/doc.toit
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ SPECIFICATION-FORMAT-HELP ::= """
'max-offline': optional. The duration the device can be offline before it
attempts to connect to the broker to sync. Expressed as
string of the form '1h2m3s' or '1h 2m 3s'.
If no value is specified, the default is '0s'.
The duration must be greater than zero and defaults to '5m'.
'connections': a list of connections, each of which must be a
connection object. At least one connection must be provided.
See below for the format of a connection object.
Expand Down
9 changes: 4 additions & 5 deletions src/cli/fleet.toit
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ..shared.scope show Scope
import .utils.names
import .server-config
import ..shared.json-diff
import ..shared.device-config show DEFAULT-MAX-OFFLINE

DEFAULT-GROUP ::= "default"

Expand Down Expand Up @@ -870,11 +871,9 @@ class FleetWithDevices extends Fleet:
else:
is-updated = json-equals (current-state or firmware-state) goal
max-offline-s/int? := (current-state or firmware-state).get "max-offline"
// If the device has no max_offline, we assume it's 20 seconds.
// TODO(florian): handle this better.
if not max-offline-s:
max-offline-s = 20
max-offline := Duration --s=max-offline-s
max-offline := max-offline-s and max-offline-s > 0
? Duration --s=max-offline-s
: DEFAULT-MAX-OFFLINE

missed-checkins/int := ?
if not get-state-events or get-state-events.is-empty:
Expand Down
7 changes: 5 additions & 2 deletions src/cli/pod-specification.toit
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import .server-config
import .utils
import .git

import ..shared.device-config show DEFAULT-MAX-OFFLINE
import ..shared.version show SDK-VERSION ARTEMIS-VERSION

JSON-SCHEMA ::= "https://toit.io/schemas/artemis/pod-specification/v1.json"
Expand Down Expand Up @@ -372,8 +373,10 @@ class PodSpecification:
json-connection-info.warn-unused
connection

max-offline := json-map.get-optional-duration "max-offline"
max-offline-seconds = max-offline ? max-offline.in-s : 0
max-offline := json-map.get-optional-duration "max-offline" or DEFAULT-MAX-OFFLINE
if max-offline <= Duration.ZERO:
format-error_ "Entry max-offline in pod specification must be positive."
max-offline-seconds = max-offline.in-s

json-map.warn-unused
validate_
Expand Down
10 changes: 7 additions & 3 deletions src/service/device.toit
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import .utils show deep-copy
import .periodic-network-request show PeriodicNetworkRequest // For toitdoc.
import ..shared.json-diff show json-equals Modification
import .storage
import ..shared.device-config show DEFAULT-MAX-OFFLINE

/**
A representation of the device we are running on.
Expand Down Expand Up @@ -112,9 +113,10 @@ class Device:
/**
The current max-offline as a Duration.
*/
max-offline -> Duration?:
max-offline-s/int? := current-state.get "max-offline"
if not max-offline-s: return null
max-offline -> Duration:
max-offline-s := current-state.get "max-offline"
if max-offline-s is not int or max-offline-s <= 0:
return DEFAULT-MAX-OFFLINE
return Duration --s=max-offline-s

/**
Expand All @@ -129,6 +131,8 @@ class Device:

/**
Sets the max-offline of the current state.

A null or non-positive value restores the compatibility default.
*/
state-set-max-offline new-max-offline/Duration?:
state := current-state-modifiable_
Expand Down
34 changes: 14 additions & 20 deletions src/service/synchronize.toit
Original file line number Diff line number Diff line change
Expand Up @@ -290,17 +290,13 @@ class SynchronizeJob extends TaskJob:
if control-level-offline_ > 0: return null
if control-level-online_ > 0: return now
max-offline := device_.max-offline
schedule/JobTime := ?
if max-offline:
// Allow the device to connect more often if we're having
// trouble synchronzing. This is particularly welcome on
// devices with a high max-offline setting (multiple hours).
status := determine-status_
if status > STATUS-YELLOW:
max-offline /= (status == STATUS-RED) ? 4 : 2
schedule = last + (max max-offline OFFLINE-MINIMUM)
else:
schedule = last + OFFLINE-MINIMUM
// Allow the device to connect more often if we're having
// trouble synchronzing. This is particularly welcome on
// devices with a high max-offline setting (multiple hours).
status := determine-status_
if status > STATUS-YELLOW:
max-offline /= (status == STATUS-RED) ? 4 : 2
schedule := last + (max max-offline OFFLINE-MINIMUM)
if now < schedule:
// If we're not going to schedule the synchronization
// job now, we allow running all other jobs.
Expand Down Expand Up @@ -472,7 +468,7 @@ class SynchronizeJob extends TaskJob:
// We are now synchronized. We cannot get here in safe mode, because
// we reboot just after synchronizing in that case.
assert: not safe-mode_
if device_.max-offline and control-level-online_ == 0: return true
if control-level-online_ == 0: return true
finally:
with-timeout TIMEOUT-BROKER-CLOSE: broker-connection.close

Expand Down Expand Up @@ -560,7 +556,7 @@ class SynchronizeJob extends TaskJob:
tags = {"safe-mode": true}
if state == STATE-SYNCHRONIZED:
max-offline := device_.max-offline
if max-offline and control-level-online_ == 0:
if control-level-online_ == 0:
tags = tags or {:}
tags["max-offline"] = max-offline
logger_.info STATE-SUCCESS[state] --tags=tags
Expand Down Expand Up @@ -689,12 +685,10 @@ class SynchronizeJob extends TaskJob:
else:
return STATUS-RED

static compute-status-limit-us_ max-offline/Duration? -> int:
static compute-status-limit-us_ max-offline/Duration -> int:
// Compute the number of time units that correspond to
// the max-offline setting by using ceiling division.
max-offline-units := max-offline
? 1 + (max-offline.in-us - 1) / STATUS-LIMIT-UNIT-US
: 1
max-offline-units := 1 + (max-offline.in-us - 1) / STATUS-LIMIT-UNIT-US
// Convert the units back to a number of microseconds and
// derive the limit from that and the number of attempts
// between status changes.
Expand All @@ -709,10 +703,10 @@ class SynchronizeJob extends TaskJob:
some time. It is a redundant safety mechanism, as there is
already a reboot strategy implemented.
*/
static start-watchdog_ watchdog/Watchdog? max-offline/Duration? -> none:
static start-watchdog_ watchdog/Watchdog? max-offline/Duration -> none:
if not watchdog: return
// TODO(florian): make this configurable?
max-watchdog-offline := max-offline ? max-offline * 5 : Duration.ZERO
max-watchdog-offline := max-offline * 5
max-watchdog-offline = max max-watchdog-offline (Duration --h=2)

watchdog.start --s=max-watchdog-offline.in-s
Expand Down Expand Up @@ -865,7 +859,7 @@ class SynchronizeJob extends TaskJob:
handle-set-max-offline_ value/any -> none:
max-offline := (value is int) ? Duration --s=value : null
device_.state-set-max-offline max-offline
status-limit-us_ = compute-status-limit-us_ max-offline
status-limit-us_ = compute-status-limit-us_ device_.max-offline

handle-firmware-update_ broker-connection/BrokerConnection new/string -> none:
storage_.ram-store RAM-FIRMWARE-IS-CLEAN-KEY null // Not necessarily clean anymore.
Expand Down
4 changes: 4 additions & 0 deletions src/shared/device-config.toit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (C) 2026 Toitware ApS. All rights reserved.

/** Default synchronization interval for configurations without max-offline. */
DEFAULT-MAX-OFFLINE ::= Duration --m=5
10 changes: 10 additions & 0 deletions tests/cmd-max-offline-test.toit
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import .utils
import artemis.service.synchronize show SynchronizeJob
import expect show expect

main args:
with-tester --args=args: | tester/Tester |
Expand All @@ -23,6 +24,15 @@ main args:
]
tester.run ["fleet", "add-existing-device", "--fleet-root", tester.tmp-dir, "$device.device-id"]

failure := tester.run --expect-exit-1 [
"--fleet-root", tester.tmp-dir,
"device",
"set-max-offline",
"--device", "$device.device-id",
"0s",
]
expect: failure.contains "Max-offline must be greater than zero."

tester.run [
"--fleet-root", tester.tmp-dir,
"device",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
├──────────────────────────────────────┼───────────────┼───────────────────────────────────────────────┼──────────┼──────────┼─────────────────┼───────────┼─────────┼─────────────┤
│ -={|~~~~~~~never-started~~~~~~~~|}=- never-started x ? never new1 │
│ -={|~~~~~~~~device-new1~~~~~~~~~|}=- device-new1 new1-pod#1 auto-tag,latest now new1 │
│ -={|~~~~~~~~~~initial1~~~~~~~~~~|}=- initial1 new1-pod#1 auto-tag,latest now test-broker │
│ -={|~~~~~~~~~~initial2~~~~~~~~~~|}=- initial2 new1-pod#1 auto-tag,latest now test-broker │
│ -={|~~~~~~~~~~initial1~~~~~~~~~~|}=- initial1 new1-pod#1 auto-tag,latest x now test-broker │
│ -={|~~~~~~~~~~initial2~~~~~~~~~~|}=- initial2 new1-pod#1 auto-tag,latest x now test-broker │
└──────────────────────────────────────┴───────────────┴───────────────────────────────────────────────┴──────────┴──────────┴─────────────────┴───────────┴─────────┴─────────────┘
1 change: 1 addition & 0 deletions tests/host-recovery-test-slow.toit
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ main args:
"host-recovery-source.toit": source,
}
--pod-spec={
"max-offline": "1s",
"containers": {
"recovery": {
"entrypoint": "host-recovery-source.toit",
Expand Down
13 changes: 11 additions & 2 deletions tests/parse-pod-specification-test.toit
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,17 @@ test-errors:

no-max-offline := new-valid
no-max-offline.remove "max-offline"
no-max-offline-spec := PodSpecification.from-json no-max-offline --path="ignored" --cli=TestCli
expect-equals 0 no-max-offline-spec.max-offline-seconds
no-max-offline-spec := PodSpecification.from-json
no-max-offline
--path="ignored"
--cli=TestCli
expect-equals 300 no-max-offline-spec.max-offline-seconds

zero-max-offline := new-valid
zero-max-offline["max-offline"] = "0s"
expect-format-error
"Entry max-offline in pod specification must be positive."
zero-max-offline

no-connections := new-valid
no-connections.remove "connections"
Expand Down
4 changes: 4 additions & 0 deletions tests/utils.toit
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,10 @@ class TestFleet:
--fleet=this
--gold-name=gold-name
--format=format
// Integration tests need devices to pick up changes made after their
// initial synchronization. Use a short, positive interval; the
// synchronize service applies its minimum interval on top of this.
--pod-spec={"max-offline": "1s"}

create-host-device name/string --start/bool -> TestDevicePipe:
tar-file := "$tester.tmp-dir/dev-$(name).tar"
Expand Down
Loading