From c2b163ab5ee21657c0be0d1774efe0f9543cf972 Mon Sep 17 00:00:00 2001 From: Bert Outtier Date: Tue, 17 Mar 2026 21:32:03 +0100 Subject: [PATCH 1/2] add a driver for the 2024 and 2026 communicator (#82) UARt is untested because of REPL being activated on the UART Co-authored-by: Bert Outtier --- .../lib/drivers/fri3d/communicator.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 internal_filesystem/lib/drivers/fri3d/communicator.py diff --git a/internal_filesystem/lib/drivers/fri3d/communicator.py b/internal_filesystem/lib/drivers/fri3d/communicator.py new file mode 100644 index 00000000..e384edca --- /dev/null +++ b/internal_filesystem/lib/drivers/fri3d/communicator.py @@ -0,0 +1,122 @@ +import struct + +from micropython import const +from machine import I2C, UART + +from .device import Device + +# registers +_COMM_REG_KEY_REPORT = const(0x03) +_COMM_REG_CONFIG = const(0x0B) +_COMM_REG_BACKLIGHT = const(0x0C) +_COMM2024_REG_RGB_LED = const(0x0E) +_COMM2024_REG_RED_LED = const(0x11) + +_COMM2024_I2CADDR_DEFAULT = const(0x38) +_COMM2026_I2CADDR_DEFAULT = const(0x39) + + +class Communicator2026(Device): + """Fri3d Badge 2026 expander MCU.""" + + def __init__( + self, + i2c_bus: I2C, + uart_bus: UART = None, + address: int = _COMM2026_I2CADDR_DEFAULT, + ): + """Read from a 2026 communicator""" + Device.__init__(self, i2c_bus, address) + self.use_uart = False + self.write_idx = 0 + self.data_ready = False + if uart_bus: + self.use_uart = True + self.uart = uart_bus.init(115200, bits=8, parity=None, stop=1) + self._rx_buf = bytearray(8) + self._rx_mv = memoryview(self._rx_buf) + self.uart.irq(handler=self.uart_handler, trigger=UART.IRQ_RX) + + def uart_handler(self, uart): + """Interrupt handler for incoming UART data""" + while uart.any() and not self.data_ready: + # Calculate how much space is left + space_left = 8 - self.write_idx + + # Read directly into the slice of the memoryview + # readinto returns the number of bytes actually read + num_read = uart.readinto(self._rx_mv[self.write_idx :], space_left) + + if num_read: + self.write_idx += num_read + + if self.write_idx >= 8: + self.data_ready = True + + @property + def key_report(self) -> tuple[int, int, int, int, int, int, int, int, int]: + """return the key report read using I2C or UART""" + ret = None + if self.use_uart and self.data_ready: + # Process the data (raw_buffer now contains the 8 bytes) + ret = tuple(self._rx_buf) + self.write_idx = 0 + self.data_ready = False + else: + ret = self._read("BBBBBBBB", _COMM_REG_KEY_REPORT, 8) + return ret + + @property + def configuration(self) -> int: + """get the configuration byte""" + return self._read("B", _COMM_REG_CONFIG, 1)[0] + + @configuration.setter + def configuration(self, value: int): + """Set the configuration byte""" + self._write(_COMM_REG_CONFIG, struct.pack("B", value)) + + @property + def backlight(self) -> int: + """Get the backlight value (0-100)""" + return self._read("= 0 and value <= 100: + self.i2c.writeto_mem( + self.address, _COMM_REG_BACKLIGHT, struct.pack(" tuple[int, int, int]: + """Get the LANA module RGB LED""" + return self._read("BBB", _COMM2024_REG_RGB_LED, 3) + + @rgb_led.setter + def rgb_led(self, value: tuple[int, int, int]): + """Set the LANA module RGB LED""" + self._write(_COMM2024_REG_RGB_LED, struct.pack("BBB", *value)) + + @property + def red_led(self) -> int: + """Get the CAPS LED""" + return self._read("B", _COMM2024_REG_RED_LED, 1)[0] + + @red_led.setter + def red_led(self, value: int): + """Set the CAPS LED""" + if value >= 0 and value <= 0xFF: + self._write(_COMM2024_REG_RED_LED, struct.pack("B", value)) From ce92b35741b63ff287100f2376673b4d5f562184 Mon Sep 17 00:00:00 2001 From: Bert Outtier Date: Tue, 17 Mar 2026 21:38:09 +0100 Subject: [PATCH 2/2] add fri3d badge 2026 expander driver (#81) Co-authored-by: Bert Outtier --- .../lib/drivers/fri3d/device.py | 26 ++++++ .../lib/drivers/fri3d/expander.py | 89 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 internal_filesystem/lib/drivers/fri3d/device.py create mode 100644 internal_filesystem/lib/drivers/fri3d/expander.py diff --git a/internal_filesystem/lib/drivers/fri3d/device.py b/internal_filesystem/lib/drivers/fri3d/device.py new file mode 100644 index 00000000..84f5c76f --- /dev/null +++ b/internal_filesystem/lib/drivers/fri3d/device.py @@ -0,0 +1,26 @@ +import struct + +from micropython import const +from machine import I2C + +# common registers +_REG_VERSION = const(0x00) + + +class Device: + """Fri3d I2C device.""" + + def __init__(self, i2c_bus: I2C, address: int): + """Read from a sensor on the given I2C bus, at the given address.""" + self.i2c = i2c_bus + self.address = address + + def _read(self, format, reg, amount): + return struct.unpack(format, self.i2c.readfrom_mem(self.address, reg, amount)) + + def _write(self, reg, value): + self.i2c.writeto_mem(self.address, reg, value, addrsize=8) + + @property + def version(self) -> tuple[int, int, int]: + return self._read("BBB", _REG_VERSION, 3) diff --git a/internal_filesystem/lib/drivers/fri3d/expander.py b/internal_filesystem/lib/drivers/fri3d/expander.py new file mode 100644 index 00000000..c24cbe6f --- /dev/null +++ b/internal_filesystem/lib/drivers/fri3d/expander.py @@ -0,0 +1,89 @@ +import struct + +from micropython import const +from machine import I2C, Pin + +from .device import Device + +# registers +_EXPANDER_REG_INPUTS = const(0x04) +_EXPANDER_REG_ANALOG = const(0x06) +_EXPANDER_REG_LCD_BRIGHTNESS = const(0x12) +_EXPANDER_REG_DEBUG_LED = const(0x14) +_EXPANDER_REG_CONFIG = const(0x16) + +_EXPANDER_I2CADDR_DEFAULT = const(0x50) + + +class Expander(Device): + """Fri3d Badge 2026 expander MCU.""" + + def __init__( + self, + i2c_bus: I2C, + address: int = _EXPANDER_I2CADDR_DEFAULT, + int_pin: Pin = None, + ): + """Read from a sensor on the given I2C bus, at the given address.""" + Device.__init__(self, i2c_bus, address) + self.use_interrupt = False + if int_pin: + self.use_interrupt = True + self._rx_buf = bytearray(2) + self._rx_mv = memoryview(self._rx_buf) + self.int_pin = int_pin + self.i2c.readfrom_mem_into(self.address, _EXPANDER_REG_INPUTS, self._rx_mv) + self.int_pin.irq(trigger=Pin.IRQ_RISING, handler=self.int_callback) + + def int_callback(self, p): + self.i2c.readfrom_mem_into(self.address, _EXPANDER_REG_INPUTS, self._rx_mv) + + @property + def analog(self) -> tuple[int, int, int, int, int, int]: + """Read the analog inputs: ain1, ain0, battery_monitor, usb_monitor, joystick_y, joystick_x""" + return self._read(" tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool]: + """Read the digital inputs: usb_plugged, joy_right, joy_left, joy_down, joy_up, button_menu, button_b, button_a, button_y, button_x, charger_standby, charger_charging""" + if self.use_interrupt: + inputs = struct.unpack(" int: + """Read the LCD brightness state (0-100)""" + return self._read("= 0 and value <= 100: + self._write(_EXPANDER_REG_LCD_BRIGHTNESS, struct.pack(" int: + """Read the Debug LED state (0-100)""" + return self._read("= 0 and value <= 100: + self._write(_EXPANDER_REG_DEBUG_LED, struct.pack(" tuple[bool, bool, bool]: + """Read the configuration bits: reboot, lcd_reset, aux_power""" + config = self._read("B", _EXPANDER_REG_CONFIG, 1)[0] + return tuple([bool(int(digit)) for digit in "{:08b}".format(config)[5:]]) + + @config.setter + def config(self, value: int): + """set the configuration byte""" + if value >= 0 and value <= 0xFF: + self._write(_EXPANDER_REG_CONFIG, struct.pack("B", value))