-
Notifications
You must be signed in to change notification settings - Fork 0
Integration and Scenes
This guide covers how to integrate glowctl as a Python library, configure device settings, and build custom animation scenes using parameter synthesis.
-
Integrating
glowctlas a Library - Configuring Device Settings
- Building Custom Scenes & Dynamic Animations
glowctl provides an asyncio-based Python API built on top of bleak and cbor2.
import asyncio
from glowctl import (
DiscoveredLamp,
LampTransport,
discover,
modes,
protocol,
const,
)Use discover() to scan for advertising Glowrium lamps:
# Scan for up to 10 seconds and return as soon as one lamp is found
lamps = await discover(timeout=10.0, stop_on_first=True)
if not lamps:
print("No lamp found.")
return
lamp_info: DiscoveredLamp = lamps[0]
print(f"Found {lamp_info.name} at {lamp_info.address} (RSSI: {lamp_info.rssi})")Manage connection lifecycle using LampTransport as an async context manager:
async with LampTransport(lamp_info.device) as lamp:
# Check connection status
print(f"Connected: {lamp.is_connected}")
# Send power-on command
await lamp.write(protocol.encode_power(True))If you already know the BLE MAC address or UUID, pass it directly:
async with LampTransport("AA:BB:CC:DD:EE:FF") as lamp:
await lamp.write(protocol.encode_brightness(80))Read the complete state map or specific characteristics:
async with LampTransport(lamp_info.device) as lamp:
# Read and decode CBOR property map
state = await lamp.read_state()
# Print annotated property summary
for line in protocol.describe_state(state):
print(line)
# Parse 20 spatial segments (top to bottom)
raw_segments = state.get(const.KEY_SEGMENTS)
if isinstance(raw_segments, bytes):
segments = protocol.segments_from_top(raw_segments)
for idx, (r, g, b, y) in enumerate(segments):
print(f"Segment {idx:2d}: R={r} G={g} B={b} Y={y}")Register a callback to process live status notifications push-event updates:
def handle_notification(char_short: str, payload: bytes):
try:
decoded = protocol.decode_state(payload)
print(f"Notification from {char_short}: {decoded}")
except Exception:
print(f"Raw notification from {char_short}: {payload.hex()}")
async with LampTransport(lamp_info.device, notify_callback=handle_notification) as lamp:
# Stream notifications for 30 seconds
await asyncio.sleep(30.0)Writing unrecognized CBOR property keys is guarded by UnsafeProperty exceptions to prevent accidental firmware misconfiguration:
try:
# Attempting to write an unconfirmed property key will raise UnsafeProperty
await lamp.send_properties({125: b"\x00" * 10})
except protocol.UnsafeProperty as exc:
print(f"Safety guard blocked write: {exc}")
# To explicitly override safety guards for experimental probing:
await lamp.send_properties({125: b"\x00" * 10}, allow_unsafe=True)Every setting helper returns raw CBOR bytes ready to send via lamp.write().
# Power controls
await lamp.write(protocol.encode_power(True)) # On
await lamp.write(protocol.encode_power(False)) # Off
# Global luminance (0 to 100)
await lamp.write(protocol.encode_brightness(75))The column contains 20 addressable segments. encode_segment_colours() accepts 20 RGBY tuples ordered top-to-bottom:
# Solid color across all 20 segments (RGB + Yellow channel)
await lamp.write(protocol.encode_colour(255, 0, 0, 0)) # Red
await lamp.write(protocol.encode_colour(0, 255, 0, 100)) # Green + Warm Yellow
# Top-to-bottom spatial color gradient
gradient = [
(int(255 * (1 - i / 19)), int(255 * (i / 19)), 0, 50)
for i in range(20)
]
await lamp.write(protocol.encode_segment_colours(gradient, from_top=True))Program hourly chime notification windows:
# Enable chime between 07:00 (25200s) and 22:00 (79200s)
await lamp.write(protocol.encode_chime(
enabled=True,
start_seconds=7 * 3600,
end_seconds=22 * 3600,
))Configure smooth luminance fade durations for power on/off transitions:
# Set 15-second gradual fade transition
await lamp.write(protocol.encode_gradual(enabled=True, duration_seconds=15))Program hardware countdown timer:
# Start 10-minute countdown (600 seconds)
await lamp.write(protocol.encode_countdown(enabled=True, seconds=600))
# Disable countdown
await lamp.write(protocol.encode_countdown(enabled=False, seconds=0))Program scheduled sunrise and sunset scene triggers:
# Sunrise at 06:30 (23400s), Sunset at 21:00 (75600s)
await lamp.write(protocol.encode_sunrise_sunset(
enabled=True,
rise_seconds=6 * 3600 + 30 * 60,
set_seconds=21 * 3600,
))The hardware supports 5 timer slots (GTimeDat0 .. GTimeDat4). Edit slot configurations using a captured slot template:
# Read current device state to get a template slot
state = await lamp.read_state()
template_slot = state.get(const.PROPERTIES[41].key) # Slot 2 (key 41)
# Build a daily timer for 07:30 AM turning the lamp ON
new_slot = protocol.build_timer_slot(
template_slot,
hour=7,
minute=30,
enabled=True,
days=protocol.TIMER_DAILY, # Mon-Sun mask (0x7F)
action="on",
)
# Write to Slot 2 (key 41)
await lamp.send_properties({41: new_slot}, allow_unsafe=True)Lighting modes are defined by two properties written together:
-
ModeCtr(key 19): 3-byte mode identifier. -
DyData(key 24): 100-byte program consisting of a 20-byte header and a 20-step RGBY palette array.
frame = protocol.encode_mode(modectr_bytes, dydata_bytes)
await lamp.write(frame)Because DyData headers contain dynamic-type-specific parameters, custom scenes are synthesized by overriding parameters of a real captured mode template:
# Load a captured template
template_dydata = modes.get("custom_descent")[24]
# Synthesize new program parameters
custom_program = protocol.build_dydata(
template_dydata,
palette=[(255, 0, 0, 0), (0, 255, 0, 0), (0, 0, 255, 0)],
led_count=10,
speed=40,
brightness=100,
direction=1,
tail_length=2,
)| Parameter | Type / Range | Description |
|---|---|---|
template |
bytes (100B) |
Required. Base captured DyData program. |
palette |
list[tuple] |
List of up to 20 (R, G, B, Y) tuples (0-255 each). |
palette_length |
int (1-20) |
Number of active palette colors the device cycles through. |
led_count |
int (1-20) |
Number of vertical segments the animation spans. |
speed |
int (0-100) |
Animation speed scale (stored inverted in header). |
speed_mode |
int (0 / 1) |
0: Accelerating step, 1: Uniform motion. |
brightness |
int (0-100) |
Mode luminance level. |
direction |
int (0 / 1) |
Animation flow direction (e.g. downward vs upward). |
tail_length |
int (0-3) |
Animation trail length / decay steps. |
colour_mode |
int |
Palette iteration mode (cycle_rotation, random, first). |
background_brightness |
int (0-100) |
Brightness of unlit segments outside led_count. |
The following script synthesizes a custom multi-colored wave effect over a captured template and applies it to the hardware:
import asyncio
from glowctl import LampTransport, build_dydata, discover, modes, protocol
async def main():
# 1. Discover lamp
lamps = await discover(stop_on_first=True)
if not lamps:
print("No lamp found.")
return
# 2. Retrieve base template from captured profiles
mode_profile = modes.get("custom_descent")
modectr = mode_profile[19]
template_dydata = mode_profile[24]
# 3. Build custom animation program
# Define a 5-color palette (Red, Amber, Green, Cyan, Blue)
custom_palette = [
(255, 0, 0, 0), # Red
(255, 128, 0, 50), # Amber + Yellow
(0, 255, 0, 0), # Green
(0, 255, 255, 0), # Cyan
(0, 0, 255, 0), # Blue
]
custom_dydata = build_dydata(
template_dydata,
palette=custom_palette,
palette_length=len(custom_palette),
led_count=15,
speed=60,
brightness=100,
direction=1,
tail_length=3,
background_brightness=10,
)
# 4. Encode and transmit frame
frame = protocol.encode_mode(modectr, custom_dydata)
async with LampTransport(lamps[0].device) as lamp:
await lamp.write(frame)
print("Custom scene transmitted successfully.")
if __name__ == "__main__":
asyncio.run(main())