Skip to content

Instantly share code, notes, and snippets.

@NicolaiSoeborg
Created January 8, 2026 15:10
Show Gist options
  • Select an option

  • Save NicolaiSoeborg/81a65703d16a81d24d6b225bfe646ba1 to your computer and use it in GitHub Desktop.

Select an option

Save NicolaiSoeborg/81a65703d16a81d24d6b225bfe646ba1 to your computer and use it in GitHub Desktop.
import asyncio
from datetime import datetime
from bleak import BleakClient, BleakScanner
#devs = await BleakScanner.discover(timeout=10.0, service_uuids=['0000fee0-0000-1000-8000-00805f9b34fb'])
#assert len(devs) == 1
device_address = 'XX:XX:XX:XX:XX:XX'
# From https://github.com/fossasia/badgemagic-app/blob/development/lib/bademagic_module/utils/data_to_bytearray_converter.dart
char_codes = {
'0': "007CC6CEDEF6E6C6C67C00",
'1': "0018387818181818187E00",
'2': "007CC6060C183060C6FE00",
'3': "007CC606063C0606C67C00",
'4': "000C1C3C6CCCFE0C0C1E00",
'5': "00FEC0C0FC060606C67C00",
'6': "007CC6C0C0FCC6C6C67C00",
'7': "00FEC6060C183030303000",
'8': "007CC6C6C67CC6C6C67C00",
'9': "007CC6C6C67E0606C67C00",
'#': "006C6CFE6C6CFE6C6C0000",
'&': "00386C6C3876DCCCCC7600",
'_': "00000000000000000000FF",
'-': "0000000000FE0000000000",
'?': "007CC6C60C181800181800",
'@': "00003C429DA5ADB6403C00",
'(': "000C183030303030180C00",
')': "0030180C0C0C0C0C183000",
'=': "0000007E00007E00000000",
'+': "00000018187E1818000000",
'!': "00183C3C3C181800181800",
'\'': "1818081000000000000000",
':': "0000001818000018180000",
'%': "006092966C106CD2920C00",
'/': "000002060C183060C08000",
'"': "6666222200000000000000",
'[': "003C303030303030303C00",
']': "003C0C0C0C0C0C0C0C3C00",
' ': "0000000000000000000000",
'*': "000000663CFF3C66000000",
',': "0000000000000030301020",
'.': "0000000000000000303000",
'$': "107CD6D6701CD6D67C1010",
'~': "0076DC0000000000000000",
'{': "000E181818701818180E00",
'}': "00701818180E1818187000",
'<': "00060C18306030180C0600",
'>': "006030180C060C18306000",
'^': "386CC60000000000000000",
'`': "1818100800000000000000",
';': "0000001818000018180810",
'\\': "0080C06030180C06020000",
'|': "0018181818001818181800",
'a': "00000000780C7CCCCC7600",
'b': "00E060607C666666667C00",
'c': "000000007CC6C0C0C67C00",
'd': "001C0C0C7CCCCCCCCC7600",
'e': "000000007CC6FEC0C67C00",
'f': "001C363078303030307800",
'g': "00000076CCCCCC7C0CCC78",
'h': "00E060606C76666666E600",
'i': "0018180038181818183C00",
'j': "0C0C001C0C0C0C0CCCCC78",
'k': "00E06060666C78786CE600",
'l': "0038181818181818183C00",
'm': "00000000ECFED6D6D6C600",
'n': "00000000DC666666666600",
'o': "000000007CC6C6C6C67C00",
'p': "000000DC6666667C6060F0",
'q': "0000007CCCCCCC7C0C0C1E",
'r': "00000000DE76606060F000",
's': "000000007CC6701CC67C00",
't': "00103030FC303030341800",
'u': "00000000CCCCCCCCCC7600",
'v': "00000000C6C6C66C381000",
'w': "00000000C6D6D6D6FE6C00",
'x': "00000000C66C38386CC600",
'y': "000000C6C6C6C67E060CF8",
'z': "00000000FE8C183062FE00",
'A': "00386CC6C6FEC6C6C6C600",
'B': "00FC6666667C666666FC00",
'C': "007CC6C6C0C0C0C6C67C00",
'D': "00FC66666666666666FC00",
'E': "00FE66626878686266FE00",
'F': "00FE66626878686060F000",
'G': "007CC6C6C0C0CEC6C67E00",
'H': "00C6C6C6C6FEC6C6C6C600",
'I': "003C181818181818183C00",
'J': "001E0C0C0C0C0CCCCC7800",
'K': "00E6666C6C786C6C66E600",
'L': "00F060606060606266FE00",
'M': "0082C6EEFED6C6C6C6C600",
'N': "0086C6E6F6DECEC6C6C600",
'O': "007CC6C6C6C6C6C6C67C00",
'P': "00FC6666667C606060F000",
'Q': "007CC6C6C6C6C6D6DE7C06",
'R': "00FC6666667C6C6666E600",
'S': "007CC6C660380CC6C67C00",
'T': "007E7E5A18181818183C00",
'U': "00C6C6C6C6C6C6C6C67C00",
'V': "00C6C6C6C6C6C66C381000",
'W': "00C6C6C6C6D6FEEEC68200",
'X': "00C6C66C7C387C6CC6C600",
'Y': "00666666663C1818183C00",
'Z': "00FEC6860C183062C6FE00",
'Á': "0810386cc6c6fec6c6c600",
'À': "2010386cc6c6fec6c6c600",
'Â': "1028386CC6C6FEC6C6C600",
'Ä': "2800386CC6C6FEC6C6C600",
'Å': "1028107CC6C6FEC6C6C600",
'É': "0810FE626878686266FE00",
'È': "2010FE626878686266FE00",
'Ê': "1028FE626878686266FE00",
'Ë': "2800FE626878686266FE00",
'Ě': "2810FE626878686266FE00",
'Í': "04083C1818181818183C00",
'Ì': "10083C1818181818183C00",
'Î': "08143C1818181818183C00",
'Ï': "14003C1818181818183C00",
'Ó': "08107CC6C6C6C6C6C67C00",
'Ò': "20107CC6C6C6C6C6C67C00",
'Ô': "10287CC6C6C6C6C6C67C00",
'Ö': "28007CC6C6C6C6C6C67C00",
'Ő': "14287CC6C6C6C6C6C67C00",
'Ú': "0810C6C6C6C6C6C6C67C00",
'Ù': "2010C6C6C6C6C6C6C67C00",
'Û': "1028C6C6C6C6C6C6C67C00",
'Ü': "2800C6C6C6C6C6C6C67C00",
'Ű': "1428C6C6C6C6C6C6C67C00",
'Ů': "102810C6C6C6C6C6C67C00",
'Ý': "04086666663C1818183C00",
'Ÿ': "14006666663C1818183C00",
'á': "00000810780C7CCCCC7600",
'à': "00002010780C7CCCCC7600",
'â': "00102800780C7CCCCC7600",
'ä': "00002800780C7CCCCC7600",
'å': "00102810780C7CCCCC7600",
'é': "000008107CC6FEC0C67C00",
'è': "000020107CC6FEC0C67C00",
'ê': "001028007CC6FEC0C67C00",
'ë': "000028007CC6FEC0C67C00",
'ě': "000028107CC6FEC0C67C00",
'í': "0000081038181818183C00",
'ì': "0000201038181818183C00",
'î': "0008140038181818183C00",
'ï': "0000140038181818183C00",
'ó': "000008107CC6C6C6C67C00",
'ò': "000020107CC6C6C6C67C00",
'ô': "001028007CC6C6C6C67C00",
'ö': "000028007CC6C6C6C67C00",
'ő': "000014287CC6C6C6C67C00",
'ú': "00000810CCCCCCCCCC7600",
'ù': "00002010CCCCCCCCCC7600",
'û': "00102800CCCCCCCCCC7600",
'ü': "00002800CCCCCCCCCC7600",
'ű': "00001428CCCCCCCCCC7600",
'ů': "00102810CCCCCCCCCC7600",
'ý': "000810C6C6C6C67E060CF8",
'ÿ': "002800C6C6C6C67E060CF8",
'Ç': "007CC6C6C0C0C0C67C1030",
'ç': "000000007CC6C0467C1030",
'Ñ': "342CC6E6F6DECEC6C6C600",
'ñ': "00342C00DC666666666600",
'Č': "28107CC6C6C0C0C6C67C00",
'č': "000028107CC6C0C0C67C00",
'Ď': "2810FC666666666666FC00",
'ď': "02061C0C7CCCCCCCCC7600",
'Ň': "2810C6E6F6DECEC6C6C600",
'ň': "00002810DC666666666600",
'Ř': "2810FC66667C6C6666E600",
'ř': "00002810DE76606060F000",
'Š': "28107CC6E0380CC6C67C00",
'š': "000028107CC6701CC67C00",
'Ť': "14087E7E5A181818183C00",
'ť': "00143430FC303030341800",
'Ž': "2810FE860C183062C6FE00",
'ž': "00002810FE8C183062FE00",
}
char_codes = {k: bytes.fromhex(v) for k, v in char_codes.items()}
def format_message(msg_str):
msg_bytes = b''.join(char_codes[c] for c in msg_str)
MAX_PAGES = 8
# Which pages have these flags
flash = bytes([0b00000000]) # flash full screen
marquee = bytes([0b00000000]) # pixel border around badge
options = bytes([0b00000100]) * MAX_PAGES
# ^ 0 => rtl, 1 => left-to-right
# ^ scroll from bottom to top
# ^^ scroll jump?
# ^^^^ speed 0b1111 => slowest?!
# page 0 has a size, rest of page has no size?
msg_len = len(msg_str)
sizes = bytes([(msg_len >> 8) & 0xFF, msg_len & 0xFF] + [0, 0] * (MAX_PAGES - 1))
now = datetime.now()
time_bytes = bytes([
now.year & 0xFF, now.month + 1, now.day,
now.hour, now.minute, now.second
])
payload = bytes([0x77, 0x61, 0x6e, 0x67, 0x00, 0x00])
payload += flash + marquee + options + sizes
payload += b"\x00"*6 + time_bytes + b"\x00"*20
payload += msg_bytes
payload = payload.ljust((len(payload) + 15) // 16 * 16, b'\x00')
yield from [payload[i:i+16] for i in range(0, len(payload), 16)]
async def main():
print(f"Connecting to {device_address}...", end='', flush=True)
client = BleakClient(device_address)
await client.connect()
print(" connected!")
for chunk in format_message("HELLO"):
await client.write_gatt_char("0000fee1-0000-1000-8000-00805f9b34fb", chunk, response=False)
if __name__ == "__main__":
asyncio.run(main())
@NicolaiSoeborg
Copy link
Author

Passive bleak scanning:

async def main():
    args = dict(
        service_uuids=['0000fee0-0000-1000-8000-00805f9b34fb'],
        scanning_mode="passive",
        bluez=BlueZArgs(or_patterns=[
            OrPattern(0, AdvertisementDataType.COMPLETE_LOCAL_NAME, b'LSLED')
        ])
    )
    async with BleakScanner(**args) as scanner:
        async for device, adv in scanner.advertisement_data():
            print(f"Found it: {device=} {adv=}")
            client = BleakClient(device)
            print(f"Connecting ...", end="", flush=True)
            await client.connect()
            print(f" connected!")
            for chunk in format_message("CLOSE"):
                await client.write_gatt_char(CHAR_UUID, chunk, response=False)
            exit(0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment