Skip to content

Commit 2c728d7

Browse files
authored
fix: correct Blynclight BitField offsets and move state policy to _on (#708)
OffBit, DimBit, FlashBit, and SpeedField had bit offsets one higher than correct. Worked by accident due to the Word layout, but the off-by-one was wrong per the protocol spec. Moves off/dim/flash state management from __bytes__ (a serialization method with side effects) into _on where it belongs. The off bit is now set when _on receives a black color, and cleared when lit. Flash and dim are cleared on turn-off. __bytes__ is now a pure serializer with no side effects. Tests updated to exercise state through _on instead of bytes(). Relates to: busylight-core #61 (Blynclight off-bit state) Dispatch #75 Authored by Jny
1 parent 2697ad9 commit 2c728d7

3 files changed

Lines changed: 42 additions & 37 deletions

File tree

packages/busylight-core/src/busylight_core/vendors/embrava/blynclight_base.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ def state(self) -> "BlynclightState":
2020

2121
def __bytes__(self) -> bytes:
2222
"""Return the device state as bytes for USB communication."""
23-
if not self.is_lit:
24-
self.state.off = True
25-
self.state.flash = False
26-
self.state.dim = False
27-
2823
return bytes([0, *bytes(self.state), 0xFF, 0x22])
2924

3025
@property
@@ -45,6 +40,12 @@ def _on(self, color: tuple[int, int, int], led: int = 0) -> None:
4540
"""
4641
with self.batch_update():
4742
self.color = color
43+
if not self.is_lit:
44+
self.state.off = True
45+
self.state.dim = False
46+
self.state.flash = False
47+
else:
48+
self.state.off = False
4849

4950
def dim(self) -> None:
5051
"""Dim the current light color."""

packages/busylight-core/src/busylight_core/vendors/embrava/implementation/blynclight_fields.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,28 @@ class OffBit(BitField):
3333
"""1-bit field to turn light off, clear to turn light on."""
3434

3535
def __init__(self) -> None:
36-
super().__init__(17, 1)
36+
super().__init__(16, 1)
3737

3838

3939
class DimBit(BitField):
4040
"""1-bit field to dim light, clear to brighten light."""
4141

4242
def __init__(self) -> None:
43-
super().__init__(18, 1)
43+
super().__init__(17, 1)
4444

4545

4646
class FlashBit(BitField):
4747
"""1-bit field to flash light, clear to stop flashing."""
4848

4949
def __init__(self) -> None:
50-
super().__init__(19, 1)
50+
super().__init__(18, 1)
5151

5252

5353
class SpeedField(BitField):
5454
"""3-bit field to set flash speed: 1=slow, 2=medium, 4=fast."""
5555

5656
def __init__(self) -> None:
57-
super().__init__(20, 3)
57+
super().__init__(19, 3)
5858

5959

6060
class RepeatBit(BitField):

packages/busylight-core/tests/test_vendor_embrava_blynclight.py

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -88,45 +88,48 @@ def test_bytes_conversion_light_on(self) -> None:
8888
assert len(result) == 9 # Expected struct size
8989
assert blynclight.state.off == 0 # Should set off=False when lit
9090

91-
def test_bytes_conversion_light_off(self) -> None:
92-
"""Test __bytes__ method when light is off."""
91+
def test_on_sets_off_bit_when_color_is_black(self) -> None:
92+
"""Test on() sets off=True when color is black (not lit)."""
9393
mock_hardware = create_mock_blynclight_hardware()
9494

95-
with patch.object(mock_hardware, "acquire"), patch.object(Blynclight, "reset"):
95+
with (
96+
patch.object(mock_hardware, "acquire"),
97+
patch.object(Blynclight, "reset"),
98+
patch.object(Blynclight, "update"),
99+
):
96100
blynclight = Blynclight(mock_hardware)
97-
blynclight.color = (0, 0, 0) # Set RGB to zero - this makes is_lit False
98-
99-
result = bytes(blynclight)
101+
blynclight.on((0, 0, 0)) # Black color - not lit
100102

101-
assert isinstance(result, bytes)
102-
assert blynclight.state.off == 1 # Should set off=True when not lit
103+
assert blynclight.state.off == 1
103104

104-
def test_bytes_flash_disabled_when_off(self) -> None:
105-
"""Test that flash is disabled when light is off."""
105+
def test_on_clears_flash_when_off(self) -> None:
106+
"""Test that on() clears flash when light is off."""
106107
mock_hardware = create_mock_blynclight_hardware()
107108

108-
with patch.object(mock_hardware, "acquire"), patch.object(Blynclight, "reset"):
109+
with (
110+
patch.object(mock_hardware, "acquire"),
111+
patch.object(Blynclight, "reset"),
112+
patch.object(Blynclight, "update"),
113+
):
109114
blynclight = Blynclight(mock_hardware)
110115
blynclight.state.flash = True
111-
blynclight.color = (0, 0, 0) # Set RGB to zero - this makes is_lit False
112-
113-
bytes(blynclight)
116+
blynclight.on((0, 0, 0)) # Black color - not lit
114117

115-
# Flash should be disabled when light is off
116118
assert blynclight.state.flash == 0
117119

118-
def test_bytes_dim_disabled_when_off(self) -> None:
119-
"""Test that dim is disabled when light is off."""
120+
def test_on_clears_dim_when_off(self) -> None:
121+
"""Test that on() clears dim when light is off."""
120122
mock_hardware = create_mock_blynclight_hardware()
121123

122-
with patch.object(mock_hardware, "acquire"), patch.object(Blynclight, "reset"):
124+
with (
125+
patch.object(mock_hardware, "acquire"),
126+
patch.object(Blynclight, "reset"),
127+
patch.object(Blynclight, "update"),
128+
):
123129
blynclight = Blynclight(mock_hardware)
124130
blynclight.state.dim = True
125-
blynclight.color = (0, 0, 0) # Set RGB to zero - this makes is_lit False
126-
127-
bytes(blynclight)
131+
blynclight.on((0, 0, 0)) # Black color - not lit
128132

129-
# Dim should be disabled when light is off
130133
assert blynclight.state.dim == 0
131134

132135
def test_bytes_flash_preserved_when_on(self) -> None:
@@ -326,17 +329,18 @@ def test_state_interactions(self) -> None:
326329
"""Test interactions between different state settings."""
327330
mock_hardware = create_mock_blynclight_hardware()
328331

329-
with patch.object(mock_hardware, "acquire"), patch.object(Blynclight, "reset"):
332+
with (
333+
patch.object(mock_hardware, "acquire"),
334+
patch.object(Blynclight, "reset"),
335+
patch.object(Blynclight, "update"),
336+
):
330337
blynclight = Blynclight(mock_hardware)
331338

332-
# Test flash and dim both set, then light turned off
339+
# Test flash and dim both set, then light turned off via on()
333340
blynclight.state.flash = True
334341
blynclight.state.dim = True
335-
blynclight.color = (0, 0, 0) # Set color to zero to make is_lit False
336-
337-
bytes(blynclight)
342+
blynclight.on((0, 0, 0)) # Turn off clears flash and dim
338343

339-
# Both should be disabled when light is off
340344
assert blynclight.state.flash == 0
341345
assert blynclight.state.dim == 0
342346

0 commit comments

Comments
 (0)