"""Command-line driver for the Shining Mask. Examples:: python cli.py text "HELLO" --color 00ff00 --mode scroll-left python cli.py image 3 python cli.py light 80 python cli.py repl # interactive, like the mask-go demo """ from __future__ import annotations import argparse import asyncio import logging import shlex from constants import TextMode from mask import ShiningMask _MODES = { "steady": TextMode.STEADY, "solid": TextMode.SOLID, "blink": TextMode.BLINK, "scroll-left": TextMode.SCROLL_LEFT, "scroll-right": TextMode.SCROLL_RIGHT, } def _hex_color(s: str): s = s.lstrip("#") if len(s) != 6: raise argparse.ArgumentTypeError("color must be RRGGBB hex") return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)) def _build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(prog="shiningmask", description=__doc__) p.add_argument("--address", help="connect to a specific BLE MAC instead of scanning") p.add_argument("--adapter", help="HCI adapter (e.g. hci0)") p.add_argument("--name-prefix", default="MASK", help="advertised-name prefix to scan for") p.add_argument("--with-response", action="store_true", help="force write-with-response (default: auto-detect from the characteristic)") p.add_argument("-v", "--verbose", action="store_true") sub = p.add_subparsers(dest="cmd", required=True) s = sub.add_parser("light", help="set brightness 0..255") s.add_argument("level", type=int) s = sub.add_parser("image", help="show built-in image id") s.add_argument("id", type=int) s = sub.add_parser("anim", help="play built-in animation id") s.add_argument("id", type=int) s = sub.add_parser("diy", help="show uploaded DIY image id") s.add_argument("id", type=int) s = sub.add_parser("text", help="upload + scroll text") s.add_argument("text") s.add_argument("--color", type=_hex_color, default=(255, 255, 255)) s.add_argument("--mode", choices=sorted(_MODES), default="scroll-left") s.add_argument("--speed", type=int, default=None) s = sub.add_parser("mode", help="set text mode") s.add_argument("mode", choices=sorted(_MODES)) s = sub.add_parser("speed", help="set text speed 0..255") s.add_argument("speed", type=int) s = sub.add_parser("fg", help="set foreground/text color RRGGBB") s.add_argument("color", type=_hex_color) s = sub.add_parser("bg", help="set background color RRGGBB") s.add_argument("color", type=_hex_color) s = sub.add_parser("browse", help="step through built-in images/animations to find faces") s.add_argument("--kind", choices=["image", "anim"], default="image") s.add_argument("--start", type=int, default=0) s.add_argument("--count", type=int, default=106) s.add_argument("--delay", type=float, default=1.5) sub.add_parser("count", help="report how many DIY images are stored") sub.add_parser("repl", help="interactive control loop") return p async def _dispatch(mask: ShiningMask, ns: argparse.Namespace): cmd = ns.cmd if cmd == "light": await mask.set_brightness(ns.level) elif cmd == "image": await mask.show_image(ns.id) elif cmd == "anim": await mask.play_animation(ns.id) elif cmd == "diy": await mask.play_diy(ns.id) elif cmd == "text": await mask.set_text(ns.text, color=ns.color, mode=_MODES[ns.mode], speed=ns.speed) elif cmd == "mode": await mask.set_text_mode(_MODES[ns.mode]) elif cmd == "speed": await mask.set_text_speed(ns.speed) elif cmd == "fg": await mask.set_foreground_color(*ns.color) elif cmd == "bg": await mask.set_background_color(*ns.color) elif cmd == "count": print("DIY images stored:", await mask.get_diy_count()) elif cmd == "browse": show = mask.play_animation if ns.kind == "anim" else mask.show_image end = ns.start + ns.count print(f"Browsing built-in {ns.kind} ids {ns.start}..{end - 1}, {ns.delay}s each.") print("Watch the mask. Note the id of a face you like, press Ctrl+C, then run " f"'{ns.kind if ns.kind == 'anim' else 'image'} ' to keep it.\n") for i in range(ns.start, end): print(f" {ns.kind} {i}", flush=True) try: await show(i) except Exception as exc: print(f" (error at {i}: {exc})") await asyncio.sleep(ns.delay) print("done browsing") async def _repl(mask: ShiningMask, parser: argparse.ArgumentParser): print("interactive mode -- type a subcommand (e.g. 'text HI --color ff0000'), 'quit' to exit") loop = asyncio.get_running_loop() while True: line = await loop.run_in_executor(None, input, "mask> ") line = line.strip() if line in ("quit", "exit", "q"): break if not line: continue try: ns = parser.parse_args(shlex.split(line)) except SystemExit: continue try: await _dispatch(mask, ns) except Exception as exc: # keep the REPL alive print("error:", exc) async def _amain(argv=None): parser = _build_parser() ns = parser.parse_args(argv) logging.basicConfig(level=logging.DEBUG if ns.verbose else logging.INFO) async with ShiningMask( address=ns.address, name_prefix=ns.name_prefix, adapter=ns.adapter, write_with_response=True if ns.with_response else None, ) as mask: if ns.cmd == "repl": await _repl(mask, parser) else: await _dispatch(mask, ns) def main(argv=None): asyncio.run(_amain(argv)) if __name__ == "__main__": main()