# Shining Mask — Python controller A clean, async **Python controller for the "Shining Mask" LED face mask**, driven over Bluetooth LE with [bleak](https://github.com/hbldh/bleak) (BlueZ on Linux). A from-scratch port of the *Shining Mask* app protocol, cross-checked byte-for-byte against the canonical Go implementation ([GoneUp/mask-go](https://github.com/GoneUp/mask-go)), the community protocol doc ([BrickCraftDream/Shining-Mask-stuff](https://github.com/BrickCraftDream/Shining-Mask-stuff)), **real encrypted app traffic** ([beclamide/mask-controller](https://github.com/beclamide/mask-controller)), and the custom-image upload recipe from ([BishopFox/shining-mask](https://github.com/BishopFox/shining-mask)). This project is **flat** — every module is a plain `.py` file in this folder. Run things from here with the env that has `bleak`/`cryptography`/`Pillow`: ```bash cd ~/Robotics_workspace/yslootahtech/Project/Mask python main.py # uses the g1_env python ``` No install/packaging — `import mask`, `import colorface`, etc. work because the files sit in the working directory. ## Features - Connect over BLE (scan for `MASK-…`, or by MAC) - Brightness, built-in images/animations - **Custom full-color images & animations** — the headline feature - **Animated face** with idle blink/glance + talking mouth (`FaceAnimator`) - **Convert any image / GIF** to the mask (`image2mask.py`) - Scrolling text (mode, speed, colors), DIY image PLAY/DELETE/COUNT - A protocol layer with no BLE dependency, fully unit-tested without hardware ## Display any image / GIF ```bash python image2mask.py photo.jpg # fit + show a still image python image2mask.py logo.png --fit cover --oval python image2mask.py dance.gif --max-frames 12 --fps 8 --loops 5 # animate a GIF python image2mask.py photo.jpg --preview # ASCII preview, no mask needed python image2mask.py photo.jpg --save out.bin # just save the raw 8004-byte frame ``` `--fit contain|cover|stretch` controls how the image fits the **46×58 portrait** oval; `--oval` blacks out the corners to match the panel shape. In code: ```python import colorface from mask import ShiningMask img = colorface.load_image("photo.jpg", fit="cover", oval=True) # -> 46x58 RGB async with ShiningMask(name_prefix="MASK") as mask: await mask.upload_raw_image(colorface.encode(img)) # show it # animated GIF -> frames -> DIY images -> PLAY-loop: frames = colorface.load_frames("dance.gif", max_frames=12) ``` ## Animated face (Marcus) The mask's image display is a portrait oval **~46×58 RGB**, stored *transposed* (`display[x,y] = raw[y,x]`). The firmware flashes an "uploading" logo during every upload, so smooth animation can't re-upload per frame — instead a frame set is uploaded **once** as DIY images, then animated with fast `PLAY` commands (no logo). That's [`FaceAnimator`](faceanim.py) + [`colorface`](colorface.py). ```bash python main.py # connect, load frames once, run a live face python main.py --reload # force re-upload of the frame set python main.py --talk # start talking and stay talking ``` Drive it from Marcus's speech: ```python from mask import ShiningMask from faceanim import FaceAnimator async with ShiningMask(name_prefix="MASK") as mask: face = FaceAnimator(mask) await face.start() # uploads frames once, starts the idle animation face.set_speaking(True) # call when TTS playback starts # ... Marcus talks; mouth animates, eyes blink/glance ... face.set_speaking(False) # call when it ends # or: face.set_mouth(0..3) from live audio amplitude for rough lip-sync await face.stop() ``` Draw your own faces in 46×58 display space: ```python import colorface img = colorface.build_face(mouth=2, look=-4) # PIL image, 46x58 await mask.upload_frame(colorface.encode(img), slot=1) await mask.play_frame(1) ``` ## Other commands ```python import asyncio from mask import ShiningMask from constants import TextMode async def main(): async with ShiningMask(name_prefix="MASK") as mask: await mask.set_brightness(80) await mask.show_image(3) # built-in image await mask.set_text("HELLO", color=(0, 200, 255), mode=TextMode.SCROLL_LEFT) asyncio.run(main()) ``` CLI for quick one-offs: `python cli.py light 80`, `python cli.py image 3`, `python cli.py text "HELLO" --color 00ff00`, `python cli.py repl`. Utilities: `python scan.py` (find the mask), `python selftest.py `, `python preview_text.py "HI"`. ## Protocol notes - Command/notify frames are **AES-128-ECB**, 16 bytes, fixed firmware key `32672f7974ad43451d9c6c894a0e8764` (not a secret; same across vendors). - Command frame: `[len][ASCII name][args…]` zero-padded to 16, `len = name+args`. - **Custom image format (solved):** the image display is portrait **46×58 RGB**, stored transposed. Upload it with an image `DATS` (toggle `0x01`), then stream the chunks **without** waiting for acks, then `DATCP` with a 4-byte timestamp (no handshake — that was the trick). Frames persist on the mask's flash and replay via `PLAY` with no logo. ### GATT characteristics | Purpose | UUID | | |---|---|---| | Command (encrypted) | `d44bc439-abfd-45a2-b575-925416129600` | write | | Notify (encrypted) | `d44bc439-abfd-45a2-b575-925416129601` | notify | | Image/bitmap upload (raw) | `d44bc439-abfd-45a2-b575-92541612960a` | write | | Audio visualization (encrypted) | `d44bc439-abfd-45a2-b575-92541612960b` | write | ## Layout (flat) ``` main.py run a live animated face image2mask.py convert any image / GIF and display it mask.py ShiningMask high-level async API faceanim.py FaceAnimator (load frames once -> PLAY-loop) colorface.py 46x58 face frames + image->mask conversion protocol.py command framing + encoders (no I/O) crypto.py AES-128-ECB transport.py bleak BLE: scan / connect / notify / writes bitmap.py faces.py talking.py cli.py constants.py exceptions.py scan.py selftest.py preview_text.py utility scripts test_*.py pytest (no hardware) NotoSans-Regular.ttf ``` ## Tests ```bash python -m pytest -q # 57 tests, no hardware needed ``` The crypto tests decrypt real captured app frames and assert they resolve to the expected commands; the colorface tests validate image→mask conversion and the 46×58 transpose round-trip. ## License MIT. Reverse-engineering credit: [mask-go](https://github.com/GoneUp/mask-go), [Shining-Mask-stuff](https://github.com/BrickCraftDream/Shining-Mask-stuff), [mask-controller](https://github.com/beclamide/mask-controller), [BishopFox/shining-mask](https://github.com/BishopFox/shining-mask).