Skip to content

Commit bcc237a

Browse files
committed
feat(ble): Add BLE beacon advertising example with OLED display.
1 parent 781135c commit bcc237a

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""BLE beacon advertising example using aioble and SSD1327 OLED.
2+
3+
Broadcasts a BLE advertisement containing the board name and a live
4+
temperature reading from the WSEN-PADS sensor. The beacon is visible
5+
from any BLE scanner app (nRF Connect, LightBlue, etc.).
6+
7+
Hardware:
8+
- STM32WB55 BLE radio
9+
- SSD1327 128x128 OLED display (round)
10+
- WSEN-PADS pressure + temperature sensor
11+
12+
BLE payload:
13+
- Complete Local Name: "STeaMi-XXXX" (last 2 bytes of MAC address)
14+
- Manufacturer Specific Data: temperature as int16 (x100, in 0.01 C)
15+
"""
16+
17+
import sys
18+
19+
sys.path.insert(0, "/remote")
20+
21+
import bluetooth
22+
import struct
23+
import uasyncio as asyncio
24+
25+
import aioble
26+
import ssd1327
27+
from machine import I2C, SPI, Pin
28+
from steami_screen import GRAY, GREEN, LIGHT, Screen, SSD1327Display, WHITE
29+
from wsen_pads import WSEN_PADS
30+
31+
# === BLE setup ===
32+
ble = bluetooth.BLE()
33+
ble.active(True)
34+
35+
mac_bytes = ble.config("mac")[1]
36+
mac_suffix = "".join(f"{b:02X}" for b in mac_bytes[-2:])
37+
DEVICE_NAME = f"STeaMi-{mac_suffix}"
38+
print("Device name:", DEVICE_NAME)
39+
40+
# === Display ===
41+
spi = SPI(1)
42+
dc = Pin("DATA_COMMAND_DISPLAY")
43+
res = Pin("RST_DISPLAY")
44+
cs = Pin("CS_DISPLAY")
45+
display = SSD1327Display(ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs))
46+
screen = Screen(display)
47+
48+
# === Sensor ===
49+
i2c = I2C(1)
50+
pads = WSEN_PADS(i2c)
51+
52+
# === BLE parameters ===
53+
ADV_INTERVAL_US = 200_000 # 200 ms between advertisements
54+
ADV_TIMEOUT_MS = 500 # advertise for 500 ms per cycle
55+
SENSOR_INTERVAL_MS = 1000 # read sensor every 1 second
56+
57+
# === Shared state ===
58+
temperature = 0.0
59+
60+
61+
def build_adv_payload(name, temp_raw):
62+
"""Build a BLE advertising payload.
63+
64+
Contains:
65+
- Complete Local Name (type 0x09)
66+
- Manufacturer Specific Data (type 0xFF): int16 temperature x100
67+
"""
68+
payload = bytearray()
69+
70+
# Complete Local Name
71+
name_bytes = name.encode()
72+
payload += bytes((len(name_bytes) + 1, 0x09)) + name_bytes
73+
74+
# Manufacturer Specific Data: temperature encoded as int16 (x100)
75+
man_data = struct.pack("h", temp_raw)
76+
payload += bytes((len(man_data) + 1, 0xFF)) + man_data
77+
78+
return payload
79+
80+
81+
async def sensor_task():
82+
"""Read temperature from WSEN-PADS every second."""
83+
global temperature
84+
while True:
85+
try:
86+
temperature = pads.temperature()
87+
except Exception as e:
88+
print("Sensor error:", e)
89+
await asyncio.sleep_ms(SENSOR_INTERVAL_MS)
90+
91+
92+
async def ble_task():
93+
"""Advertise BLE beacon with device name and temperature."""
94+
while True:
95+
# Encode temperature as int16 (multiply by 100 to keep 2 decimals)
96+
temp_raw = int(temperature * 100)
97+
adv_payload = build_adv_payload(DEVICE_NAME, temp_raw)
98+
99+
try:
100+
await aioble.advertise(
101+
interval_us=ADV_INTERVAL_US,
102+
adv_data=adv_payload,
103+
connectable=False,
104+
timeout_ms=ADV_TIMEOUT_MS,
105+
)
106+
except asyncio.TimeoutError:
107+
pass # Normal: non-connectable advertisement timeout
108+
109+
await asyncio.sleep_ms(100)
110+
111+
112+
async def display_task():
113+
"""Update OLED display with current beacon state."""
114+
while True:
115+
screen.clear()
116+
screen.title("BLE BEACON")
117+
screen.value(f"{temperature:.1f}", unit="C")
118+
screen.subtitle(DEVICE_NAME, "Advertising...")
119+
screen.show()
120+
await asyncio.sleep_ms(500)
121+
122+
123+
async def main():
124+
await asyncio.gather(
125+
sensor_task(),
126+
ble_task(),
127+
display_task(),
128+
)
129+
130+
131+
asyncio.run(main())

0 commit comments

Comments
 (0)