169 lines
5.7 KiB
Python

"""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'} <id>' 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()