Skip to content

Commit 87545cc

Browse files
Merge pull request #133 from bertouttier/feat/add_dj_addon_driver
Add a driver and demo app for the DJ add-on
2 parents 9f6694a + 2527fb0 commit 87545cc

4 files changed

Lines changed: 356 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "DJ Addon",
3+
"publisher": "MicroPythonOS",
4+
"short_description": "DJ Addon demo",
5+
"long_description": "Shows off the capabilities of the Fri3d DJ Addon 2026.",
6+
"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.dj_addon/icons/com.micropythonos.dj_addon_0.0.1_64x64.png",
7+
"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.dj_addon/mpks/com.micropythonos.dj_addon_0.0.1.mpk",
8+
"fullname": "com.micropythonos.dj_addon",
9+
"version": "0.0.1",
10+
"category": "development",
11+
"activities": [
12+
{
13+
"entrypoint": "assets/dj_addon.py",
14+
"classname": "DJAddonActivity",
15+
"intent_filters": [
16+
{
17+
"action": "main",
18+
"category": "launcher"
19+
}
20+
]
21+
}
22+
]
23+
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import math
2+
import time
3+
from mpos import DeviceManager
4+
import lvgl as lv
5+
from mpos import Activity, DisplayMetrics
6+
7+
_ADC_MAX = 4095
8+
9+
# ADC channel indices in DMA rank order (rank N → adc_channels[N-1])
10+
_CH_PM_LEFT_BOTTOM = 0 # rank 1
11+
_CH_PM_LEFT_MID = 1 # rank 2
12+
_CH_PM_LEFT_TOP = 2 # rank 3
13+
_CH_SLIDER_LEFT = 3 # rank 4
14+
_CH_PM_RIGHT_BOTTOM = 4 # rank 5
15+
_CH_PM_RIGHT_MID = 5 # rank 6
16+
_CH_PM_RIGHT_TOP = 6 # rank 7
17+
_CH_SLIDER_RIGHT = 7 # rank 8
18+
_CH_SLIDER_MID = 8 # rank 9
19+
20+
_MARGIN = 4
21+
_ARC_GAP = 4
22+
_BTN_GAP = 2
23+
_CROSSFADER_H = 22
24+
_REFRESH_MS = 100
25+
26+
# LVGL arc default angles (0°=east, clockwise). In REVERSE mode the needle
27+
# tip sits at _ARC_END_DEG when value=0 and sweeps _ARC_RANGE_DEG CCW to
28+
# _ARC_START_DEG when value=_ADC_MAX.
29+
_ARC_END_DEG = 45
30+
_ARC_RANGE_DEG = 270
31+
32+
# Maps dj.buttons index → pad_buttons index.
33+
# DJ row 0 (indices 0-3) sits at the bottom of the hardware, which corresponds
34+
# to pad row 1 (indices 4-7) at the bottom of the display grid, and vice versa.
35+
_DJ_TO_PAD = (3,7,1,2,0,5,6,4)
36+
37+
class _MockDJAddon:
38+
version = (1, 0, 0)
39+
40+
@property
41+
def analog(self):
42+
t = time.ticks_ms()
43+
result = []
44+
for i in range(9):
45+
phase = (t + i * 222) % 2000
46+
val = phase if phase < 1000 else 2000 - phase
47+
result.append(int(val * 4.095))
48+
return result
49+
50+
@property
51+
def buttons(self):
52+
idx = (time.ticks_ms() // 1000) % 8
53+
return tuple(i == idx for i in range(8))
54+
55+
def set_led(self, idx, r, g, b):
56+
pass
57+
58+
59+
class DJAddonActivity(Activity):
60+
61+
def __init__(self):
62+
super().__init__()
63+
self.dj = None
64+
self.timer = None
65+
self.arcs_left = [] # list of (arc, needle, cx, cy, r)
66+
self.arcs_right = [] # list of (arc, needle, cx, cy, r)
67+
self.bar_left = None
68+
self.bar_right = None
69+
self.slider_mid = None
70+
self.pad_buttons = []
71+
self.pad_button_states = [] # 0=off, 1=R, 2=G, 3=B cycles on each click
72+
73+
def onCreate(self):
74+
screen = lv.obj()
75+
screen.set_style_bg_color(lv.color_black(), lv.PART.MAIN)
76+
screen.set_style_border_width(0, lv.PART.MAIN)
77+
screen.set_style_pad_all(0, lv.PART.MAIN)
78+
79+
try:
80+
from drivers.fri3d.dj import DJAddon
81+
i2c_bus = DeviceManager.getBus(type="i2c")
82+
self.dj = DJAddon(i2c_bus=i2c_bus)
83+
version = self.dj.version
84+
print("DJ Addon FW version:", ".".join(str(i) for i in version))
85+
if version != (1, 0, 0):
86+
raise ValueError("unexpected firmware version")
87+
except Exception as e:
88+
print("DJ Addon not available, using mock:", e)
89+
self.dj = _MockDJAddon()
90+
91+
self._build_ui(screen)
92+
self.setContentView(screen)
93+
94+
# --- widget factories ---
95+
96+
def _make_arc_at(self, parent, size, x, y):
97+
arc = lv.arc(parent)
98+
arc.set_size(size, size)
99+
arc.set_pos(x, y)
100+
arc.set_range(0, _ADC_MAX)
101+
arc.set_value(0)
102+
arc.set_mode(lv.arc.MODE.REVERSE)
103+
arc.set_style_opa(lv.OPA.TRANSP, lv.PART.INDICATOR)
104+
arc.set_style_opa(lv.OPA.TRANSP, lv.PART.KNOB)
105+
arc.remove_flag(lv.obj.FLAG.CLICKABLE)
106+
107+
cx = x + size // 2
108+
cy = y + size // 2
109+
r = size // 2 - 4
110+
111+
needle = lv.line(parent)
112+
needle.set_style_line_width(4, lv.PART.MAIN)
113+
needle.set_style_line_color(lv.color_white(), lv.PART.MAIN)
114+
needle.set_style_line_rounded(True, lv.PART.MAIN)
115+
self._set_needle(needle, cx, cy, r, 0)
116+
117+
return arc, needle, cx, cy, r
118+
119+
def _make_vbar(self, parent, w, h):
120+
bar = lv.bar(parent)
121+
bar.set_size(w, h)
122+
bar.set_range(0, _ADC_MAX)
123+
bar.set_value(0, False)
124+
bar.remove_flag(lv.obj.FLAG.CLICKABLE)
125+
return bar
126+
127+
def _make_pad_grid(self, parent, x, y, w, h):
128+
btn_w = (w - _BTN_GAP * 3) // 4
129+
btn_h = (h - _BTN_GAP) // 2
130+
self.pad_buttons = []
131+
self.pad_button_states = []
132+
for row in range(2):
133+
for col in range(4):
134+
idx = len(self.pad_buttons)
135+
btn = lv.obj(parent)
136+
btn.set_size(btn_w, btn_h)
137+
btn.set_pos(x + col * (btn_w + _BTN_GAP), y + row * (btn_h + _BTN_GAP))
138+
btn.set_style_bg_color(lv.color_black(), lv.PART.MAIN)
139+
btn.set_style_border_width(2, lv.PART.MAIN)
140+
btn.set_style_border_color(lv.color_hex(0x444444), lv.PART.MAIN)
141+
btn.set_style_radius(3, lv.PART.MAIN)
142+
btn.remove_flag(lv.obj.FLAG.SCROLLABLE)
143+
btn.add_event_cb(lambda _e, i=idx: self._on_pad_click(i), lv.EVENT.CLICKED, None)
144+
self.pad_buttons.append(btn)
145+
self.pad_button_states.append(0)
146+
147+
@staticmethod
148+
def _set_needle(needle, cx, cy, r, value):
149+
angle_rad = math.radians(_ARC_END_DEG - (value / _ADC_MAX) * _ARC_RANGE_DEG)
150+
tx = int(cx + r * math.cos(angle_rad))
151+
ty = int(cy + r * math.sin(angle_rad))
152+
needle.set_points([{'x': cx, 'y': cy}, {'x': tx, 'y': ty}], 2)
153+
154+
def _on_pad_click(self, idx):
155+
_COLORS = ((0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255))
156+
self.pad_button_states[idx] = (self.pad_button_states[idx] + 1) % 4
157+
r, g, b = _COLORS[self.pad_button_states[idx]]
158+
self.set_button_color(idx, r, g, b)
159+
160+
# --- public API ---
161+
162+
def set_button_color(self, idx: int, r: int, g: int, b: int):
163+
if 0 <= idx < len(self.pad_buttons):
164+
self.pad_buttons[idx].set_style_bg_color(lv.color_make(r, g, b), lv.PART.MAIN)
165+
if self.dj is not None:
166+
self.dj.set_led(idx, r, g, b)
167+
168+
# --- layout ---
169+
170+
def _build_ui(self, screen):
171+
W = DisplayMetrics.width()
172+
H = DisplayMetrics.height()
173+
174+
top_h = H - _CROSSFADER_H - _MARGIN * 3
175+
176+
# Three-zone layout: [left deck | button grid | right deck]
177+
center_w = W // 2
178+
side_w = (W - center_w) // 2
179+
center_x = side_w
180+
181+
# Arcs fill the full side width (no separate slider column)
182+
arc_size = min((top_h - _ARC_GAP * 2) // 3, side_w - _MARGIN * 2)
183+
arc_h = 3 * arc_size + 2 * _ARC_GAP
184+
185+
left_arc_x = _MARGIN
186+
right_arc_x = center_x + center_w + _MARGIN
187+
188+
# Left deck: bar first (background), then arcs on top
189+
self.bar_left = self._make_vbar(screen, arc_size, arc_h)
190+
self.bar_left.set_pos(left_arc_x, _MARGIN)
191+
192+
self.arcs_left = []
193+
for i in range(3):
194+
self.arcs_left.append(
195+
self._make_arc_at(screen, arc_size, left_arc_x, _MARGIN + i * (arc_size + _ARC_GAP))
196+
)
197+
198+
# Center: 2-row × 4-column pad button grid
199+
self._make_pad_grid(screen, center_x, _MARGIN, center_w, top_h)
200+
201+
# Right deck: bar first (background), then arcs on top
202+
self.bar_right = self._make_vbar(screen, arc_size, arc_h)
203+
self.bar_right.set_pos(right_arc_x, _MARGIN)
204+
205+
self.arcs_right = []
206+
for i in range(3):
207+
self.arcs_right.append(
208+
self._make_arc_at(screen, arc_size, right_arc_x, _MARGIN + i * (arc_size + _ARC_GAP))
209+
)
210+
211+
# Crossfader: horizontal, full width, bottom
212+
self.slider_mid = lv.slider(screen)
213+
self.slider_mid.set_size(W - _MARGIN * 2, _CROSSFADER_H - 6)
214+
self.slider_mid.set_range(_ADC_MAX, 0)
215+
self.slider_mid.set_value(_ADC_MAX // 2, False)
216+
self.slider_mid.set_style_opa(lv.OPA.TRANSP, lv.PART.INDICATOR)
217+
self.slider_mid.remove_flag(lv.obj.FLAG.CLICKABLE)
218+
self.slider_mid.set_pos(_MARGIN, H - _MARGIN - _CROSSFADER_H)
219+
220+
# --- data update ---
221+
222+
def _update_ui(self, analog, buttons):
223+
vals_left = (analog[_CH_PM_LEFT_TOP], analog[_CH_PM_LEFT_MID], analog[_CH_PM_LEFT_BOTTOM])
224+
vals_right = (analog[_CH_PM_RIGHT_TOP], analog[_CH_PM_RIGHT_MID], analog[_CH_PM_RIGHT_BOTTOM])
225+
226+
for (_, needle, cx, cy, r), val in zip(self.arcs_left, vals_left):
227+
self._set_needle(needle, cx, cy, r, val)
228+
229+
for (_, needle, cx, cy, r), val in zip(self.arcs_right, vals_right):
230+
self._set_needle(needle, cx, cy, r, val)
231+
232+
for dj_idx, pressed in enumerate(buttons):
233+
color = lv.color_white() if pressed else lv.color_hex(0x444444)
234+
self.pad_buttons[_DJ_TO_PAD[dj_idx]].set_style_border_color(color, lv.PART.MAIN)
235+
236+
self.bar_left.set_value(_ADC_MAX - analog[_CH_SLIDER_LEFT], False)
237+
self.bar_right.set_value(_ADC_MAX - analog[_CH_SLIDER_RIGHT], False)
238+
self.slider_mid.set_value(analog[_CH_SLIDER_MID], False)
239+
240+
# --- lifecycle ---
241+
242+
def onResume(self, screen):
243+
if self.timer is None:
244+
self.timer = lv.timer_create(self.refresh, _REFRESH_MS, None)
245+
246+
def onPause(self, screen):
247+
if self.timer:
248+
self.timer.delete()
249+
self.timer = None
250+
251+
def refresh(self, timer):
252+
if self.dj is None:
253+
return
254+
try:
255+
self._update_ui(self.dj.analog, self.dj.buttons)
256+
except Exception as e:
257+
print("DJ refresh error:", e)
6.3 KB
Loading
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import struct
2+
3+
from micropython import const
4+
from machine import I2C, UART
5+
6+
from .device import Device
7+
8+
# registers
9+
_DJ_ADDON_REG_BUTTONS = const(0x03)
10+
_DJ_ADDON_REG_ANALOG = const(0x04)
11+
_DJ_ADDON_REG_LEFT_ENCODER = const(0x16)
12+
_DJ_ADDON_REG_RIGHT_ENCODER = const(0x18)
13+
_DJ_ADDON_REG_LEDS = const(0x1A)
14+
15+
_DJ_ADDON_BAUDRATE = const(115200)
16+
17+
_DJ_ADDON_I2CADDR_DEFAULT = const(0x3A)
18+
19+
20+
class DJAddon(Device):
21+
"""Fri3d Badge 2026 expander MCU."""
22+
23+
def __init__(self, i2c_bus: I2C, uart_bus: UART = None, address: int = _DJ_ADDON_I2CADDR_DEFAULT):
24+
"""Read from a sensor on the given I2C bus, at the given address."""
25+
Device.__init__(self, i2c_bus, address)
26+
self.use_uart = False
27+
self.write_idx = 0
28+
self.data_ready = False
29+
if uart_bus:
30+
self.use_uart = True
31+
self.uart = uart_bus
32+
self.uart.init(_DJ_ADDON_BAUDRATE, bits=8, parity=None, stop=1)
33+
self._rx_buf = bytearray(4)
34+
self._rx_mv = memoryview(self._rx_buf)
35+
self.uart.irq(handler=self.uart_handler, trigger=UART.IRQ_RX)
36+
37+
def uart_handler(self, uart):
38+
"""Interrupt handler for incoming UART data"""
39+
while uart.any() and not self.data_ready:
40+
# Calculate how much space is left
41+
space_left = 4 - self.write_idx
42+
43+
# Read directly into the slice of the memoryview
44+
# readinto returns the number of bytes actually read
45+
num_read = uart.readinto(self._rx_mv[self.write_idx :], space_left)
46+
47+
if num_read:
48+
self.write_idx += num_read
49+
50+
if self.write_idx >= 4:
51+
self.data_ready = True
52+
53+
@property
54+
def buttons(self) -> tuple[bool, bool, bool, bool, bool, bool, bool, bool]:
55+
buttons = self._read("B", _DJ_ADDON_REG_BUTTONS, 1)[0]
56+
return tuple([bool(int(digit)) for digit in "{:08b}".format(buttons)])
57+
58+
@property
59+
def analog(self) -> tuple[int, int, int, int, int, int, int, int, int]:
60+
return self._read("<HHHHHHHHH", _DJ_ADDON_REG_ANALOG, 18)
61+
62+
@property
63+
def left_encoder(self) -> int:
64+
return self._read("<H", _DJ_ADDON_REG_LEFT_ENCODER, 2)[0]
65+
66+
@property
67+
def right_encoder(self) -> int:
68+
return self._read("<H", _DJ_ADDON_REG_RIGHT_ENCODER, 2)[0]
69+
70+
def set_led(self, idx: int, r: int, g: int, b: int):
71+
self._write(_DJ_ADDON_REG_LEDS + (idx * 3), struct.pack("BBB", g, r, b))
72+
73+
def send_midi(self, data: bytes):
74+
if self.use_uart and len(data) == 4:
75+
self.uart.write(data)
76+
self.uart.flush()

0 commit comments

Comments
 (0)