@@ -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