112 lines
4.2 KiB
Python
112 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Convert ANY image (or animated GIF/WEBP) and display it on the mask.
|
|
|
|
Still image -> fit to the 46x58 oval display and show it.
|
|
Animated GIF -> upload its frames as DIY images and PLAY-loop them.
|
|
|
|
Usage:
|
|
python image2mask.py photo.jpg
|
|
python image2mask.py logo.png --fit cover --oval
|
|
python image2mask.py dance.gif --max-frames 12 --fps 8 --loops 5
|
|
python image2mask.py photo.jpg --save out.bin # just save raw bytes, no mask
|
|
python image2mask.py photo.jpg --preview # print ASCII preview, no mask
|
|
|
|
Options:
|
|
--fit contain|cover|stretch how to fit the image (default contain)
|
|
--bg RRGGBB letterbox/oval background color (default 000000)
|
|
--oval black out corners to match the oval panel
|
|
--brightness N 0..255 (default 95)
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import sys
|
|
|
|
import colorface
|
|
from mask import ShiningMask
|
|
from exceptions import MaskNotFound
|
|
|
|
|
|
def _hex(s):
|
|
s = s.lstrip("#")
|
|
return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16))
|
|
|
|
|
|
def _ascii(image):
|
|
img = image.convert("L")
|
|
ramp = " .:-=+*#%@"
|
|
out = []
|
|
for y in range(0, colorface.DISPLAY_H, 2):
|
|
row = "".join(ramp[min(len(ramp) - 1, img.getpixel((x, y)) * len(ramp) // 256)]
|
|
for x in range(colorface.DISPLAY_W))
|
|
out.append(row)
|
|
return "\n".join(out)
|
|
|
|
|
|
async def run(args):
|
|
frames = colorface.load_frames(
|
|
args.image, max_frames=args.max_frames, fit=args.fit,
|
|
bg=_hex(args.bg), oval=args.oval,
|
|
)
|
|
print(f"loaded {len(frames)} frame(s) from {args.image} (fit={args.fit}, oval={args.oval})")
|
|
|
|
if args.preview:
|
|
print(_ascii(frames[0]))
|
|
return
|
|
if args.save:
|
|
with open(args.save, "wb") as fh:
|
|
fh.write(colorface.encode(frames[0]))
|
|
print(f"saved raw frame ({colorface.DISPLAY_W}x{colorface.DISPLAY_H} -> 8004 B) to {args.save}")
|
|
return
|
|
|
|
mask = ShiningMask(address=args.address, name_prefix=args.name_prefix)
|
|
print("connecting ...")
|
|
try:
|
|
await mask.connect(timeout=20.0, attempts=8)
|
|
except MaskNotFound as exc:
|
|
print(f"could not find the mask: {exc}")
|
|
return
|
|
try:
|
|
await mask.set_brightness(args.brightness)
|
|
if len(frames) == 1:
|
|
await mask.upload_raw_image(colorface.encode(frames[0]), index=1)
|
|
print("image shown on the mask.")
|
|
else:
|
|
print(f"uploading {len(frames)} frames (one-time, ~{len(frames) * 4}s) ...")
|
|
await mask.clear_diy()
|
|
for i, fr in enumerate(frames, start=1):
|
|
await mask.upload_frame(colorface.encode(fr), i)
|
|
print(f" frame {i}/{len(frames)}", flush=True)
|
|
print(f"animating {args.loops} loop(s) at {args.fps} fps (Ctrl+C to stop) ...")
|
|
try:
|
|
for _ in range(args.loops):
|
|
for i in range(1, len(frames) + 1):
|
|
await mask.play_frame(i)
|
|
await asyncio.sleep(1.0 / args.fps)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
await mask.disconnect()
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("image", help="path to any image or animated GIF/WEBP")
|
|
ap.add_argument("--fit", choices=["contain", "cover", "stretch"], default="contain")
|
|
ap.add_argument("--bg", default="000000", help="background RRGGBB")
|
|
ap.add_argument("--oval", action="store_true", help="black out corners (oval shape)")
|
|
ap.add_argument("--max-frames", type=int, default=16)
|
|
ap.add_argument("--fps", type=float, default=8.0)
|
|
ap.add_argument("--loops", type=int, default=10)
|
|
ap.add_argument("--brightness", type=int, default=95)
|
|
ap.add_argument("--save", help="save the raw 8004-byte frame to a file instead of uploading")
|
|
ap.add_argument("--preview", action="store_true", help="print an ASCII preview instead of uploading")
|
|
ap.add_argument("--address", help="connect to a specific BLE MAC")
|
|
ap.add_argument("--name-prefix", default="MASK")
|
|
asyncio.run(run(ap.parse_args()))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|