Skip to content
Merged
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
43 changes: 39 additions & 4 deletions plugins/glider-maimu/src/glider_maimu/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
MODES = (MODE_ON, MODE_OFF, MODE_PULSE)

DEFAULT_PERIOD_MS = 500
DEFAULT_OFF_MS = 0 # 0 = same as On: the original symmetric toggle
DEFAULT_DURATION_S = 10


Expand Down Expand Up @@ -58,15 +59,30 @@ class MaimuNode(HardwareNode):
"help": "On and Off latch; Pulse runs a train and stops on its own.",
},
{
# Still saved as period_ms so existing files open unchanged: with
# Off at 0 it is the original toggle period, i.e. on time = off time.
"key": "period_ms",
"label": "Period (ms)",
"label": "On (ms)",
"type": "int",
"default": DEFAULT_PERIOD_MS,
"min": 1,
"max": 3_600_000,
"help": (
"Pulse mode only. On/off toggle period in milliseconds -- a "
"period, not a frequency. 500 ms toggles about once a second."
"Pulse mode only. How long each pulse stays on. With Off at 0 "
"the light toggles every On ms. 20 Hz with 10 ms pulses: On 10, Off 40."
),
},
{
"key": "off_ms",
"label": "Off (ms)",
"type": "int",
"default": DEFAULT_OFF_MS,
"min": 0,
"max": 3_600_000,
"help": (
"Pulse mode only. Gap between pulses; 0 = same as On. An Off "
"different from On needs Maimu firmware 0.1.0+ -- older "
"firmware ignores the command and stays dark."
),
},
{
Expand Down Expand Up @@ -109,6 +125,7 @@ def __init__(self):
super().__init__()
self._mode = MODE_PULSE
self._period_ms = DEFAULT_PERIOD_MS
self._off_ms = DEFAULT_OFF_MS
self._duration_s = DEFAULT_DURATION_S

@property
Expand All @@ -130,6 +147,14 @@ def period_ms(self) -> int:
def period_ms(self, value: Any) -> None:
self._period_ms = int(value)

@property
def off_ms(self) -> int:
return self._off_ms

@off_ms.setter
def off_ms(self, value: Any) -> None:
self._off_ms = int(value)

@property
def duration_s(self) -> int:
return self._duration_s
Expand All @@ -140,7 +165,15 @@ def duration_s(self, value: Any) -> None:

async def hardware_operation(self) -> None:
"""Send the configured command, then trigger the exec output."""
if self._mode == MODE_PULSE:
off_ms = self._off_ms or self._period_ms
if self._mode == MODE_PULSE and off_ms != self._period_ms:
# "p<on_ms>,<off_ms>,<duration_s>" (firmware 0.1.0+). The 'p' makes
# older firmware ignore it rather than misread "10,40,10" as a
# 10 ms toggle for 40 s.
await self._device.execute_action(
"write", f"p{self._period_ms}", off_ms, self._duration_s
)
elif self._mode == MODE_PULSE:
await self._device.execute_action(MODE_PULSE, self._period_ms, self._duration_s)
else:
await self._device.execute_action(self._mode)
Expand All @@ -151,6 +184,7 @@ def get_state(self) -> dict[str, Any]:
state = super().get_state()
state["mode"] = self._mode
state["period_ms"] = self._period_ms
state["off_ms"] = self._off_ms
state["duration_s"] = self._duration_s
return state

Expand All @@ -162,4 +196,5 @@ def set_state(self, state: dict[str, Any]) -> None:
mode = str(state.get("mode", MODE_PULSE)).strip().lower()
self._mode = mode if mode in MODES else MODE_PULSE
self._period_ms = int(state.get("period_ms", DEFAULT_PERIOD_MS))
self._off_ms = int(state.get("off_ms", DEFAULT_OFF_MS))
self._duration_s = int(state.get("duration_s", DEFAULT_DURATION_S))
27 changes: 27 additions & 0 deletions plugins/glider-maimu/tests/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ async def test_pulse_passes_period_and_duration():
assert node._device.calls == [("pulse", (250, 5))]


async def test_off_zero_keeps_the_original_toggle():
node = _node(mode="pulse", period_ms=25, duration_s=5)
node.off_ms = 0
await node.hardware_operation()
assert node._device.calls == [("pulse", (25, 5))]


async def test_off_sends_the_duty_cycle_command():
"""10 ms on / 40 ms off is 20 Hz; firmware 0.1.0 parses "p<on>,<off>,<s>"."""
node = _node(mode="pulse", period_ms=10, duration_s=10)
node.off_ms = 40
await node.hardware_operation()
assert node._device.calls == [("write", ("p10", 40, 10))]


def test_off_round_trips_and_old_files_default_to_symmetric():
node = _node(mode="pulse", period_ms=10, duration_s=10)
node.off_ms = 40
restored = MaimuNode()
restored.set_state(node.get_state())
assert (restored.period_ms, restored.off_ms) == (10, 40)

old_file = MaimuNode()
old_file.set_state({"mode": "pulse", "period_ms": 25, "duration_s": 5})
assert old_file.off_ms == 0


async def test_on_calls_bare_action():
node = _node(mode="on")
await node.hardware_operation()
Expand Down