169 lines
5.7 KiB
Python
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()
|