173 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <MAC>`,
`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).