..
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00
2026-07-04 23:28:12 +04:00

Shining Mask — Python controller

A clean, async Python controller for the "Shining Mask" LED face mask, driven over Bluetooth LE with 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), the community protocol doc (BrickCraftDream/Shining-Mask-stuff), real encrypted app traffic (beclamide/mask-controller), and the custom-image upload recipe from (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:

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

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:

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 + colorface.

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:

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:

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

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

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, Shining-Mask-stuff, mask-controller, BishopFox/shining-mask.