Skip to content

feat(mysensors): set-sleep-duration, bootloader detect, and pre-OTA reboot - #81

Open
Eddine97 wants to merge 4 commits into
mycontroller-org:mainfrom
Eddine97:feature/set-sleep-duration-and-bootloader-detect
Open

Eddine97 wants to merge 4 commits into
mycontroller-org:mainfrom
Eddine97:feature/set-sleep-duration-and-bootloader-detect

Conversation

@Eddine97

@Eddine97 Eddine97 commented Oct 17, 2026 •

Copy link
Copy Markdown

Summary

Detect the bootloader kind from ST_FIRMWARE_CONFIG_REQUEST and persist it in
node labels (bootloader_kind, bootloader_version). This lets the controller
pick the right OTA strategy:

trigger I_REBOOT before serving OTA when needed:
    automatically for MYSBootloader nodes (any 1.x),
    manually via bootloader_ota_reboot=true for other cases,
    queue the reboot for sleeping nodes and deliver it on the next wake.

Add set-sleep-duration node action for smart-sleep nodes:

myc action node <alias> set-sleep-duration:206:32 <gw.node> (first use, with sensor ID)
myc action node <alias> set-sleep-duration:32 <gw.node> (subsequent, sensor ID remembered)
Queues the command for sleeping nodes and delivers it on the next wake.

These features are coupled: bootloader detection is what makes OTA reliable, and the queue introduced for set-sleep-duration is reused for the pré-OTA reboot of sleeping nodes.

Why bootloader detection

OTA needs a different strategy depending on two independent axes:

Always-on node Smart-sleep node
MYSBootloader (RF24) Send I_REBOOT, then serve CONFIG Queue I_REBOOT, deliver on next wake, then serve CONFIG
Protocol 3.1 / DualOptiboot + custom FOTA Serve CONFIG directly (no I_REBOOT) Wait for wake, then serve CONFIG (no I_REBOOT)

I_REBOOT here is a pré-OTA reboot, sent only to put MYSBootloader nodes
into bootloader mode before serving firmware; it is never sent for Protocol 3.1
nodes, whose application handles the OTA directly. Independently, every node
performs a post-OTA reset (bootloader or watchdog) once the new firmware is
written — that reset is initiated by the node, not by the controller.

Without knowing the bootloader kind, the controller cannot pick the right
strategy. On MYSBootloader nodes, OTA silently fails today: the node never
enters bootloader mode, so it never sends ST_FIRMWARE_CONFIG_REQUEST.

Detecting bootloader_kind from the first ST_FIRMWARE_CONFIG_REQUEST
lets the controller:

trigger the pré-OTA I_REBOOT automatically for MYSBootloader nodes (any 1.x),
skip the pré-OTA I_REBOOT for protocol 3.1 nodes,
queue the pré-OTA I_REBOOT for sleeping nodes and deliver it on the next wake.

The bootloader_ota_reboot=true label remains available as a manual override,
for cases where the user wants to force or disable the pré-OTA reboot regardless
of the detected kind.

Backward compatibility

Client.ExecuteNodeAction(action, ids) still works. New usage:
Client.ExecuteNodeAction(action, ids, payload). No server-side break.

ExecuteNodeAction gains an optional payload parameter, variadic for
backward compatibility.

Test plan

  • go test ./cmd/client/command/action/ ./plugin/gateway/provider/mysensors_v2/
  • myc action node <alias> set-sleep-duration:<sensor_id>:<pct> <gw.node> sends C_SET/V_PERCENTAGE.
  • Second call without sensor ID reuses the remembered one.
  • Sleeping node: check log line sleep command queued until next wake.
  • MYSBootloader node (auto-detected): check sent I_REBOOT before OTA and
    successful config handshake.
  • Protocol 3.1 node: check no reboot is sent.
  • Check detected bootloader log line with correct kind and version.

Notes

  • Queued commands and reboots are in-memory only, same as the FOTA firmware cache.
  • The OTA reboot for MYSBootloader nodes is now automatic on first detection.
    This is a behavior change for users who previously relied on the OTA being
    passive. The bootloader_ota_reboot=false label does not currently disable
    the automatic reboot; if this is desired, a follow-up PR can add an opt-out
    label.

@Eddine97
Eddine97 marked this pull request as ready for review October 17, 2026 08:42
@jkandasa

Copy link
Copy Markdown
Member

Hi @Eddine97 Thanks for your PR! The summary is helpful, but I feel some context is still missing.

Can you please provide a bit more detail about the PR, particularly the current challenge it addresses and how this change solves the problem? This would help us better understand the motivation behind the changes.

pendingRebootCount is exposed for tests but was not exercised by any
test, which golangci-lint flagged as unused. Add a minimal test that
covers the empty, add, remove, and cleanup paths.
@Eddine97

Copy link
Copy Markdown
Author

Thanks for the review. The missing context is the OTA strategy itself.

The problem

MySensors has two OTA mechanisms that need opposite controller behaviors,
and they use different boot message formats:

  1. Online OTA (Protocol 3.1 / DualOptiboot + custom FOTA). The node's
    application stays alive and drives the OTA. The controller just serves
    the firmware when asked. No reboot needed. The ST_FIRMWARE_CONFIG_REQUEST
    uses the extended format: BLVersion is 3.1, and the message carries
    blockSize, img_commited, img_revision, img_build_num.

  2. Offline OTA (MYSBootloader over RF24). The application does not
    handle OTA. The bootloader does, but only if the node reboots into
    bootloader mode. On a running node, the application never sends
    ST_FIRMWARE_CONFIG_REQUEST, so the controller never starts serving.
    OTA silently fails today. The ST_FIRMWARE_CONFIG_REQUEST uses the
    legacy format: BLVersion is 1.x, and there is no extended field.

The controller had no way to know which kind of node it was talking to,
because it did not inspect the format of the config request. This PR
fixes that.

How it works

  1. Bootloader detection. The first ST_FIRMWARE_CONFIG_REQUEST
    contains the bootloader version in BLVersion. The controller reads
    the BLVersion and the message length to distinguish the two formats:

    • legacy format (BLVersion 1.x, short message) → mysbootloader,
    • extended format (BLVersion 3.1, longer message) → protocol_3_1.
      The kind and version are persisted in node labels.
  2. Pre-OTA reboot. For MYSBootloader nodes, the controller sends
    I_REBOOT before serving firmware. For Protocol 3.1 nodes, no reboot
    is sent. For sleeping nodes, the I_REBOOT is queued and delivered on
    the next wake.

  3. Queue for sleeping nodes. Reused from set-sleep-duration, which
    is the second feature of this PR. A command targeting a sleeping node
    is queued and delivered on wake.

This is why the two features are in the same PR: they share the queue.

What changes in the logs

Before:
RX I_PRE_SLEEP_NOTIFICATION
TX (nothing)

After:
RX I_PRE_SLEEP_NOTIFICATION
TX I_REBOOT (queued, delivered on next wake)
RX ST_FIRMWARE_CONFIG_REQUEST
TX ST_FIRMWARE_CONFIG_RESPONSE
... OTA proceeds ...

Tested in production

I have been running this patched version since early October, together
with #46 and #47. The OTA campaigns covered MYSBootloader nodes
(always-on and smart-sleep), Protocol 3.1 nodes, and mixed networks.
Before that, I used MYSController (tekka's tool) for my MYSBootloader nodes.

This PR is part of a broader effort to bring the most useful MYSController
features into MyController.org.

Backward compatibility

Client.ExecuteNodeAction(action, ids) still works. New usage:
Client.ExecuteNodeAction(action, ids, payload). Protocol 3.1 nodes are
unaffected.

Notes

  • Queued commands and reboots are in-memory only, same as the FOTA cache.
  • The OTA reboot for MYSBootloader nodes is now automatic on first
    detection. This is a behavior change for users who relied on OTA being
    passive.
  • bootloader_ota_reboot=false does not currently disable the automatic
    reboot; a follow-up PR can add an opt-out label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants