Skip to content

Commit b6b8b0f

Browse files
committed
feat: expose remaining 0xB6 scribble/effect options in CLI (numeric effect id, per-LED blink, enter_mode, extended variant)
- Gap 1: accept numeric effect id 0-8 in -C scribble <effect>; named effects still work; out-of-range/garbage -> parser.error - Gap 2: per-LED blink suffix syntax <value>[/blinkmode[/blinkspeed]]; per-token blink overrides global blink=/blinkspeed= defaults - Gap 3: enter=true|false spec token controls async_set_scribble enter_mode; default true - Gap 4: --variant 0|1|2 optparse option overrides --colorchange for ExtendedCustomEffect option byte; threaded through processCustomArgs and parseArgs call site - Dispatch: pass raw int effect id (not ScribbleEffect()) so unnamed ids 3/4/6/7 don't raise ValueError; pass enter_mode from parsed dict - 23 new TDD tests covering all four gaps (parser-level); all 287 tests pass
1 parent 618e054 commit b6b8b0f

2 files changed

Lines changed: 363 additions & 21 deletions

File tree

flux_led/fluxled.py

Lines changed: 113 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -546,8 +546,45 @@ def _parse_scribble_led_token(
546546
blink_mode: ScribbleBlinkMode,
547547
blink_speed: int,
548548
) -> ScribbleLED:
549-
"""Parse a single LED value token (without run-length prefix) into a ScribbleLED."""
550-
token_lower = token.lower()
549+
"""Parse a single LED value token into a ScribbleLED.
550+
551+
Accepts an optional per-token blink suffix using '/' as separator:
552+
``<value>[/<blinkmode>[/<blinkspeed>]]``
553+
554+
``<value>`` is a color name/hex/r,g,b, a white-level ``w<level>``, or
555+
``off``. ``<blinkmode>`` is ``none|slow|fast``; ``<blinkspeed>`` is
556+
0-100. A per-token blink suffix overrides the caller-supplied global
557+
blink_mode / blink_speed defaults.
558+
"""
559+
# Split off optional /blinkmode[/blinkspeed] suffix.
560+
# '/' cannot appear in color names (hex uses only hex digits; r,g,b uses
561+
# commas; color names are plain words), so splitting on '/' is unambiguous.
562+
parts = token.split("/")
563+
value_part = parts[0]
564+
if len(parts) > 1:
565+
# Per-token blink override present
566+
blink_part = parts[1].lower()
567+
if blink_part not in _SCRIBBLE_BLINK_NAMES:
568+
parser.error(
569+
f"Invalid blink mode '{parts[1]}' in token '{token}'. "
570+
f"Use none, slow, or fast."
571+
)
572+
raise SystemExit(1)
573+
blink_mode = _SCRIBBLE_BLINK_NAMES[blink_part]
574+
if len(parts) > 2:
575+
try:
576+
blink_speed = int(parts[2])
577+
except ValueError:
578+
parser.error(
579+
f"Invalid blink speed '{parts[2]}' in token '{token}'. "
580+
f"Must be an integer."
581+
)
582+
raise SystemExit(1)
583+
if len(parts) > 3:
584+
parser.error(f"Too many '/' separators in token '{token}'.")
585+
raise SystemExit(1)
586+
587+
token_lower = value_part.lower()
551588
if token_lower == "off":
552589
return ScribbleLED(rgb=None, white=None, blink_mode=blink_mode, blink_speed=blink_speed)
553590
# White-level token: 'w' followed by DIGITS ONLY (e.g. w50). A token like
@@ -557,10 +594,10 @@ def _parse_scribble_led_token(
557594
level = int(white_match.group(1))
558595
return ScribbleLED(rgb=None, white=level, blink_mode=blink_mode, blink_speed=blink_speed)
559596
# Color token
560-
c = utils.color_object_to_tuple(token)
597+
c = utils.color_object_to_tuple(value_part)
561598
if c is None or len(c) < 3:
562599
parser.error(
563-
f"Invalid color token '{token}'. Use a color name, hex value, or r,g,b triple."
600+
f"Invalid color token '{value_part}'. Use a color name, hex value, or r,g,b triple."
564601
)
565602
raise SystemExit(1)
566603
return ScribbleLED(rgb=(c[0], c[1], c[2]), white=None, blink_mode=blink_mode, blink_speed=blink_speed)
@@ -570,15 +607,32 @@ def _process_scribble_args(
570607
parser: OptionParser,
571608
args: Any,
572609
) -> dict[str, Any] | None:
573-
"""Parse scribble-mode args: ('scribble', <effect>, '<per-led-spec>')."""
610+
"""Parse scribble-mode args: ('scribble', <effect>, '<per-led-spec>').
611+
612+
``<effect>`` may be a named effect (static/flowing/twinkling/stars_wink/
613+
accumulate) OR a raw integer 0-8.
614+
"""
574615
effect_name = args[1].lower() if len(args) > 1 else "static"
575-
if effect_name not in _SCRIBBLE_EFFECT_NAMES:
576-
parser.error(
577-
f"Unknown scribble effect '{effect_name}'. "
578-
f"Valid: {', '.join(_SCRIBBLE_EFFECT_NAMES)}."
579-
)
580-
return None
581-
effect = _SCRIBBLE_EFFECT_NAMES[effect_name]
616+
if effect_name in _SCRIBBLE_EFFECT_NAMES:
617+
effect_id: int = _SCRIBBLE_EFFECT_NAMES[effect_name].value
618+
else:
619+
# Try numeric id
620+
try:
621+
effect_id = int(effect_name)
622+
except ValueError:
623+
parser.error(
624+
f"Unknown scribble effect '{effect_name}'. "
625+
f"Valid names: {', '.join(_SCRIBBLE_EFFECT_NAMES)}; "
626+
f"or a number 0-8."
627+
)
628+
return None
629+
if not 0 <= effect_id <= 8:
630+
parser.error(
631+
f"Scribble effect id {effect_id} out of range. "
632+
f"Valid names: {', '.join(_SCRIBBLE_EFFECT_NAMES)}; "
633+
f"or a number 0-8."
634+
)
635+
return None
582636

583637
spec_str = args[2].strip() if len(args) > 2 else ""
584638

@@ -588,6 +642,7 @@ def _process_scribble_args(
588642
direction = ExtendedCustomEffectDirection.LEFT_TO_RIGHT
589643
blink_mode = ScribbleBlinkMode.NONE
590644
blink_speed = 100
645+
enter_mode = True
591646

592647
# Split spec on whitespace; separate LED tokens from key=value tokens
593648
all_tokens = spec_str.split()
@@ -631,6 +686,14 @@ def _process_scribble_args(
631686
except ValueError:
632687
parser.error(f"Invalid blinkspeed value '{val}'. Must be an integer.")
633688
return None
689+
elif key == "enter":
690+
if val.lower() == "true":
691+
enter_mode = True
692+
elif val.lower() == "false":
693+
enter_mode = False
694+
else:
695+
parser.error(f"Invalid enter value '{val}'. Use true or false.")
696+
return None
634697
else:
635698
parser.error(f"Unknown scribble option '{key}'.")
636699
return None
@@ -655,11 +718,12 @@ def _process_scribble_args(
655718

656719
return {
657720
"mode": "scribble",
658-
"effect": effect.value,
721+
"effect": effect_id,
659722
"leds": leds,
660723
"direction": direction.value,
661724
"density": density,
662725
"speed": speed,
726+
"enter_mode": enter_mode,
663727
}
664728

665729

@@ -687,6 +751,7 @@ def processCustomArgs(
687751
density: int = 50,
688752
direction: str = "l2r",
689753
colorchange: bool = False,
754+
variant: int | None = None,
690755
) -> dict[str, Any] | None:
691756
"""Process custom pattern arguments.
692757
@@ -816,8 +881,16 @@ def processCustomArgs(
816881
parser.error(f"Invalid direction: {direction}. Use l2r or r2l")
817882
return None
818883

819-
# Option value
820-
option_value = 0x01 if colorchange else 0x00
884+
# Option value: --variant takes precedence; fall back to --colorchange.
885+
if variant is not None:
886+
if not 0 <= variant <= 2:
887+
parser.error(
888+
f"Invalid --variant {variant}. Must be 0, 1, or 2."
889+
)
890+
return None
891+
option_value = variant
892+
else:
893+
option_value = 0x01 if colorchange else 0x00
821894

822895
return {
823896
"mode": "extended",
@@ -982,9 +1055,9 @@ def parseArgs() -> tuple[Values, Any]:
9821055
+ "TYPE: jump, gradual, strobe (standard), or extended pattern names "
9831056
+ "(wave, meteor, breathe, etc. for 0xB6 devices), or 'segments' for static colors, "
9841057
+ "or 'scribble' for per-LED colors (0xB6 devices). "
985-
+ "SPEED is percent (0-100) (for scribble, use the effect name instead). "
1058+
+ "SPEED is percent (0-100) (for scribble, use the effect name or 0-8 id instead). "
9861059
+ "COLORLIST is a space-separated list of color names, hex values, or RGB triples. "
987-
+ "Use --density, --direction, --colorchange for extended pattern options.",
1060+
+ "Use --density, --direction, --colorchange, --variant 0|1|2 for extended pattern options.",
9881061
)
9891062
parser.add_option_group(mode_group)
9901063

@@ -1008,7 +1081,16 @@ def parseArgs() -> tuple[Values, Any]:
10081081
action="store_true",
10091082
dest="colorchange",
10101083
default=False,
1011-
help="Enable color change option for extended effect",
1084+
help="Enable color change option for extended effect (equivalent to --variant 1)",
1085+
)
1086+
other_group.add_option(
1087+
"--variant",
1088+
dest="variant",
1089+
default=None,
1090+
type="int",
1091+
metavar="VARIANT",
1092+
help="Extended effect option variant: 0, 1, or 2 (overrides --colorchange). "
1093+
"Use with -C <extended-pattern> SPEED COLORLIST.",
10121094
)
10131095

10141096
parser.add_option(
@@ -1148,6 +1230,7 @@ def parseArgs() -> tuple[Values, Any]:
11481230
density=options.density,
11491231
direction=options.direction,
11501232
colorchange=options.colorchange,
1233+
variant=options.variant,
11511234
)
11521235

11531236
if options.color:
@@ -1349,10 +1432,13 @@ def buf_in(str: str) -> None:
13491432
)
13501433
else:
13511434
leds = custom["leds"]
1352-
effect = ScribbleEffect(custom["effect"])
1435+
# Pass the raw int effect id; the API accepts ScribbleEffect|int
1436+
# so unnamed ids (3, 4, 6, 7) work without raising ValueError.
1437+
effect_id_raw: int = custom["effect"]
13531438
direction = ExtendedCustomEffectDirection(custom["direction"])
13541439
density = custom["density"]
13551440
speed = custom["speed"]
1441+
scribble_enter_mode: bool = custom.get("enter_mode", True)
13561442
# Reconcile parsed LED count with the device's configured count:
13571443
# pad the tail with off-LEDs or truncate so the upload is exactly
13581444
# the device's LED count (avoids the API's hard count check).
@@ -1368,17 +1454,23 @@ def buf_in(str: str) -> None:
13681454
)
13691455
)
13701456
leds = _reconcile_scribble_led_count(leds, device_count)
1457+
# Build display name for the effect id
1458+
try:
1459+
effect_display = ScribbleEffect(effect_id_raw).name.lower()
1460+
except ValueError:
1461+
effect_display = str(effect_id_raw)
13711462
buf_in(
13721463
f"Setting scribble: {len(leds)} LEDs, "
1373-
f"effect={effect.name.lower()}, "
1464+
f"effect={effect_display}, "
13741465
f"speed={speed}, density={density}"
13751466
)
13761467
await bulb.async_set_scribble(
13771468
leds,
1378-
effect=effect,
1469+
effect=effect_id_raw,
13791470
direction=direction,
13801471
density=density,
13811472
speed=speed,
1473+
enter_mode=scribble_enter_mode,
13821474
)
13831475

13841476
elif options.preset is not None:

0 commit comments

Comments
 (0)