Skip to content

Commit 366258c

Browse files
committed
Add support for image editing and upload and BLE in the browser for experiments.
1 parent e725212 commit 366258c

File tree

4 files changed

+2005
-163
lines changed

4 files changed

+2005
-163
lines changed

examples/test_image_upload.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test script for uploading a custom image to the LED badge.
4+
5+
Creates a 12x48 pixel sunglasses image and uploads it to the badge.
6+
7+
Usage:
8+
python test_image_upload.py <badge_address>
9+
10+
Example:
11+
python test_image_upload.py AA:BB:CC:DD:EE:FF
12+
"""
13+
14+
import asyncio
15+
import sys
16+
17+
# Add parent directory to path for imports
18+
sys.path.insert(0, '..')
19+
20+
from badge_controller import Badge, ScrollMode
21+
22+
23+
def create_sunglasses_bitmap():
24+
"""
25+
Create a 12x48 pixel sunglasses image.
26+
27+
Returns the raw bitmap data in the badge's expected format:
28+
- 8 segments of 6 columns each (48 total columns)
29+
- 9 bytes per segment
30+
- Total: 72 bytes
31+
"""
32+
33+
# Define the sunglasses as a 12-row × 48-column grid
34+
# 1 = pixel on, 0 = pixel off
35+
# Each string is one row, left to right
36+
image = [
37+
# 1111111111222222222233333333334444444
38+
#23456789012345678901234567890123456789012345678
39+
"000000000000000000000000000000000000000000000000", # Row 0
40+
"000000000000000000000000000000000000000000000000", # Row 1
41+
"000111111111000000000000000000001111111110000000", # Row 2
42+
"001111111111100000000000000000011111111111000000", # Row 3
43+
"011111111111110000111111110000111111111111100000", # Row 4
44+
"011111111111110000111111110000111111111111100000", # Row 5
45+
"011111111111110000111111110000111111111111100000", # Row 6
46+
"011111111111110000111111110000111111111111100000", # Row 7
47+
"001111111111100000011111100000011111111111000000", # Row 8
48+
"000111111111000000000000000000001111111110000000", # Row 9
49+
"000000000000000000000000000000000000000000000000", # Row 10
50+
"000000000000000000000000000000000000000000000000", # Row 11
51+
]
52+
53+
# Convert to a 2D array of booleans for easier manipulation
54+
pixels = []
55+
for row_str in image:
56+
row = [c == '1' for c in row_str]
57+
# Ensure exactly 48 columns
58+
while len(row) < 48:
59+
row.append(False)
60+
pixels.append(row[:48])
61+
62+
# Convert to byte format
63+
# The badge uses the same format as the font: 9 bytes per 6-column segment
64+
# Byte layout per segment:
65+
# B0: col0 rows 0-7 (bit 7 = row 0)
66+
# B1: col0 rows 8-11 (bits 7-4) | col1 rows 8-11 (bits 3-0)
67+
# B2: col1 rows 0-7
68+
# B3: col2 rows 0-7
69+
# B4: col2 rows 8-11 (bits 7-4) | col3 rows 8-11 (bits 3-0)
70+
# B5: col3 rows 0-7
71+
# B6: col4 rows 0-7
72+
# B7: col4 rows 8-11 (bits 7-4) | col5 rows 8-11 (bits 3-0)
73+
# B8: col5 rows 0-7
74+
75+
def encode_segment(pixels, start_col):
76+
"""Encode 6 columns starting at start_col into 9 bytes."""
77+
segment = [0] * 9
78+
79+
for local_col in range(6):
80+
col = start_col + local_col
81+
82+
# Rows 0-7: one byte per column
83+
byte_val = 0
84+
for row in range(8):
85+
if pixels[row][col]:
86+
byte_val |= (1 << (7 - row))
87+
88+
# Map local column to byte index for rows 0-7
89+
byte_map = [0, 2, 3, 5, 6, 8]
90+
segment[byte_map[local_col]] = byte_val
91+
92+
# Rows 8-11: nibble-packed into shared bytes
93+
nibble_val = 0
94+
for row in range(8, 12):
95+
if pixels[row][col]:
96+
nibble_val |= (1 << (11 - row)) # bits 3-0 for rows 8-11
97+
98+
# Map local column to nibble byte and position
99+
# Cols 0,1 share byte 1; cols 2,3 share byte 4; cols 4,5 share byte 7
100+
nibble_byte_map = [1, 1, 4, 4, 7, 7]
101+
byte_idx = nibble_byte_map[local_col]
102+
103+
if local_col % 2 == 0:
104+
# Even columns: upper nibble (bits 7-4)
105+
segment[byte_idx] |= (nibble_val << 4)
106+
else:
107+
# Odd columns: lower nibble (bits 3-0)
108+
segment[byte_idx] |= nibble_val
109+
110+
return segment
111+
112+
# Encode all 8 segments (48 columns / 6 = 8 segments)
113+
all_bytes = []
114+
for seg_idx in range(8):
115+
start_col = seg_idx * 6
116+
segment_bytes = encode_segment(pixels, start_col)
117+
all_bytes.extend(segment_bytes)
118+
119+
return bytes(all_bytes)
120+
121+
122+
def print_bitmap_preview(bitmap_data):
123+
"""Print a text preview of the bitmap data."""
124+
print("\nBitmap preview (48x12):")
125+
print("-" * 50)
126+
127+
# Decode the bytes back to pixels for display
128+
def get_pixel(data, segment, col, row):
129+
"""Get pixel value from encoded segment data."""
130+
base = segment * 9
131+
local_col = col
132+
133+
if row < 8:
134+
byte_map = [0, 2, 3, 5, 6, 8]
135+
byte_idx = base + byte_map[local_col]
136+
bit = 7 - row
137+
return (data[byte_idx] >> bit) & 1
138+
else:
139+
nibble_byte_map = [1, 1, 4, 4, 7, 7]
140+
byte_idx = base + nibble_byte_map[local_col]
141+
if local_col % 2 == 0:
142+
bit = 7 - (row - 8)
143+
else:
144+
bit = 3 - (row - 8)
145+
return (data[byte_idx] >> bit) & 1
146+
147+
for row in range(12):
148+
line = ""
149+
for segment in range(8):
150+
for col in range(6):
151+
if get_pixel(bitmap_data, segment, col, row):
152+
line += "█"
153+
else:
154+
line += "."
155+
print(f"Row {row:2d}: {line}")
156+
157+
print("-" * 50)
158+
print(f"Total bytes: {len(bitmap_data)}")
159+
print(f"Hex: {bitmap_data.hex()}")
160+
161+
162+
async def main():
163+
if len(sys.argv) < 2:
164+
print("Usage: python test_image_upload.py <badge_address>")
165+
print(" python test_image_upload.py --preview (preview only, no upload)")
166+
print()
167+
print("Example: python test_image_upload.py AA:BB:CC:DD:EE:FF")
168+
sys.exit(1)
169+
170+
# Create the sunglasses bitmap
171+
print("Creating sunglasses bitmap...")
172+
bitmap_data = create_sunglasses_bitmap()
173+
print_bitmap_preview(bitmap_data)
174+
175+
# Check for preview-only mode
176+
if sys.argv[1] == "--preview":
177+
print("\nPreview mode - no upload performed.")
178+
return
179+
180+
address = sys.argv[1]
181+
182+
# Upload to badge
183+
print(f"\nConnecting to badge at {address}...")
184+
185+
async with Badge(address) as badge:
186+
print("Connected!")
187+
188+
# Upload the image
189+
print("Uploading image...")
190+
success = await badge.upload_image(bitmap_data)
191+
192+
if success:
193+
print("Upload successful!")
194+
195+
# Set display to static mode so it doesn't scroll
196+
print("Setting static display mode...")
197+
await badge.set_scroll_mode(ScrollMode.STATIC)
198+
await badge.set_brightness(128)
199+
200+
print("Done! The sunglasses should now be displayed on your badge.")
201+
else:
202+
print("Upload may have failed (no acknowledgment received).")
203+
print("The image might still be displayed - check your badge.")
204+
205+
print("\nDisconnected.")
206+
207+
208+
if __name__ == "__main__":
209+
asyncio.run(main())

font-editor/badge-image.json

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"width": 48,
3+
"height": 12,
4+
"bytes": [
5+
160,
6+
90,
7+
80,
8+
175,
9+
90,
10+
86,
11+
175,
12+
90,
13+
86,
14+
47,
15+
72,
16+
22,
17+
15,
18+
0,
19+
6,
20+
6,
21+
0,
22+
6,
23+
6,
24+
0,
25+
6,
26+
6,
27+
0,
28+
6,
29+
6,
30+
0,
31+
6,
32+
6,
33+
0,
34+
6,
35+
6,
36+
0,
37+
6,
38+
6,
39+
0,
40+
6,
41+
6,
42+
0,
43+
6,
44+
6,
45+
0,
46+
6,
47+
6,
48+
0,
49+
6,
50+
6,
51+
0,
52+
6,
53+
6,
54+
0,
55+
6,
56+
6,
57+
0,
58+
6,
59+
6,
60+
0,
61+
6,
62+
6,
63+
0,
64+
6,
65+
6,
66+
1,
67+
134,
68+
198,
69+
62,
70+
118,
71+
63,
72+
200,
73+
31,
74+
15,
75+
0,
76+
6
77+
]
78+
}

font-editor/font.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -771,26 +771,26 @@
771771
],
772772
"♠": [
773773
[
774-
7,
775-
8,
776-
8,
777-
16,
778-
149,
774+
3,
775+
132,
776+
4,
777+
24,
778+
89,
779779
32,
780780
64,
781-
83,
781+
174,
782782
128
783783
],
784784
[
785785
128,
786-
53,
786+
234,
787787
64,
788788
32,
789-
89,
790-
16,
791-
8,
792-
128,
793-
7
789+
149,
790+
24,
791+
4,
792+
72,
793+
3
794794
]
795795
],
796796
"👀": [

0 commit comments

Comments
 (0)