@@ -546,39 +546,102 @@ 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" :
552- return ScribbleLED (rgb = None , white = None , blink_mode = blink_mode , blink_speed = blink_speed )
589+ return ScribbleLED (
590+ rgb = None , white = None , blink_mode = blink_mode , blink_speed = blink_speed
591+ )
553592 # White-level token: 'w' followed by DIGITS ONLY (e.g. w50). A token like
554593 # 'white' or 'wheat' is a color name, so it falls through to the color parser.
555594 white_match = _WHITE_LEVEL_RE .match (token_lower )
556595 if white_match :
557596 level = int (white_match .group (1 ))
558- return ScribbleLED (rgb = None , white = level , blink_mode = blink_mode , blink_speed = blink_speed )
597+ return ScribbleLED (
598+ rgb = None , white = level , blink_mode = blink_mode , blink_speed = blink_speed
599+ )
559600 # Color token
560- c = utils .color_object_to_tuple (token )
601+ c = utils .color_object_to_tuple (value_part )
561602 if c is None or len (c ) < 3 :
562603 parser .error (
563- f"Invalid color token '{ token } '. Use a color name, hex value, or r,g,b triple."
604+ f"Invalid color token '{ value_part } '. Use a color name, hex value, or r,g,b triple."
564605 )
565606 raise SystemExit (1 )
566- return ScribbleLED (rgb = (c [0 ], c [1 ], c [2 ]), white = None , blink_mode = blink_mode , blink_speed = blink_speed )
607+ return ScribbleLED (
608+ rgb = (c [0 ], c [1 ], c [2 ]),
609+ white = None ,
610+ blink_mode = blink_mode ,
611+ blink_speed = blink_speed ,
612+ )
567613
568614
569615def _process_scribble_args (
570616 parser : OptionParser ,
571617 args : Any ,
572618) -> dict [str , Any ] | None :
573- """Parse scribble-mode args: ('scribble', <effect>, '<per-led-spec>')."""
619+ """Parse scribble-mode args: ('scribble', <effect>, '<per-led-spec>').
620+
621+ ``<effect>`` may be a named effect (static/flowing/twinkling/stars_wink/
622+ accumulate) OR a raw integer 0-8.
623+ """
574624 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 ]
625+ if effect_name in _SCRIBBLE_EFFECT_NAMES :
626+ effect_id : int = _SCRIBBLE_EFFECT_NAMES [effect_name ].value
627+ else :
628+ # Try numeric id
629+ try :
630+ effect_id = int (effect_name )
631+ except ValueError :
632+ parser .error (
633+ f"Unknown scribble effect '{ effect_name } '. "
634+ f"Valid names: { ', ' .join (_SCRIBBLE_EFFECT_NAMES )} ; "
635+ f"or a number 0-8."
636+ )
637+ return None
638+ if not 0 <= effect_id <= 8 :
639+ parser .error (
640+ f"Scribble effect id { effect_id } out of range. "
641+ f"Valid names: { ', ' .join (_SCRIBBLE_EFFECT_NAMES )} ; "
642+ f"or a number 0-8."
643+ )
644+ return None
582645
583646 spec_str = args [2 ].strip () if len (args ) > 2 else ""
584647
@@ -588,6 +651,7 @@ def _process_scribble_args(
588651 direction = ExtendedCustomEffectDirection .LEFT_TO_RIGHT
589652 blink_mode = ScribbleBlinkMode .NONE
590653 blink_speed = 100
654+ enter_mode = True
591655
592656 # Split spec on whitespace; separate LED tokens from key=value tokens
593657 all_tokens = spec_str .split ()
@@ -629,7 +693,17 @@ def _process_scribble_args(
629693 try :
630694 blink_speed = int (val )
631695 except ValueError :
632- parser .error (f"Invalid blinkspeed value '{ val } '. Must be an integer." )
696+ parser .error (
697+ f"Invalid blinkspeed value '{ val } '. Must be an integer."
698+ )
699+ return None
700+ elif key == "enter" :
701+ if val .lower () == "true" :
702+ enter_mode = True
703+ elif val .lower () == "false" :
704+ enter_mode = False
705+ else :
706+ parser .error (f"Invalid enter value '{ val } '. Use true or false." )
633707 return None
634708 else :
635709 parser .error (f"Unknown scribble option '{ key } '." )
@@ -655,11 +729,12 @@ def _process_scribble_args(
655729
656730 return {
657731 "mode" : "scribble" ,
658- "effect" : effect . value ,
732+ "effect" : effect_id ,
659733 "leds" : leds ,
660734 "direction" : direction .value ,
661735 "density" : density ,
662736 "speed" : speed ,
737+ "enter_mode" : enter_mode ,
663738 }
664739
665740
@@ -687,6 +762,7 @@ def processCustomArgs(
687762 density : int = 50 ,
688763 direction : str = "l2r" ,
689764 colorchange : bool = False ,
765+ variant : int | None = None ,
690766) -> dict [str , Any ] | None :
691767 """Process custom pattern arguments.
692768
@@ -816,8 +892,14 @@ def processCustomArgs(
816892 parser .error (f"Invalid direction: { direction } . Use l2r or r2l" )
817893 return None
818894
819- # Option value
820- option_value = 0x01 if colorchange else 0x00
895+ # Option value: --variant takes precedence; fall back to --colorchange.
896+ if variant is not None :
897+ if not 0 <= variant <= 2 :
898+ parser .error (f"Invalid --variant { variant } . Must be 0, 1, or 2." )
899+ return None
900+ option_value = variant
901+ else :
902+ option_value = 0x01 if colorchange else 0x00
821903
822904 return {
823905 "mode" : "extended" ,
@@ -982,9 +1064,9 @@ def parseArgs() -> tuple[Values, Any]:
9821064 + "TYPE: jump, gradual, strobe (standard), or extended pattern names "
9831065 + "(wave, meteor, breathe, etc. for 0xB6 devices), or 'segments' for static colors, "
9841066 + "or 'scribble' for per-LED colors (0xB6 devices). "
985- + "SPEED is percent (0-100) (for scribble, use the effect name instead). "
1067+ + "SPEED is percent (0-100) (for scribble, use the effect name or 0-8 id instead). "
9861068 + "COLORLIST is a space-separated list of color names, hex values, or RGB triples. "
987- + "Use --density, --direction, --colorchange for extended pattern options." ,
1069+ + "Use --density, --direction, --colorchange, --variant 0|1|2 for extended pattern options." ,
9881070 )
9891071 parser .add_option_group (mode_group )
9901072
@@ -1008,7 +1090,16 @@ def parseArgs() -> tuple[Values, Any]:
10081090 action = "store_true" ,
10091091 dest = "colorchange" ,
10101092 default = False ,
1011- help = "Enable color change option for extended effect" ,
1093+ help = "Enable color change option for extended effect (equivalent to --variant 1)" ,
1094+ )
1095+ other_group .add_option (
1096+ "--variant" ,
1097+ dest = "variant" ,
1098+ default = None ,
1099+ type = "int" ,
1100+ metavar = "VARIANT" ,
1101+ help = "Extended effect option variant: 0, 1, or 2 (overrides --colorchange). "
1102+ "Use with -C <extended-pattern> SPEED COLORLIST." ,
10121103 )
10131104
10141105 parser .add_option (
@@ -1105,9 +1196,9 @@ def parseArgs() -> tuple[Values, Any]:
11051196 print ("\n Segment colors (-C segments, 0xB6 devices only):" )
11061197 print (" segments - Set individual segment colors (up to 20)" )
11071198 print ("\n Per-LED scribble (-C scribble, 0xB6 devices only):" )
1108- print (" scribble <effect> \ " <per-led-spec>\ " - Set per-LED colors/blink" )
1199+ print (' scribble <effect> "<per-led-spec>" - Set per-LED colors/blink' )
11091200 print (" Effects: static, flowing, twinkling, stars_wink, accumulate" )
1110- print (" Example: -C scribble static \ " 10xred 70xoff\" " )
1201+ print (' Example: -C scribble static "10xred 70xoff"' )
11111202 sys .exit (0 )
11121203
11131204 if options .listcolors :
@@ -1148,6 +1239,7 @@ def parseArgs() -> tuple[Values, Any]:
11481239 density = options .density ,
11491240 direction = options .direction ,
11501241 colorchange = options .colorchange ,
1242+ variant = options .variant ,
11511243 )
11521244
11531245 if options .color :
@@ -1344,15 +1436,16 @@ def buf_in(str: str) -> None:
13441436 elif mode == "scribble" :
13451437 # Per-LED scribble configuration
13461438 if not bulb .supports_scribble :
1347- buf_in (
1348- f"{ bulb .model } does not support the scribble (per-LED) feature."
1349- )
1439+ buf_in (f"{ bulb .model } does not support the scribble (per-LED) feature." )
13501440 else :
13511441 leds = custom ["leds" ]
1352- effect = ScribbleEffect (custom ["effect" ])
1442+ # Pass the raw int effect id; the API accepts ScribbleEffect|int
1443+ # so unnamed ids (3, 4, 6, 7) work without raising ValueError.
1444+ effect_id_raw : int = custom ["effect" ]
13531445 direction = ExtendedCustomEffectDirection (custom ["direction" ])
13541446 density = custom ["density" ]
13551447 speed = custom ["speed" ]
1448+ scribble_enter_mode : bool = custom .get ("enter_mode" , True )
13561449 # Reconcile parsed LED count with the device's configured count:
13571450 # pad the tail with off-LEDs or truncate so the upload is exactly
13581451 # the device's LED count (avoids the API's hard count check).
@@ -1368,17 +1461,23 @@ def buf_in(str: str) -> None:
13681461 )
13691462 )
13701463 leds = _reconcile_scribble_led_count (leds , device_count )
1464+ # Build display name for the effect id
1465+ try :
1466+ effect_display = ScribbleEffect (effect_id_raw ).name .lower ()
1467+ except ValueError :
1468+ effect_display = str (effect_id_raw )
13711469 buf_in (
13721470 f"Setting scribble: { len (leds )} LEDs, "
1373- f"effect={ effect . name . lower () } , "
1471+ f"effect={ effect_display } , "
13741472 f"speed={ speed } , density={ density } "
13751473 )
13761474 await bulb .async_set_scribble (
13771475 leds ,
1378- effect = effect ,
1476+ effect = effect_id_raw ,
13791477 direction = direction ,
13801478 density = density ,
13811479 speed = speed ,
1480+ enter_mode = scribble_enter_mode ,
13821481 )
13831482
13841483 elif options .preset is not None :
0 commit comments