Fixes and new apps (#30)

- M5 Unit Modules library + M5 Unit Test app
- Minor fixes for TodoList, TwoEleven, Snake, Brainfuck and Breakout
- Fixed SerialConsole to use the uart controller as it was broken in one of the many updates
- Fixed keyboard input in TwoEleven, Breakout, Magic8Ball and Snake
- Bluetooth Media Keys app, supports pressing physical keys to trigger the corresponding buttonmatrix button
- Epub Reader app
This commit is contained in:
Shadowtrance
2026-06-07 23:56:32 +10:00
committed by GitHub
parent 1f959c8bbf
commit dbf850c434
127 changed files with 11156 additions and 174 deletions
@@ -0,0 +1,90 @@
/*
* Unit8Encoder.h - M5Stack 8Encoder Unit (STM32, I2C addr 0x41)
*
* 8 rotary encoders with push buttons, individual RGB LEDs, plus one toggle
* switch with its own RGB LED.
*
* Register map:
* 0x00 + i*4 : int32_t absolute counter for encoder i (0-3) [R/W]
* 0x10 + i*4 : int32_t absolute counter for encoder i (4-7) [R/W]
* 0x20 + i*4 : int32_t increment (delta) for encoder i (0-3), resets after read [R]
* 0x30 + i*4 : int32_t increment (delta) for encoder i (4-7), resets after read [R]
* 0x40 + i : uint8_t write 1 to reset counter i to 0 (i = 0-7) [W]
* 0x50 + i : uint8_t button state for encoder i (0=released, 1=pressed) [R]
* 0x60 : uint8_t toggle switch state (0=off, 1=on) [R]
* 0x70 + i*3 : [R, G, B] LED colour for encoder i (i = 0-7, 0x00RRGGBB) [R/W]
* 0x88 : [R, G, B] LED colour for the toggle switch (LED8) [R/W]
* 0xF0 : uint8_t firmware version [R]
* 0xFF : uint8_t I2C address (R/W)
*
* LED indices: 0-7 = encoder LEDs, 8 = switch LED (total 9 LEDs).
*
* Usage:
* Unit8Encoder enc;
* if (enc.begin(dev)) { ... } // dev = device_find_by_name("i2c1")
* enc.readAll(deltas, buttons); // deltas in detents (÷4), buttons 0/1
* bool sw = false;
* if (enc.readSwitch(sw)) { sw holds switch state } // false = read failed
* enc.setAllLeds(0xFF0000); // set all 9 LEDs (encoders + switch) to red
* enc.setSwitchLed(0x00FF00); // set switch LED colour only
*/
#pragma once
#include <UnitCommon.h>
#include <cstdint>
class Unit8Encoder {
public:
static constexpr uint8_t DEFAULT_ADDR = 0x41;
static constexpr uint8_t LED_COUNT = 9; // 8 encoder LEDs + 1 switch LED
static constexpr uint8_t ENCODER_LED_COUNT = 8;
Unit8Encoder() = default;
Unit8Encoder(const Unit8Encoder&) = delete;
Unit8Encoder& operator=(const Unit8Encoder&) = delete;
// Pass a I2C controller device
// Probe and initialise. Returns false if the unit is not present.
[[nodiscard]] bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR);
bool isPresent() const { return dev_ != nullptr; }
// Read all 8 encoder deltas (in detents, hardware counts ÷4) and button states.
// Returns false on I2C error.
bool readAll(int32_t deltas[8], uint8_t buttons[8]);
// Read the toggle switch state into `state`. Returns false on I2C error.
// On error, `state` is left unchanged. Use the return value to distinguish
// "switch off" (returns true, state=false) from read failure (returns false).
bool readSwitch(bool& state);
// Set one LED immediately (0x00RRGGBB). Updates cached colour.
// idx 0-7 = encoder LEDs, idx 8 = switch LED. Silently ignores idx > 8.
void setLed(uint8_t idx, uint32_t rgb);
// Set the switch LED colour (0x00RRGGBB) immediately.
void setSwitchLed(uint32_t rgb);
// Flush pending[LED_COUNT] to hardware if anything changed.
// pending[0-7] = encoder LEDs, pending[8] = switch LED.
// Call every poll with desired colours; only sends if dirty.
void flushLeds(const uint32_t pending[LED_COUNT]);
// Set all 9 LEDs (encoder + switch) to one colour immediately.
void setAllLeds(uint32_t rgb);
// Cached LED colours (0x00RRGGBB). Read-only - updated by set*/flush*.
// Index 0-7 = encoder LEDs, index 8 = switch LED.
const uint32_t* ledColors() const { return ledColor_; }
private:
Device* dev_ = nullptr;
uint8_t addr_ = DEFAULT_ADDR;
uint32_t ledColor_[LED_COUNT] = {};
static constexpr uint8_t REG_INCREMENT = 0x20;
static constexpr uint8_t REG_BUTTON = 0x50;
static constexpr uint8_t REG_SWITCH = 0x60;
static constexpr uint8_t REG_LED = 0x70; // encoder LEDs 0-7 (24 bytes)
static constexpr uint8_t REG_SWITCH_LED = 0x88; // switch LED (3 bytes)
};
@@ -0,0 +1,77 @@
/*
* UnitByteButton.h - M5Stack ByteButton Unit (STM32, I2C addr 0x47)
*
* 8 momentary buttons with individual RGB LEDs.
*
* Register map:
* 0x00 : uint8_t all 8 button states as a bitmask (bit i = button i)
* 0x10 + i : uint8_t LED brightness for button i (0-255)
* 0x19 : uint8_t LED show mode (0=user-defined, 1=system-default)
* 0x20 + i*4 : uint32_t LED RGB colour for button i (LE: bytes = [B,G,R,0x00])
* 0x50 + i : uint8_t LED colour in RGB233 compressed format
* 0x60 + i : uint8_t individual button state (0=released, non-zero=pressed)
* 0x70 + i*4 : uint32_t system "off" colour for button i
* 0x90 + i*4 : uint32_t system "on" colour for button i
* 0xFE : uint8_t firmware version
* 0xFF : uint8_t I2C address (R/W)
*
* LED colour format: write uint32_t 0x00RRGGBB as little-endian → wire sees [BB, GG, RR, 00]
*
* Usage:
* UnitByteButton bb;
* if (bb.begin(dev)) { ... }
* uint8_t mask = bb.readButtons(); // bitmask, bit 0 = button 0
* bb.setLed(i, 0xFF4400); // per-button
* bb.flushLeds(pending); // burst write if dirty
*/
#pragma once
#include <UnitCommon.h>
#include <cstdint>
class UnitByteButton {
public:
static constexpr uint8_t DEFAULT_ADDR = 0x47;
static constexpr uint8_t BUTTON_COUNT = 8;
UnitByteButton() = default;
UnitByteButton(const UnitByteButton&) = delete;
UnitByteButton& operator=(const UnitByteButton&) = delete;
// Pass a I2C controller device
// Probe and initialise. Returns false if not present.
// Sets show mode to user-defined so setLed/flushLeds take effect.
[[nodiscard]] bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR);
bool isPresent() const { return dev_ != nullptr; }
// Read all 8 buttons as a bitmask. Bit i = button i (1=pressed).
// Returns the bitmask via value; sets *ok=false on I2C error (ok may be nullptr).
uint8_t readButtons(bool* ok = nullptr);
// Read individual button state (idx 0-7). Returns true if pressed.
bool readButton(uint8_t idx);
// Set one LED (0x00RRGGBB, idx 0-7). Updates cache and sends immediately.
void setLed(uint8_t idx, uint32_t rgb);
// Flush pending[BUTTON_COUNT] to hardware in one burst if anything changed
// since the last flush. pending[] entries are 0x00RRGGBB.
void flushLeds(const uint32_t pending[BUTTON_COUNT]);
// Set all 8 LEDs to one colour immediately.
void setAllLeds(uint32_t rgb);
const uint32_t* ledColors() const { return ledColor_; }
private:
Device* dev_ = nullptr;
uint8_t addr_ = DEFAULT_ADDR;
uint32_t ledColor_[BUTTON_COUNT] = {};
static constexpr uint8_t REG_STATUS = 0x00;
static constexpr uint8_t REG_SHOW_MODE = 0x19;
static constexpr uint8_t REG_RGB888 = 0x20; // + i*4, LE uint32_t
static constexpr uint8_t REG_STATUS_8 = 0x60; // + i, individual
};
@@ -0,0 +1,95 @@
/*
* UnitCardKB2.h - M5Stack CardKB V2 keyboard unit
*
* Supports two connection modes:
* I2C (addr 0x5F, factory default) - via GROVE port
* UART (115200-8N-1) - via GROVE port after Fn+Sym+2 mode switch
*
* I2C: plain 1-byte read returns ASCII of held key (0 = none).
*
* UART frame: AA [DATA_LEN=0x03] [KEY_ID] [KEY_STATE] [checksum]
* KEY_ID = row*11 + col (0-43, rows 0-3, cols 0-10)
* KEY_STATE = 0x01 pressed, 0x02 released
* checksum = (DATA_LEN + KEY_ID + KEY_STATE) & 0xFF
*
* Special keys (UART translation by this driver; NOT available in I2C mode):
* Esc=0x1B Del=0x08 Enter=0x0A Space=0x20
* Fn+D=0x1E(up) Fn+X=0x1F(down) Fn+Z=0x1D(left) Fn+C=0x1C(right)
*
* I2C mode: Fn+D/X/Z/C and Fn+1 (Esc) produce no output - the firmware only
* pushes regular ASCII into the I2C queue; Fn combos go to BLE HID directly.
* UART mode: Fn combos work; firmware sends raw KEY_ID, driver translates.
* BLE HID mode: USB HID keycodes (ESC=0x29, arrows=0x4F-0x52), handled by
* BluetoothHidHost → LV_KEY_*.
*
* Mode switch: Fn+Sym+1 = I2C, Fn+Sym+2 = UART (persists across reboot)
*/
#pragma once
#include <UnitCommon.h>
#include <tactility/drivers/uart_controller.h>
#include <cstdint>
class UnitCardKB2 {
public:
enum class Mode { I2C, Uart };
static constexpr uint8_t DEFAULT_ADDR = 0x5F;
UnitCardKB2() = default;
~UnitCardKB2() { end(); }
UnitCardKB2(const UnitCardKB2&) = delete;
UnitCardKB2& operator=(const UnitCardKB2&) = delete;
// Pass a I2C controller device
// I2C mode - probe and initialise. Returns false if not present.
[[nodiscard]] bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR);
// Pass a UART controller device
// UART mode - set_config then open uart_controller.
// Returns false if the device is null or open fails.
[[nodiscard]] bool beginUart(Device* dev);
void end();
bool isPresent() const { return mode_ == Mode::I2C ? i2cDev_ != nullptr : uartDev_ != nullptr; }
// Only meaningful when isPresent() returns true.
// Call end() before switching modes (begin→beginUart or vice versa).
Mode mode() const { return mode_; }
// Returns the ASCII value of the currently/last pressed key, or 0 if none.
// In UART mode: drains available bytes from the UART RX buffer, parses frames,
// returns the ASCII of the most recently pressed key (releases are ignored).
// In I2C mode: behaviour unchanged from before.
char getKey();
// Returns true if a key is currently held (I2C mode only; always false in UART mode).
bool hasKey();
private:
// I2C
Device* i2cDev_ = nullptr;
uint8_t addr_ = DEFAULT_ADDR;
char cachedKey_ = 0;
// UART
Device* uartDev_ = nullptr;
// Frame parser state
enum class FrameState : uint8_t { WaitAA, WaitLen, WaitId, WaitState, WaitCsum };
FrameState frameState_ = FrameState::WaitAA;
uint8_t frameId_ = 0;
uint8_t frameKs_ = 0;
// UART modifier state (tracked from key events)
bool capsLock_ = false;
bool symMode_ = false;
bool fnHeld_ = false;
bool oneShiftPending_ = false; // Aa single-tap one-shot uppercase
uint32_t lastAATimestamp_ = 0; // ms timestamp of the Aa press that set oneShiftPending_
Mode mode_ = Mode::I2C;
char readFromI2C();
char pollUart();
};
@@ -0,0 +1,56 @@
/*
* UnitCommon.h - shared I2C helpers for M5Stack STM32-based units
*
* All M5Stack Grove units with STM32 firmware require a STOP condition between
* the register-pointer write and the data read (the STM32 HAL receive callback
* populates tx_buffer after STOP, before the next START). Using a repeated-START
* (the default i2c_controller_read_register) causes bus errors on every read.
*
* Pattern: write(reg) → STOP → vTaskDelay(2ms) → read(data)
*/
#pragma once
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <cstring>
static constexpr uint32_t UNIT_I2C_TIMEOUT_MS = 20;
static constexpr uint32_t UNIT_I2C_PROBE_TIMEOUT_MS = 10; // shorter timeout for presence checks
static constexpr uint32_t UNIT_STM32_READ_DELAY_MS = 2; // STM32 needs STOP + delay before read
static constexpr uint16_t UNIT_MAX_WRITE_PAYLOAD = 32; // max bytes in a single I2C write
// Probe whether a device exists at the given address on the given I2C bus.
inline bool unitProbe(Device* dev, uint8_t addr) {
return i2c_controller_has_device_at_address(
dev, addr, pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) == ERROR_NONE;
}
// Write [reg, buf...] as a single transaction (STOP at end).
inline bool unitWriteReg(Device* dev, uint8_t addr, uint8_t reg,
const uint8_t* buf, uint16_t len) {
if (len > UNIT_MAX_WRITE_PAYLOAD) return false;
if (len > 0 && buf == nullptr) return false;
uint8_t pkt[33];
pkt[0] = reg;
if (len) memcpy(pkt + 1, buf, len);
return i2c_controller_write(dev, addr, pkt, (uint16_t)(len + 1),
pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE;
}
// Write register pointer only (no data payload) - convenience wrapper.
inline bool unitWriteRegPtr(Device* dev, uint8_t addr, uint8_t reg) {
return i2c_controller_write(dev, addr, &reg, 1,
pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE;
}
// Read from a register: write pointer → STOP → 2ms delay → read.
inline bool unitReadReg(Device* dev, uint8_t addr, uint8_t reg,
uint8_t* buf, uint16_t len) {
if (len > 0 && buf == nullptr) return false;
if (!unitWriteRegPtr(dev, addr, reg)) return false;
vTaskDelay(pdMS_TO_TICKS(UNIT_STM32_READ_DELAY_MS));
return i2c_controller_read(dev, addr, buf, len,
pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE;
}
@@ -0,0 +1,60 @@
/*
* UnitDualButton.h - M5Stack Dual Button Unit (GPIO, Port B)
*
* Two independent momentary push buttons connected to GPIO pins.
* Buttons are active-low - pressing pulls the pin to GND.
* Internal pull-up resistors are enabled by the driver.
*
* Typical Port B pin assignment (M5Stack Core2):
* Button A (red) - GPIO 36
* Button B (blue) - GPIO 26
*
* Usage:
* UnitDualButton btn;
* Device* gpioCtrl = device_find_by_name("gpio0");
* if (btn.begin(gpioCtrl, 36, 26)) {
* bool a = btn.isButtonAPressed();
* bool b = btn.isButtonBPressed();
* }
* // When done: btn.end() or let destructor release pins
*/
#pragma once
#include <tactility/device.h>
#include <tactility/drivers/gpio_controller.h>
#include <cstdint>
class UnitDualButton {
public:
~UnitDualButton();
UnitDualButton() = default;
UnitDualButton(const UnitDualButton&) = delete;
UnitDualButton& operator=(const UnitDualButton&) = delete;
UnitDualButton(UnitDualButton&&) = delete;
UnitDualButton& operator=(UnitDualButton&&) = delete;
// Pass a GPIO controller device
// Acquire and configure both GPIO pins as inputs with pull-up.
// controller: GPIO controller device (e.g. device_find_by_name("gpio0"))
// pinA / pinB: physical GPIO pin numbers
bool begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB);
// Release GPIO descriptors. Safe to call multiple times.
void end();
bool isPresent() const { return ready_; }
// True when button A is pressed (active-low: pin reads low).
bool isButtonAPressed() const;
// True when button B is pressed (active-low: pin reads low).
bool isButtonBPressed() const;
private:
GpioDescriptor* descA_ = nullptr;
GpioDescriptor* descB_ = nullptr;
bool ready_ = false;
static bool readPin(GpioDescriptor* desc);
};
@@ -0,0 +1,67 @@
/*
* UnitJoystick2.h - M5Stack Joystick2 Unit (STM32, I2C addr 0x63)
*
* XY analogue joystick with push button and one RGB LED.
*
* Register map:
* 0x00 : uint16_t[2] X, Y raw 12-bit ADC (4 bytes LE, 0-4095)
* 0x10 : uint8_t[2] X, Y 8-bit ADC (2 bytes)
* 0x20 : uint8_t button (0=pressed, 1=released - inverted!)
* 0x30 : uint32_t RGB LED colour (LE, 0x00RRGGBB)
* 0x40 : uint16_t[8] calibration data (16 bytes)
* 0x50 : int16_t[2] offset-corrected 12-bit X, Y
* 0x60 : int8_t[2] offset-corrected 8-bit X, Y
* 0xFE : uint8_t firmware version
* 0xFF : uint8_t I2C address (R/W)
*
* Coordinate conventions after read:
* Raw 12-bit: 0-4095, centre ~2048
* Offset value: signed, centre = 0
* Button: isPressed() returns true when button is pressed (inverts the 0=pressed register)
*
* Usage:
* UnitJoystick2 joy;
* if (joy.begin(dev)) { ... }
* int16_t x, y; joy.readXY12(&x, &y); // offset-corrected, centre=0
* bool btn = joy.isPressed();
* joy.setLed(0xFF0000);
*/
#pragma once
#include <UnitCommon.h>
#include <cstdint>
class UnitJoystick2 {
public:
static constexpr uint8_t DEFAULT_ADDR = 0x63;
// Pass a I2C controller device
[[nodiscard]] bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR);
bool isPresent() const { return dev_ != nullptr; }
// Read offset-corrected 12-bit XY. Centre = 0, range ~±2048.
bool readXY12(int16_t* x, int16_t* y);
// Read raw 12-bit XY (0-4095, unsigned). Centre ~2048.
bool readXY12Raw(uint16_t* x, uint16_t* y);
// Read offset-corrected 8-bit XY. Centre = 0, range ~±128.
bool readXY8(int8_t* x, int8_t* y);
// Button state. True = pressed (inverts hardware 0=pressed convention).
bool isPressed() const;
// Set the RGB LED colour (0x00RRGGBB). Returns false on I2C error.
bool setLed(uint32_t rgb);
private:
Device* dev_ = nullptr;
uint8_t addr_ = DEFAULT_ADDR;
static constexpr uint8_t REG_ADC_12BIT = 0x00;
static constexpr uint8_t REG_ADC_8BIT = 0x10;
static constexpr uint8_t REG_BUTTON = 0x20;
static constexpr uint8_t REG_RGB = 0x30;
static constexpr uint8_t REG_OFFSET_12BIT = 0x50;
static constexpr uint8_t REG_OFFSET_8BIT = 0x60;
};
+176
View File
@@ -0,0 +1,176 @@
/*
* UnitLcd.h - M5Stack LCD Unit (ESP32-PICO inside, I2C addr 0x3E)
*
* 135×240 IPS display driven by an internal ESP32 that bridges I2C commands
* to an ST7789V2. Unlike the STM32-based units there is no register map -
* instead you send drawing commands into a command buffer. The internal ESP32
* processes them independently, so writes are fire-and-forget for most ops.
*
* Key differences from STM32 units:
* - No STOP+delay+read pattern - standard repeated-START reads work fine
* - No 2ms inter-transaction delay needed
* - Pixel data goes in a single uninterrupted I2C transaction (WRITE_RAW)
* - Check READ_BUFCOUNT (0x09) before large writes to avoid overflow
* - Max I2C clock: 400 kHz
*
* Physical display: 135 wide × 240 tall (portrait, rotation 0/2)
* 240 wide × 135 tall (landscape, rotation 1/3)
*
* Coordinate system: origin top-left, X right, Y down, in the current
* rotation's logical space. Use width()/height() for safe bounds.
*
* Command quick-reference:
* 0x22 [brightness] Set backlight (0=off, 255=max)
* 0x36 [rotation] Set rotation (0-3 = 0/90/180/270°)
* 0x2A [x0] [x1] Set X window (column range, inclusive)
* 0x2B [y0] [y1] Set Y window (row range, inclusive)
* 0x68 [x0][y0][x1][y1] Fill rect with stored colour
* 0x6A [x0][y0][x1][y1][hi][lo] Fill rect with RGB565 colour inline
* 0x62 [x][y][hi][lo] Draw pixel RGB565
* 0x42 [pixels...] Write raw RGB565 stream (STOP ends it)
* 0x09 Read buffer remaining count (1 byte)
*/
#pragma once
#include <UnitCommon.h>
#include <cstdint>
class UnitLcd {
public:
static constexpr uint8_t DEFAULT_ADDR = 0x3E;
static constexpr uint16_t PHYS_WIDTH = 135;
static constexpr uint16_t PHYS_HEIGHT = 240;
// Legacy constants - equal to physical portrait dimensions.
static constexpr uint16_t WIDTH = PHYS_WIDTH;
static constexpr uint16_t HEIGHT = PHYS_HEIGHT;
UnitLcd() = default;
UnitLcd(const UnitLcd&) = delete;
UnitLcd& operator=(const UnitLcd&) = delete;
// Pass a I2C controller device
// Probe and initialise. Sets brightness to 128, rotation to 0.
[[nodiscard]] bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR);
bool isPresent() const { return dev_ != nullptr; }
// Logical dimensions in the current rotation's coordinate space.
uint16_t width() const { return (rotation_ & 1) ? PHYS_HEIGHT : PHYS_WIDTH; }
uint16_t height() const { return (rotation_ & 1) ? PHYS_WIDTH : PHYS_HEIGHT; }
uint8_t rotation() const { return rotation_; }
// -----------------------------------------------------------------------
// Control
// -----------------------------------------------------------------------
void setBrightness(uint8_t brightness);
// Rotation: 0=portrait(135×240), 1=landscape(240×135),
// 2=portrait flipped, 3=landscape flipped.
void setRotation(uint8_t rotation);
// -----------------------------------------------------------------------
// Filled primitives (fast hardware commands)
// -----------------------------------------------------------------------
void fillScreen(uint16_t rgb565);
void fillRect(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint16_t rgb565);
void drawPixel(uint8_t x, uint8_t y, uint16_t rgb565);
// -----------------------------------------------------------------------
// Raw pixel streaming
// -----------------------------------------------------------------------
bool setWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1);
void writePixels(const uint16_t* pixels, uint32_t len);
// -----------------------------------------------------------------------
// Software-rendered shapes (Bresenham / midpoint algorithms)
// -----------------------------------------------------------------------
// Horizontal / vertical lines (faster than drawLine for axis-aligned).
void drawHLine(uint8_t x, uint8_t y, uint8_t len, uint16_t rgb565);
void drawVLine(uint8_t x, uint8_t y, uint8_t len, uint16_t rgb565);
// Arbitrary line (Bresenham).
void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t rgb565);
// Rectangle outline.
void drawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t rgb565);
// Filled / outline circle (midpoint algorithm).
void fillCircle(int16_t cx, int16_t cy, int16_t r, uint16_t rgb565);
void drawCircle(int16_t cx, int16_t cy, int16_t r, uint16_t rgb565);
// Rounded rectangle (filled / outline).
void fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t rgb565);
void drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t rgb565);
// Triangle (filled / outline).
void fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1,
int16_t x2, int16_t y2, uint16_t rgb565);
void drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1,
int16_t x2, int16_t y2, uint16_t rgb565);
// Arc - angles in degrees (0=right, 90=down, 180=left, 270=up).
// r0 = outer radius, r1 = inner radius (0 for solid filled wedge).
// fillArc draws a filled arc/wedge; drawArc draws the outline only.
void fillArc(int16_t cx, int16_t cy, int16_t r0, int16_t r1,
float startDeg, float endDeg, uint16_t rgb565);
void drawArc(int16_t cx, int16_t cy, int16_t r0, int16_t r1,
float startDeg, float endDeg, uint16_t rgb565);
// -----------------------------------------------------------------------
// Text rendering - 5×7 bitmap font (ASCII 32-126)
// -----------------------------------------------------------------------
// Draw a single character at (x, y). scale 1=5×7 px, 2=10×14 px, …
void drawChar(uint8_t x, uint8_t y, char c, uint16_t fg, uint16_t bg, uint8_t scale = 1);
// Draw a null-terminated string starting at (x, y), advancing 6*scale px per char.
void drawText(uint8_t x, uint8_t y, const char* str, uint16_t fg, uint16_t bg, uint8_t scale = 1);
// -----------------------------------------------------------------------
// Misc
// -----------------------------------------------------------------------
uint8_t bufferRemaining();
static uint16_t rgb888to565(uint32_t rgb888) {
uint8_t r = (rgb888 >> 16) & 0xFF;
uint8_t g = (rgb888 >> 8) & 0xFF;
uint8_t b = rgb888 & 0xFF;
return (uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
}
// color565 alias used by M5GFX-style code
static uint16_t color565(uint8_t r, uint8_t g, uint8_t b) {
return (uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
}
private:
Device* dev_ = nullptr;
uint8_t addr_ = DEFAULT_ADDR;
uint8_t rotation_ = 0;
bool sendCmd(const uint8_t* data, uint16_t len);
// Helper: draw pixel only if in bounds (used by circle/line algorithms).
void plotPixel(int16_t x, int16_t y, uint16_t rgb565);
// Helper: fill horizontal span, clamped to screen.
void hspan(int16_t x, int16_t y, int16_t len, uint16_t rgb565);
// Octant helper for circle algorithms.
void circleOctants(int16_t cx, int16_t cy, int16_t x, int16_t y,
uint16_t rgb565, bool fill);
// Arc pixel helper - plots or fills one pixel/column depending on fill flag.
void arcImpl(int16_t cx, int16_t cy, int16_t r0, int16_t r1,
float startDeg, float endDeg, uint16_t rgb565, bool fill);
// ESP32 I2C driver buffer is 256 bytes. With 1 command byte, 255 bytes remain.
// 255 / 2 bytes per pixel = 127, rounded down to 126 for alignment.
static constexpr uint16_t CHUNK_PIXELS = 126;
};
@@ -0,0 +1,60 @@
/*
* UnitMidi.h - M5Stack MIDI Unit / Synth Unit (SAM2695, UART 31250 bps)
*
* Both the MIDI Unit and the Synth Unit use the SAM2695 audio synthesizer IC
* connected via a standard MIDI UART link. This driver covers both.
*
* SAM2695 specs:
* - 64-voice polyphony (38 with effects)
* - 16 MIDI channels
* - MIDI 1.0 protocol, 31250 bps, 8N1
*
* Usage:
* Device* uart = device_find_by_name("uart1");
* UnitMidi midi;
* if (midi.begin(uart)) {
* midi.programChange(0, 0); // channel 0, Grand Piano
* midi.noteOn(0, 60, 100); // middle C, velocity 100
* vTaskDelay(pdMS_TO_TICKS(500));
* midi.noteOff(0, 60);
* }
*/
#pragma once
#include <tactility/device.h>
#include <cstddef>
#include <cstdint>
class UnitMidi {
public:
UnitMidi() = default;
~UnitMidi() { end(); }
UnitMidi(const UnitMidi&) = delete;
UnitMidi& operator=(const UnitMidi&) = delete;
UnitMidi(UnitMidi&&) = delete;
UnitMidi& operator=(UnitMidi&&) = delete;
// Pass a UART controller device
// begin() opens the controller, sets baud rate to 31250, and issues a system reset.
[[nodiscard]] bool begin(Device* dev);
void end();
bool isPresent() const { return ready_; }
// Send raw MIDI bytes.
void send(const uint8_t* data, size_t len);
// MIDI 1.0 message helpers
void reset(); // 0xFF - system reset
void noteOn(uint8_t channel, uint8_t note, uint8_t velocity); // 0x90|ch, note, vel
void noteOff(uint8_t channel, uint8_t note); // 0x80|ch, note, 0
void programChange(uint8_t channel, uint8_t program); // 0xC0|ch, program
void controlChange(uint8_t channel, uint8_t controller, uint8_t value); // 0xB0|ch, ctrl, val
void pitchBend(uint8_t channel, int16_t value); // 0xE0|ch, LSB, MSB (-8192..+8191)
void allNotesOff(uint8_t channel); // CC 123 = 0
private:
Device* dev_ = nullptr;
bool ready_ = false;
};
@@ -0,0 +1,50 @@
/*
* UnitPaHub.h - M5Stack PaHub v2 I2C multiplexer (TCA9548A, addr 0x70)
*
* 6-channel I2C mux. Selecting a channel routes the downstream bus to that
* port only. Selecting channel 255 (or calling deselect()) disables all ports.
*
* Usage:
* UnitPaHub hub;
* if (hub.begin(dev)) {
* hub.select(0); // enable channel 0
* enc.begin(dev); // devices on channel 0 now reachable
* hub.deselect(); // disable all channels when done
* }
*
* Note: for devices that stay selected for extended periods (e.g. polling),
* leave the channel selected - only call deselect() if you need to prevent
* address conflicts between units on different channels.
*/
#pragma once
#include <UnitCommon.h>
#include <cstdint>
class UnitPaHub {
public:
UnitPaHub() = default;
UnitPaHub(const UnitPaHub&) = delete;
UnitPaHub& operator=(const UnitPaHub&) = delete;
static constexpr uint8_t DEFAULT_ADDR = 0x70;
static constexpr uint8_t NUM_CHANNELS = 6;
static constexpr uint8_t NO_CHANNEL = 0xFF;
// Pass a I2C controller device
bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR);
bool isPresent() const { return dev_ != nullptr; }
// Select a single channel (0-5). Returns false on error.
bool select(uint8_t channel);
// Disable all channels.
bool deselect();
uint8_t currentChannel() const { return channel_; }
private:
Device* dev_ = nullptr;
uint8_t addr_ = DEFAULT_ADDR;
uint8_t channel_ = NO_CHANNEL;
};
+284
View File
@@ -0,0 +1,284 @@
/*
* UnitRfid2.h - M5Stack RFID2 Unit (WS1850S, MFRC522-compatible, I2C addr 0x28)
*
* 13.56 MHz RFID reader/writer. The WS1850S chip is a drop-in replacement for
* the MFRC522 and uses the same register map.
*
* Supported standards: ISO/IEC 14443 Type A (MIFARE Classic, NTAG series)
* Read range: < 20mm
*
* Key MFRC522 registers:
* 0x01 CommandReg - issue commands
* 0x04 ComIrqReg - interrupt flags
* 0x05 DivIrqReg - CRC / other irq flags
* 0x06 ErrorReg - error flags
* 0x08 Status2Reg - MFCrypto1On bit
* 0x09 FIFODataReg - FIFO read/write
* 0x0A FIFOLevelReg - bytes in FIFO
* 0x0D BitFramingReg - bit framing for anticollision
* 0x0E CollReg - collision position
* 0x11 ModeReg - TX/RX mode, CRC preset
* 0x14 TxControlReg - antenna Tx1/Tx2 enable
* 0x15 TxASKReg - 100% ASK modulation
* 0x21 CRCResultRegH - CRC result high byte
* 0x22 CRCResultRegL - CRC result low byte
* 0x2A TModeReg - timer mode
* 0x2B TPrescalerReg - timer prescaler
* 0x2C TReloadRegH - timer reload high
* 0x2D TReloadRegL - timer reload low
*
* Usage:
* UnitRfid2 rfid;
* if (rfid.begin(dev)) {
* UnitRfid2::Uid uid;
* if (rfid.readCard(&uid)) {
* auto type = rfid.getCardType(uid);
* if (type == UnitRfid2::CardType::MifareClassic1K) {
* uint8_t block[16];
* rfid.mfReadBlock(4, uid, UnitRfid2::KEY_DEFAULT, block);
* }
* rfid.haltCard();
* }
* }
*/
#pragma once
#include <UnitCommon.h>
#include <cstdint>
class UnitRfid2 {
public:
// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
enum class CardType : uint8_t {
Unknown,
MifareClassicMini, // SAK 0x09, 5 sectors, 20 blocks
MifareClassic1K, // SAK 0x08, 16 sectors, 64 blocks
MifareClassic4K, // SAK 0x18, 40 sectors, 256 blocks
MifareUltralight, // SAK 0x00, 16 pages
NTAG213, // SAK 0x00, CC[2]=0x12, 45 pages
NTAG215, // SAK 0x00, CC[2]=0x3E, 135 pages
NTAG216, // SAK 0x00, CC[2]=0x6D, 231 pages
};
struct Uid {
uint8_t size = 0;
uint8_t bytes[10] = {};
uint8_t sak = 0;
uint8_t atqa[2] = {};
};
struct MifareKey {
uint8_t k[6] = {};
};
static const MifareKey KEY_DEFAULT; // 0xFF x6
static const MifareKey KNOWN_KEYS[15]; // common keys for auto-auth
static constexpr uint8_t KNOWN_KEY_COUNT = 15;
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
static constexpr uint8_t DEFAULT_ADDR = 0x28;
// Pass a I2C controller device
bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR);
bool isPresent() const { return dev_ != nullptr; }
// ---------------------------------------------------------------------------
// Card detection & UID
// ---------------------------------------------------------------------------
// Detect card, run full anticollision/select (4/7-byte UID), fill uid.
bool readCard(Uid* uid);
// Legacy helpers kept for compatibility.
bool isCardPresent();
uint8_t readUID(uint8_t* uid, uint8_t maxLen);
// ---------------------------------------------------------------------------
// Card type
// ---------------------------------------------------------------------------
CardType getCardType(const Uid& uid);
const char* cardTypeName(CardType t);
// User-accessible page count for UL/NTAG types (first user page = 4).
// Returns 0 for MIFARE Classic types.
uint8_t ultralightPageCount(CardType t);
// ---------------------------------------------------------------------------
// MIFARE Classic
// ---------------------------------------------------------------------------
// Read one 16-byte block; authenticates with keyA first.
bool mfReadBlock(uint8_t block, const Uid& uid,
const MifareKey& keyA, uint8_t out[16]);
// Read one block trying Key A then Key B for a given key.
bool mfReadBlockKeyAB(uint8_t block, const Uid& uid,
const MifareKey& key, uint8_t out[16]);
// Read one block trying all 15 KNOWN_KEYS (both Key A and Key B each).
// keyUsedOut: if non-null, receives the key that succeeded.
// Returns false if no key works.
bool mfReadBlockAuto(uint8_t block, const Uid& uid,
uint8_t out[16], MifareKey* keyUsedOut = nullptr);
// Write one 16-byte block; authenticates with keyA first.
bool mfWriteBlock(uint8_t block, const Uid& uid,
const MifareKey& keyA, const uint8_t data[16]);
// Returns 4 for sectors 0-31, 16 for sectors 32-39, 0 if sector >= 40.
static uint8_t mfSectorBlockCount(uint8_t sector);
// Read all blocks of a sector.
// Sectors 0-31: 4 blocks each (MIFARE Classic 1K/4K/Mini).
// Sectors 32-39: 16 blocks each (MIFARE Classic 4K only).
// out must be at least mfSectorBlockCount(sector)*16 bytes; blockCount is written on success.
bool mfReadSector(uint8_t sector, const Uid& uid,
const MifareKey& keyA, uint8_t* out, uint8_t* blockCount = nullptr);
// Write a custom UID to a magic (gen1a backdoor) MIFARE card.
// Overwrites block 0 including manufacturer data - use with care.
// newUid must be 4 bytes; bcc is computed automatically.
bool mfWriteUid(const uint8_t newUid[4], const Uid& uid);
// Zero all writable data blocks (skips block 0 and sector trailers).
// Authenticates each sector with keyA. Returns count of blocks erased.
uint8_t mfErase(const Uid& uid, const MifareKey& keyA = KEY_DEFAULT);
// ---------------------------------------------------------------------------
// MIFARE Ultralight / NTAG
// ---------------------------------------------------------------------------
// Read one 4-byte page (chip returns 4 pages; we take the first).
bool ulReadPage(uint8_t page, uint8_t out4[4]);
// Read 'count' consecutive pages starting at startPage (count*4 bytes out).
bool ulReadPages(uint8_t startPage, uint8_t count, uint8_t* out);
// Write one 4-byte page. Pages 0-3 are UID/lock/OTP - guarded against write.
// Pass force=true to override the guard (e.g. for clone operations).
bool ulWritePage(uint8_t page, const uint8_t data4[4], bool force = false);
// Zero all user pages (pages 4..4+pageCount-1). Returns count of pages erased.
uint8_t ulErase(CardType t);
// ---------------------------------------------------------------------------
// Housekeeping
// ---------------------------------------------------------------------------
void haltCard();
private:
Device* dev_ = nullptr;
uint8_t addr_ = DEFAULT_ADDR;
// ---------------------------------------------------------------------------
// Low-level register access
// ---------------------------------------------------------------------------
void writeReg(uint8_t reg, uint8_t val);
uint8_t readReg(uint8_t reg);
void setBitMask(uint8_t reg, uint8_t mask);
void clearBitMask(uint8_t reg, uint8_t mask);
void writeFifo(const uint8_t* buf, uint8_t len);
// ---------------------------------------------------------------------------
// Init helpers
// ---------------------------------------------------------------------------
bool softReset();
void antennaOn();
// ---------------------------------------------------------------------------
// CRC & transceive
// ---------------------------------------------------------------------------
bool calcCRC(const uint8_t* data, uint8_t len, uint8_t result[2]);
// Raw transceive - returns bytes received, 0 on error/timeout.
uint8_t transceive(const uint8_t* txBuf, uint8_t txLen,
uint8_t* rxBuf, uint8_t rxMaxLen,
uint8_t* rxValidBits = nullptr);
// Transceive with CRC appended to tx and CRC verified on rx.
bool transceiveCRC(const uint8_t* txBuf, uint8_t txLen,
uint8_t* rxBuf, uint8_t rxMaxLen, uint8_t* rxLen);
// ---------------------------------------------------------------------------
// ISO 14443A anticollision
// ---------------------------------------------------------------------------
// cmd: PICC_REQA (wakes IDLE only) or PICC_WUPA (wakes IDLE + HALT)
bool requestA(uint8_t atqa[2], uint8_t cmd = PICC_REQA);
bool select(Uid* uid);
// ---------------------------------------------------------------------------
// MIFARE internals
// ---------------------------------------------------------------------------
bool mfAuthenticate(uint8_t authCmd, uint8_t block,
const MifareKey& key, const Uid& uid);
bool mfReadBlock16(uint8_t block, uint8_t out[16]);
bool mfWriteBlock16(uint8_t block, const uint8_t data[16]);
bool mfMagicOpen(); // gen1a backdoor sequence
void stopCrypto1();
bool haltA();
// ---------------------------------------------------------------------------
// MFRC522 register addresses
// ---------------------------------------------------------------------------
static constexpr uint8_t REG_COMMAND = 0x01;
static constexpr uint8_t REG_COM_IRQ = 0x04;
static constexpr uint8_t REG_DIV_IRQ = 0x05;
static constexpr uint8_t REG_ERROR = 0x06;
static constexpr uint8_t REG_STATUS2 = 0x08;
static constexpr uint8_t REG_FIFO_DATA = 0x09;
static constexpr uint8_t REG_FIFO_LEVEL = 0x0A;
static constexpr uint8_t REG_CONTROL = 0x0C;
static constexpr uint8_t REG_BIT_FRAMING = 0x0D;
static constexpr uint8_t REG_COLL = 0x0E;
static constexpr uint8_t REG_MODE = 0x11;
static constexpr uint8_t REG_TX_CONTROL = 0x14;
static constexpr uint8_t REG_TX_ASK = 0x15;
static constexpr uint8_t REG_CRC_RESULT_H = 0x21;
static constexpr uint8_t REG_CRC_RESULT_L = 0x22;
static constexpr uint8_t REG_TMODE = 0x2A;
static constexpr uint8_t REG_TPRESCALER = 0x2B;
static constexpr uint8_t REG_TRELOAD_H = 0x2C;
static constexpr uint8_t REG_TRELOAD_L = 0x2D;
// ---------------------------------------------------------------------------
// MFRC522 commands
// ---------------------------------------------------------------------------
static constexpr uint8_t CMD_IDLE = 0x00;
static constexpr uint8_t CMD_CALC_CRC = 0x03;
static constexpr uint8_t CMD_TRANSCEIVE = 0x0C;
static constexpr uint8_t CMD_MF_AUTHENT = 0x0E;
static constexpr uint8_t CMD_SOFT_RESET = 0x0F;
// ---------------------------------------------------------------------------
// ISO 14443A / MIFARE PICC command bytes
// ---------------------------------------------------------------------------
static constexpr uint8_t PICC_REQA = 0x26;
static constexpr uint8_t PICC_WUPA = 0x52; // wakes IDLE + HALT cards
static constexpr uint8_t PICC_HLTA = 0x50;
static constexpr uint8_t PICC_CT = 0x88;
static constexpr uint8_t PICC_SEL_CL1 = 0x93;
static constexpr uint8_t PICC_SEL_CL2 = 0x95;
static constexpr uint8_t PICC_SEL_CL3 = 0x97;
static constexpr uint8_t PICC_ANTICOLL = 0x20;
static constexpr uint8_t PICC_MF_AUTH_KEY_A = 0x60;
static constexpr uint8_t PICC_MF_AUTH_KEY_B = 0x61;
static constexpr uint8_t PICC_MF_READ = 0x30;
static constexpr uint8_t PICC_MF_WRITE = 0xA0;
static constexpr uint8_t PICC_UL_WRITE = 0xA2;
};
@@ -0,0 +1,63 @@
/*
* UnitScroll.h - M5Stack Scroll Unit (STM32, I2C addr 0x40)
*
* Rotary encoder with push button and one RGB LED.
*
* Register map:
* 0x10 : int16_t absolute encoder value
* 0x20 : uint8_t button (0=pressed, 1=released - inverted!)
* 0x30 : 4 bytes LED: [index(0), R, G, B] - index byte always 0
* 0x40 : uint8_t write 1 to reset encoder to 0
* 0x50 : int16_t increment (delta) since last read - resets after read
* 0xFE : uint8_t firmware version
* 0xFF : uint8_t I2C address (R/W)
*
* Usage:
* UnitScroll scroll;
* if (scroll.begin(dev)) { ... }
* int16_t delta = scroll.readDelta(); // ±detents since last call
* bool btn = scroll.isPressed();
* scroll.setLed(0x00FF00);
*/
#pragma once
#include <UnitCommon.h>
#include <cstdint>
class UnitScroll {
public:
static constexpr uint8_t DEFAULT_ADDR = 0x40;
UnitScroll() = default;
UnitScroll(const UnitScroll&) = delete;
UnitScroll& operator=(const UnitScroll&) = delete;
// Pass a I2C controller device
[[nodiscard]] bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR);
bool isPresent() const { return dev_ != nullptr; }
// Read increment (delta) since last call. Resets hardware counter.
int16_t readDelta();
// Read absolute encoder value.
int16_t readAbsolute() const;
// Button state. True = pressed (register value 0 = pressed, inverted hardware).
bool isPressed() const;
// Set the RGB LED colour (0x00RRGGBB).
void setLed(uint32_t rgb);
// Reset absolute encoder to 0.
void resetEncoder();
private:
Device* dev_ = nullptr;
uint8_t addr_ = DEFAULT_ADDR;
static constexpr uint8_t REG_ENCODER = 0x10;
static constexpr uint8_t REG_BUTTON = 0x20;
static constexpr uint8_t REG_LED = 0x30;
static constexpr uint8_t REG_RESET = 0x40;
static constexpr uint8_t REG_INC_ENCODER = 0x50;
};
+267
View File
@@ -0,0 +1,267 @@
# M5UnitModules
Drivers for M5Stack Unit peripheral modules, for use with [Tactility OS](https://github.com/tactility) on ESP32 devices.
## Available drivers
| Header | Unit | Interface | Address |
|--------|------|-----------|---------|
| `Unit8Encoder.h` | 8Encoder - 8 rotary encoders + LEDs | I2C | 0x41 |
| `UnitByteButton.h` | ByteButton - 8 buttons + RGB LEDs | I2C | 0x47 |
| `UnitJoystick2.h` | Joystick2 - XY joystick + button + LED | I2C | 0x63 |
| `UnitScroll.h` | Scroll - single encoder + button + LED | I2C | 0x40 |
| `UnitPaHub.h` | PaHub - TCA9548A 6-channel I2C mux | I2C | 0x70 |
| `UnitLcd.h` | LCD Unit - 135x240 IPS display (ESP32-PICO) | I2C | 0x3E |
| `UnitDualButton.h` | Dual Button - 2 GPIO buttons | GPIO | - |
| `UnitCardKB2.h` | CardKB2 - QWERTY keyboard | I2C | 0x5F |
| `UnitMidi.h` | MIDI Unit / Synth Unit - SAM2695 synth | UART | 31250 bps |
| `UnitRfid2.h` | RFID2 - WS1850S/MFRC522-compat RFID reader/writer | I2C | 0x28 |
## Usage
### Without CMake component
Source files are compiled directly into your app (same pattern as `TactilityCpp` and `SoundEngine`). In your app's `main/CMakeLists.txt`:
```cmake
file(GLOB_RECURSE SOURCE_FILES
Source/*.c*
../../../Libraries/M5UnitModules/Source/*.c*
)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS
../../../Libraries/TactilityCpp/Include
../../../Libraries/M5UnitModules/Include
REQUIRES TactilitySDK
)
```
Then include headers as:
```cpp
#include <UnitByteButton.h>
```
### Standalone (direct I2C)
All I2C units take a `Device*` from `device_find_by_name("i2c1")`. External peripherals are on `i2c1`; `i2c0` is reserved for internal device-tree hardware.
```cpp
#include <tactility/device.h>
#include <UnitByteButton.h>
Device* i2c = device_find_by_name("i2c1");
UnitByteButton bb;
if (bb.begin(i2c)) {
uint8_t mask = bb.readButtons(); // bitmask, bit i = button i
bb.setLed(0, 0x00FF00); // set button 0 LED to green
}
```
### Through PaHub (I2C multiplexer)
When multiple units share the same I2C address, or more than 6 units are needed, connect them via a PaHub. Select the channel before each operation:
```cpp
#include <UnitPaHub.h>
#include <Unit8Encoder.h>
Device* i2c = device_find_by_name("i2c1");
UnitPaHub hub;
Unit8Encoder enc;
if (hub.begin(i2c)) {
hub.select(0); // channel 0
enc.begin(i2c); // device on channel 0 is now reachable
}
// In your poll loop:
hub.select(0);
int32_t deltas[8];
uint8_t buttons[8];
enc.readAll(deltas, buttons);
```
### GPIO unit (DualButton)
`UnitDualButton` uses the Tactility GPIO controller rather than raw ESP-IDF GPIO:
```cpp
#include <tactility/device.h>
#include <UnitDualButton.h>
Device* gpio = device_find_by_name("gpio0");
UnitDualButton db;
if (db.begin(gpio, GPIO_NUM_36, GPIO_NUM_26)) {
bool a = db.isButtonAPressed();
bool b = db.isButtonBPressed();
}
```
### UART unit (MIDI / Synth)
`UnitMidi` uses the `uart_controller` kernel driver. Pass a `Device*` from `device_find_by_name()` to `begin()`:
```cpp
#include <tactility/device.h>
#include <UnitMidi.h>
Device* uart = device_find_by_name("uart1");
UnitMidi midi;
if (midi.begin(uart)) {
midi.programChange(0, 0); // channel 0, program 0 (Grand Piano)
midi.noteOn(0, 60, 100); // middle C, velocity 100
midi.noteOff(0, 60);
}
```
### CardKB2 in UART mode
After switching the CardKB2 to UART mode (Fn+Sym+2), use `beginUart()` instead of `begin()`:
```cpp
#include <tactility/device.h>
#include <UnitCardKB2.h>
Device* uart = device_find_by_name("uart1");
UnitCardKB2 kb;
if (kb.beginUart(uart)) {
// Poll at ~50ms; returns ASCII of last pressed key, 0 if none
char c = kb.getKey();
}
```
The driver tracks Aa (caps lock / one-shot shift), Sym (symbol mode), and Fn internally. Switch back to I2C mode on the keyboard with Fn+Sym+1.
**UART mode:** Fn+1 (Esc = 0x1B) and Fn+D/X/Z/C (cursor up/down/left/right = 0x1E/0x1F/0x1D/0x1C) are fully supported - the firmware sends the raw KEY_ID in the UART frame and the driver translates it.
**I2C mode:** Fn+1 and Fn+D/X/Z/C produce no output. The firmware's I2C slave queue only holds regular ASCII; the Fn-combo branch routes directly to BLE HID and does not push to the I2C queue.
**BLE HID mode:** Standard USB HID keycodes are sent (ESC=0x29, Up=0x52, Down=0x51, Left=0x50, Right=0x4F). The Tactility `BluetoothHidHost` maps all five to the correct `LV_KEY_*` values.
## I2C protocol note
Most M5Stack Units use an STM32 microcontroller internally. These require a STOP condition between the register-pointer write and the data read (STM32 HAL populates the tx buffer after STOP, before the next START). Using a repeated-START causes bus errors.
`UnitCommon.h` provides `unitReadReg` which implements the required STOP + 2ms delay pattern automatically. All STM32-based drivers use this helper.
The LCD Unit is an exception - it contains an ESP32-PICO and uses a command-based I2C protocol without the STOP+delay requirement.
## UnitRfid2 - RFID2 Unit
The RFID2 Unit uses a WS1850S chip (drop-in MFRC522 replacement) operating at 13.56 MHz. Read range is under 20 mm.
### Supported card types
| Card | SAK | Notes |
|------|-----|-------|
| MIFARE Classic Mini | 0x09 | 5 sectors, 20 blocks |
| MIFARE Classic 1K | 0x08 | 16 sectors, 64 blocks |
| MIFARE Classic 4K | 0x18 | 40 sectors, 256 blocks |
| MIFARE Ultralight | 0x00 | 16 user pages |
| NTAG213 | 0x00 | 45 pages; CC byte 0x12 at page 3 |
| NTAG215 | 0x00 | 135 pages; CC byte 0x3E at page 3 |
| NTAG216 | 0x00 | 231 pages; CC byte 0x6D at page 3 |
### Basic usage
```cpp
#include <UnitRfid2.h>
Device* i2c = device_find_by_name("i2c1");
UnitRfid2 rfid;
if (rfid.begin(i2c)) {
UnitRfid2::Uid uid;
if (rfid.readCard(&uid)) {
auto type = rfid.getCardType(uid);
// ...
rfid.haltCard();
}
}
```
`readCard()` runs the full ISO 14443-3 anticollision/SELECT sequence (including 7-byte and 10-byte cascaded UIDs) and uses WUPA so cards in HALT state are re-detected without needing to be removed.
### MIFARE Classic
```cpp
uint8_t block[16];
// Read with default key (0xFF×6)
rfid.mfReadBlock(4, uid, UnitRfid2::KEY_DEFAULT, block);
// Read trying both Key A and Key B
rfid.mfReadBlockKeyAB(4, uid, UnitRfid2::KEY_DEFAULT, block);
// Read trying all 15 built-in common keys automatically
UnitRfid2::MifareKey keyUsed;
rfid.mfReadBlockAuto(4, uid, block, &keyUsed);
// Write a block
rfid.mfWriteBlock(4, uid, UnitRfid2::KEY_DEFAULT, block);
// Read a full sector (416 blocks × 16 bytes; sectors 0-31 = 4 blocks, 32-39 = 16 blocks)
uint8_t sectorBuf[16 * 16]; // max 16 blocks for 4K large sectors
uint8_t blockCount = 0;
rfid.mfReadSector(1, uid, UnitRfid2::KEY_DEFAULT, sectorBuf, &blockCount);
```
`KNOWN_KEYS[15]` contains 15 widely-used default keys. `mfReadBlockAuto` tries each as Key A then Key B, which recovers data from most factory-default or commercially-programmed cards.
Sector trailers (block index % 4 == 3 for sectors 0-31; block index % 16 == 15 for sectors 32-39) contain key and access-condition data; they require the correct key to read and should not be blindly overwritten.
### MIFARE Ultralight / NTAG
```cpp
uint8_t page[4];
rfid.ulReadPage(4, page); // pages 0-3 are UID/config - skip for user data
uint8_t pages[16];
rfid.ulReadPages(4, 4, pages); // 4 consecutive pages starting at 4
rfid.ulWritePage(4, page); // pages 0-3 blocked (pass force=true to override)
```
Use `ultralightPageCount(type)` to get the number of user pages for a given NTAG variant. The first user page is always 4.
### NDEF write (NTAG)
Write a URI NDEF record starting at page 4. The driver in `TestUnitRfid2` handles the full TLV framing, URI abbreviation prefix table (http://, https://, mailto:, tel:, etc.), and page-aligned writes via `ulWritePage`.
### Magic card UID clone (gen1a)
```cpp
// Clone the UID from `sourceUid` onto a gen1a magic MIFARE Classic card
rfid.mfWriteUid(sourceUid.bytes, targetUid);
```
`mfWriteUid` uses the gen1a backdoor sequence (0x40 7-bit frame + 0x43) to open direct write access to block 0, then builds the block with the new UID, computed BCC, and the original SAK/ATQA bytes. Only works on magic gen1a cards - regular MIFARE cards protect block 0 at the chip level.
NTAG21x UID bytes are read-only at the hardware level and cannot be cloned.
## M5UnitTest app
`Apps/M5UnitTest/` is a hardware test app that provides a live interactive test view for each unit. It auto-detects whether a unit is connected directly or via a PaHub, and scales its UI for both portrait and landscape orientations across all supported screen sizes.
| Test view | Unit tested | Notes |
|-----------|-------------|-------|
| `TestUnit8Encoder` | 8Encoder | Live delta/button readout; LED colour cycling on encoder press |
| `TestUnitByteButton` | ByteButton | 8 button indicators; tapping a button lights its LED |
| `TestUnitCardKB2` | CardKB2 | Keystroke echo in both I2C and UART modes |
| `TestUnitDualButton` | Dual Button | Configurable GPIO pin picker; red/blue circle indicators |
| `TestUnitJoystick2` | Joystick2 | XY position bar graphs + button state + LED colour picker |
| `TestUnitLcd` | LCD Unit | Displays a colour gradient and version info on the unit's screen |
| `TestUnitMidi` | MIDI / Synth | Channel + program picker; Note On/Off for middle C |
| `TestUnitPaHub` | PaHub | Per-channel I2C address scanner with periodic re-probe |
| `TestUnitRfid2` | RFID2 | Card detection with UID, type, SAK/ATQA display; Clear to re-scan |
| `TestUnitScroll` | Scroll | Encoder delta counter + button state + LED colour |
The RFID2 test view (`TestUnitRfid2`) shows a pulsing green circle while waiting, then displays card info on tap. Tapping **Clear** returns to idle.
@@ -0,0 +1,137 @@
#include <Unit8Encoder.h>
#include <esp_log.h>
static constexpr auto* TAG = "Unit8Encoder";
static inline void packRgb(uint8_t* dst, uint32_t rgb) {
dst[0] = (uint8_t)((rgb >> 16) & 0xFF);
dst[1] = (uint8_t)((rgb >> 8) & 0xFF);
dst[2] = (uint8_t)( rgb & 0xFF);
}
bool Unit8Encoder::begin(Device* dev, uint8_t addr) {
if (!dev || !device_is_ready(dev)) return false;
if (!unitProbe(dev, addr)) {
ESP_LOGW(TAG, "8Encoder not found at 0x%02X", addr);
return false;
}
dev_ = dev;
addr_ = addr;
// Turn encoder LEDs off
uint8_t off[ENCODER_LED_COUNT * 3] = {};
if (!unitWriteReg(dev_, addr_, REG_LED, off, sizeof(off)))
ESP_LOGW(TAG, "8Encoder LED init write failed at 0x%02X", addr_);
// Turn switch LED off
uint8_t offSw[3] = {};
if (!unitWriteReg(dev_, addr_, REG_SWITCH_LED, offSw, 3))
ESP_LOGW(TAG, "8Encoder switch LED init write failed at 0x%02X", addr_);
// Poison cache so first flushLeds() always sends
memset(ledColor_, 0xFF, sizeof(ledColor_));
ESP_LOGI(TAG, "8Encoder ready at 0x%02X", addr_);
return true;
}
bool Unit8Encoder::readAll(int32_t deltas[8], uint8_t buttons[8]) {
if (!dev_) return false;
for (int i = 0; i < 8; i++) {
uint8_t buf[4] = {};
if (!unitReadReg(dev_, addr_, (uint8_t)(REG_INCREMENT + i * 4), buf, 4)) {
ESP_LOGW(TAG, "delta read failed ch%d", i);
return false;
}
int32_t val;
memcpy(&val, buf, 4);
deltas[i] = val / 4; // 4 pulses per detent
}
for (int i = 0; i < 8; i++) {
uint8_t val = 0;
if (!unitReadReg(dev_, addr_, (uint8_t)(REG_BUTTON + i), &val, 1)) {
ESP_LOGW(TAG, "button read failed ch%d", i);
return false;
}
buttons[i] = val;
}
return true;
}
bool Unit8Encoder::readSwitch(bool& state) {
if (!dev_) return false;
uint8_t val = 0;
if (!unitReadReg(dev_, addr_, REG_SWITCH, &val, 1)) {
ESP_LOGW(TAG, "switch read failed");
return false;
}
state = (val != 0);
return true;
}
void Unit8Encoder::setLed(uint8_t idx, uint32_t rgb) {
if (!dev_ || idx >= LED_COUNT) return;
if (ledColor_[idx] == rgb) return;
uint8_t buf[3];
packRgb(buf, rgb);
uint8_t reg = (idx < ENCODER_LED_COUNT) ? (uint8_t)(REG_LED + idx * 3) : REG_SWITCH_LED;
if (unitWriteReg(dev_, addr_, reg, buf, 3))
ledColor_[idx] = rgb;
}
void Unit8Encoder::setSwitchLed(uint32_t rgb) {
setLed(ENCODER_LED_COUNT, rgb);
}
void Unit8Encoder::flushLeds(const uint32_t pending[LED_COUNT]) {
if (!dev_) return;
// Encoder LEDs 0-7: batch write if any changed
bool encDirty = false;
for (int i = 0; i < ENCODER_LED_COUNT; i++)
if (pending[i] != ledColor_[i]) { encDirty = true; break; }
if (encDirty) {
uint8_t buf[ENCODER_LED_COUNT * 3];
for (int i = 0; i < ENCODER_LED_COUNT; i++) packRgb(buf + i * 3, pending[i]);
if (unitWriteReg(dev_, addr_, REG_LED, buf, sizeof(buf))) {
for (int i = 0; i < ENCODER_LED_COUNT; i++) ledColor_[i] = pending[i];
} else {
ESP_LOGW(TAG, "flushLeds encoder write failed");
}
}
// Switch LED (index 8): write if changed
if (pending[ENCODER_LED_COUNT] != ledColor_[ENCODER_LED_COUNT]) {
uint8_t buf[3];
packRgb(buf, pending[ENCODER_LED_COUNT]);
if (unitWriteReg(dev_, addr_, REG_SWITCH_LED, buf, 3))
ledColor_[ENCODER_LED_COUNT] = pending[ENCODER_LED_COUNT];
else
ESP_LOGW(TAG, "flushLeds switch LED write failed");
}
}
void Unit8Encoder::setAllLeds(uint32_t rgb) {
if (!dev_) return;
// Encoder LEDs 0-7
bool encDirty = false;
for (int i = 0; i < ENCODER_LED_COUNT; i++)
if (ledColor_[i] != rgb) { encDirty = true; break; }
if (encDirty) {
uint8_t buf[ENCODER_LED_COUNT * 3];
for (int i = 0; i < ENCODER_LED_COUNT; i++) packRgb(buf + i * 3, rgb);
if (unitWriteReg(dev_, addr_, REG_LED, buf, sizeof(buf))) {
for (int i = 0; i < ENCODER_LED_COUNT; i++) ledColor_[i] = rgb;
} else {
ESP_LOGW(TAG, "setAllLeds encoder write failed");
}
}
// Switch LED (index 8)
if (ledColor_[ENCODER_LED_COUNT] != rgb) {
uint8_t buf[3];
packRgb(buf, rgb);
if (unitWriteReg(dev_, addr_, REG_SWITCH_LED, buf, 3))
ledColor_[ENCODER_LED_COUNT] = rgb;
else
ESP_LOGW(TAG, "setAllLeds switch LED write failed");
}
}
@@ -0,0 +1,104 @@
#include <UnitByteButton.h>
#include <esp_log.h>
static constexpr auto* TAG = "UnitByteButton";
bool UnitByteButton::begin(Device* dev, uint8_t addr) {
if (!dev || !device_is_ready(dev)) return false;
if (!unitProbe(dev, addr)) {
ESP_LOGW(TAG, "ByteButton not found at 0x%02X", addr);
return false;
}
dev_ = dev;
addr_ = addr;
// Set LED show mode to user-defined (0x00) so our colour writes take effect
uint8_t mode = 0x00;
if (!unitWriteReg(dev_, addr_, REG_SHOW_MODE, &mode, 1)) {
ESP_LOGW(TAG, "ByteButton show mode write failed at 0x%02X", addr_);
dev_ = nullptr;
return false;
}
// Turn all LEDs off and poison cache
uint8_t off[32] = {};
if (!unitWriteReg(dev_, addr_, REG_RGB888, off, 32)) {
ESP_LOGW(TAG, "ByteButton LED init write failed at 0x%02X", addr_);
dev_ = nullptr;
return false;
}
memset(ledColor_, 0xFF, sizeof(ledColor_));
ESP_LOGI(TAG, "ByteButton ready at 0x%02X", addr_);
return true;
}
uint8_t UnitByteButton::readButtons(bool* ok) {
if (!dev_) { if (ok) *ok = false; return 0; }
uint8_t val = 0;
bool success = unitReadReg(dev_, addr_, REG_STATUS, &val, 1);
if (ok) *ok = success;
return val;
}
bool UnitByteButton::readButton(uint8_t idx) {
if (!dev_ || idx >= 8) return false;
uint8_t val = 0;
unitReadReg(dev_, addr_, (uint8_t)(REG_STATUS_8 + idx), &val, 1);
return val != 0;
}
void UnitByteButton::setLed(uint8_t idx, uint32_t rgb) {
if (!dev_ || idx >= 8) return;
if (ledColor_[idx] == rgb) return;
uint8_t buf[4] = {
(uint8_t)( rgb & 0xFF),
(uint8_t)((rgb >> 8) & 0xFF),
(uint8_t)((rgb >> 16) & 0xFF),
0x00,
};
if (unitWriteReg(dev_, addr_, (uint8_t)(REG_RGB888 + idx * 4), buf, 4)) {
ledColor_[idx] = rgb;
}
}
void UnitByteButton::flushLeds(const uint32_t pending[8]) {
if (!dev_) return;
bool dirty = false;
for (int i = 0; i < 8; i++)
if (pending[i] != ledColor_[i]) { dirty = true; break; }
if (!dirty) return;
// Build 32-byte burst: 8 × 4-byte LE colour
uint8_t buf[32];
for (int i = 0; i < 8; i++) {
buf[i*4+0] = (uint8_t)( pending[i] & 0xFF); // B
buf[i*4+1] = (uint8_t)((pending[i] >> 8) & 0xFF); // G
buf[i*4+2] = (uint8_t)((pending[i] >> 16) & 0xFF); // R
buf[i*4+3] = 0x00;
}
if (unitWriteReg(dev_, addr_, REG_RGB888, buf, 32)) {
for (int i = 0; i < 8; i++)
ledColor_[i] = pending[i];
} else {
ESP_LOGW(TAG, "flushLeds write failed - cache not updated");
}
}
void UnitByteButton::setAllLeds(uint32_t rgb) {
if (!dev_) return;
bool dirty = false;
for (int i = 0; i < 8; i++)
if (ledColor_[i] != rgb) { dirty = true; break; }
if (!dirty) return;
uint8_t buf[32];
for (int i = 0; i < 8; i++) {
buf[i*4+0] = (uint8_t)( rgb & 0xFF);
buf[i*4+1] = (uint8_t)((rgb >> 8) & 0xFF);
buf[i*4+2] = (uint8_t)((rgb >> 16) & 0xFF);
buf[i*4+3] = 0x00;
}
if (unitWriteReg(dev_, addr_, REG_RGB888, buf, 32)) {
for (int i = 0; i < 8; i++) ledColor_[i] = rgb;
} else {
ESP_LOGW(TAG, "setAllLeds write failed - cache not updated");
}
}
@@ -0,0 +1,260 @@
#include <UnitCardKB2.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
static constexpr auto* TAG = "UnitCardKB2";
static constexpr uint32_t UART_BAUD = 115200;
// ---------------------------------------------------------------------------
// Key ID positions for modifier keys (row*11 + col)
// ---------------------------------------------------------------------------
static constexpr uint8_t ID_AA = 2*11 + 0; // 22 - caps lock
static constexpr uint8_t ID_FN = 3*11 + 0; // 33 - function key
static constexpr uint8_t ID_SYM = 3*11 + 1; // 34 - symbol key
// ---------------------------------------------------------------------------
// Three translation tables - normal, caps, sym (44 entries each, 0=no output)
// Source: manual Table 3/4/5
// ---------------------------------------------------------------------------
// Normal/lowercase
static constexpr char KEY_NORMAL[44] = {
// Row 0: 1 2 3 4 5 6 7 8 9 0 [col10 unused]
'1','2','3','4','5','6','7','8','9','0', 0,
// Row 1: q w e r t y u i o p Del
'q','w','e','r','t','y','u','i','o','p', 0x08,
// Row 2: Aa(mod) a s d f g h j k l Enter
0, 'a','s','d','f','g','h','j','k','l', 0x0A,
// Row 3: Fn(mod) Sym(mod) z x c v b n m Space [col10 unused]
0, 0, 'z','x','c','v','b','n','m', 0x20, 0,
};
// Caps lock - letters uppercase, digits/space/del/enter unchanged
static constexpr char KEY_CAPS[44] = {
'1','2','3','4','5','6','7','8','9','0', 0,
'Q','W','E','R','T','Y','U','I','O','P', 0x08,
0, 'A','S','D','F','G','H','J','K','L', 0x0A,
0, 0, 'Z','X','C','V','B','N','M', 0x20, 0,
};
// Symbol mode - from manual Table 5
// Row 0: ! @ # $ % ^ & * ( ) [col10 unused]
// Row 1: ~ ` ? \ / | _ - + = Del
// Row 2: Aa(mod) { } ^ [ ] " ' ; : Enter
// Row 3: Fn(mod) Sym(mod) Z X C < > , . Space [col10 unused]
static constexpr char KEY_SYM[44] = {
'!','@','#','$','%','^','&','*','(',')', 0,
'~','`','?','\\','/','|','_','-','+','=', 0x08,
0, '{','}','^','[',']','"','\'',';',':', 0x0A,
0, 0, 'Z','X','C','<','>',',','.', 0x20, 0,
};
// Fn combos: key ID → ASCII (only for IDs that produce something with Fn)
// Fn+D(col3,row2=ID25)=up, Fn+X(col3,row3=ID36)=down,
// Fn+Z(col2,row3=ID35)=left, Fn+C(col4,row3=ID37)=right
// Fn+1(col0,row0=ID0)=Esc
static char fnCombo(uint8_t id) {
switch (id) {
case 0: return 0x1B; // Fn+1 = Esc
case 25: return 0x1E; // Fn+D = up
case 36: return 0x1F; // Fn+X = down
case 35: return 0x1D; // Fn+Z = left
case 37: return 0x1C; // Fn+C = right
default: return 0;
}
}
// ---------------------------------------------------------------------------
// I2C
// ---------------------------------------------------------------------------
bool UnitCardKB2::begin(Device* dev, uint8_t addr) {
end();
if (!dev || !device_is_ready(dev)) return false;
if (!unitProbe(dev, addr)) {
ESP_LOGW(TAG, "CardKB2 not found at 0x%02X", addr);
return false;
}
i2cDev_ = dev;
addr_ = addr;
mode_ = Mode::I2C;
ESP_LOGI(TAG, "CardKB2 ready (I2C) at 0x%02X", addr_);
return true;
}
char UnitCardKB2::readFromI2C() {
if (!i2cDev_) return 0;
uint8_t val = 0;
if (i2c_controller_read(i2cDev_, addr_, &val, 1, pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) != ERROR_NONE)
ESP_LOGD(TAG, "CardKB2 I2C read failed at 0x%02X", addr_);
return (char)val;
}
// ---------------------------------------------------------------------------
// UART
// ---------------------------------------------------------------------------
bool UnitCardKB2::beginUart(Device* dev) {
end();
if (!dev) return false;
UartConfig cfg = {
UART_BAUD,
UART_CONTROLLER_DATA_8_BITS,
UART_CONTROLLER_PARITY_DISABLE,
UART_CONTROLLER_STOP_BITS_1,
};
if (uart_controller_set_config(dev, &cfg) != ERROR_NONE) {
ESP_LOGW(TAG, "CardKB2 UART set_config failed");
return false;
}
if (uart_controller_open(dev) != ERROR_NONE) {
ESP_LOGW(TAG, "CardKB2 UART open failed");
return false;
}
uartDev_ = dev;
frameState_ = FrameState::WaitAA;
capsLock_ = false;
symMode_ = false;
fnHeld_ = false;
oneShiftPending_ = false;
mode_ = Mode::Uart;
ESP_LOGI(TAG, "CardKB2 ready (UART) at %lu bps", (unsigned long)UART_BAUD);
return true;
}
char UnitCardKB2::pollUart() {
if (!uartDev_) return 0;
char result = 0;
uint8_t b;
// Drain all available bytes this tick; non-blocking (timeout=0)
while (uart_controller_read_byte(uartDev_, &b, 0) == ERROR_NONE) {
switch (frameState_) {
case FrameState::WaitAA:
if (b == 0xAA) frameState_ = FrameState::WaitLen;
break;
case FrameState::WaitLen:
frameState_ = (b == 0x03) ? FrameState::WaitId : FrameState::WaitAA;
break;
case FrameState::WaitId:
frameId_ = b;
frameState_ = FrameState::WaitState;
break;
case FrameState::WaitState:
frameKs_ = b;
frameState_ = FrameState::WaitCsum;
break;
case FrameState::WaitCsum: {
frameState_ = FrameState::WaitAA;
uint8_t expected = (0x03 + frameId_ + frameKs_) & 0xFF;
if (b != expected) {
ESP_LOGD(TAG, "UART frame csum err: got 0x%02X exp 0x%02X", b, expected);
break;
}
bool pressed = (frameKs_ == 0x01);
bool released = (frameKs_ == 0x02);
// --- Modifier tracking ---
if (frameId_ == ID_FN) {
fnHeld_ = pressed;
break;
}
if (frameId_ == ID_SYM && pressed) {
symMode_ = !symMode_;
if (symMode_) capsLock_ = false; // Aa ineffective in sym mode
break;
}
if (frameId_ == ID_AA && pressed && !symMode_) {
static constexpr uint32_t AA_DOUBLE_CLICK_MS = 400;
uint32_t now = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
if (!capsLock_) {
if (oneShiftPending_ && (now - lastAATimestamp_) <= AA_DOUBLE_CLICK_MS) {
// Quick double-tap - engage caps lock, clear one-shot
capsLock_ = true;
oneShiftPending_ = false;
lastAATimestamp_ = 0;
} else {
// First tap or too slow - start/restart one-shot
oneShiftPending_ = true;
lastAATimestamp_ = now;
}
} else {
// Already locked - release caps lock
capsLock_ = false;
oneShiftPending_ = false;
lastAATimestamp_ = 0;
}
break;
}
// --- Key press → ASCII ---
if (!pressed) break;
char ascii = 0;
if (fnHeld_) {
ascii = fnCombo(frameId_);
} else if (symMode_) {
if (frameId_ < 44) ascii = KEY_SYM[frameId_];
} else if (capsLock_ || oneShiftPending_) {
if (frameId_ < 44) ascii = KEY_CAPS[frameId_];
if (oneShiftPending_) {
// Consume the one-shot only when a letter was actually shifted
if (ascii >= 'A' && ascii <= 'Z') oneShiftPending_ = false;
}
} else {
if (frameId_ < 44) ascii = KEY_NORMAL[frameId_];
}
// First key press wins; stop draining once we have a result.
if (ascii != 0) { result = ascii; return result; }
break;
}
}
}
return result;
}
// ---------------------------------------------------------------------------
// end
// ---------------------------------------------------------------------------
void UnitCardKB2::end() {
if (mode_ == Mode::Uart && uartDev_) {
uart_controller_close(uartDev_);
uartDev_ = nullptr;
}
i2cDev_ = nullptr;
cachedKey_ = 0;
frameState_ = FrameState::WaitAA;
capsLock_ = false;
symMode_ = false;
fnHeld_ = false;
oneShiftPending_ = false;
lastAATimestamp_ = 0;
mode_ = Mode::I2C;
}
// ---------------------------------------------------------------------------
// Public getKey / hasKey
// ---------------------------------------------------------------------------
char UnitCardKB2::getKey() {
if (cachedKey_ != 0) {
char k = cachedKey_;
cachedKey_ = 0;
return k;
}
if (mode_ == Mode::Uart) return pollUart();
return readFromI2C();
}
bool UnitCardKB2::hasKey() {
if (mode_ == Mode::Uart) {
if (cachedKey_ == 0) cachedKey_ = pollUart();
return cachedKey_ != 0;
}
if (cachedKey_ != 0) return true;
cachedKey_ = readFromI2C();
return cachedKey_ != 0;
}
@@ -0,0 +1,70 @@
#include <UnitDualButton.h>
#include <esp_log.h>
static constexpr auto* TAG = "UnitDualButton";
UnitDualButton::~UnitDualButton() {
end();
}
bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB) {
if (!controller) return false;
descA_ = gpio_descriptor_acquire(controller, pinA, GPIO_OWNER_GPIO);
if (!descA_) {
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinA);
return false;
}
descB_ = gpio_descriptor_acquire(controller, pinB, GPIO_OWNER_GPIO);
if (!descB_) {
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinB);
gpio_descriptor_release(descA_);
descA_ = nullptr;
return false;
}
gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP;
if (gpio_descriptor_set_flags(descA_, flags) != ERROR_NONE) {
ESP_LOGW(TAG, "Failed to configure pin %d flags", (int)pinA);
gpio_descriptor_release(descA_); gpio_descriptor_release(descB_);
descA_ = descB_ = nullptr;
return false;
}
if (gpio_descriptor_set_flags(descB_, flags) != ERROR_NONE) {
ESP_LOGW(TAG, "Failed to configure pin %d flags", (int)pinB);
gpio_descriptor_release(descA_); gpio_descriptor_release(descB_);
descA_ = descB_ = nullptr;
return false;
}
ready_ = true;
ESP_LOGI(TAG, "DualButton ready on pins %d/%d", (int)pinA, (int)pinB);
return true;
}
void UnitDualButton::end() {
if (descA_) { gpio_descriptor_release(descA_); descA_ = nullptr; }
if (descB_) { gpio_descriptor_release(descB_); descB_ = nullptr; }
ready_ = false;
}
bool UnitDualButton::readPin(GpioDescriptor* desc) {
bool high = true;
if (gpio_descriptor_get_level(desc, &high) != ERROR_NONE) {
// Read failed - treat as not pressed (safe fallback)
ESP_LOGW(TAG, "gpio_descriptor_get_level failed");
return false;
}
return !high; // active-low: low = pressed
}
bool UnitDualButton::isButtonAPressed() const {
if (!descA_) return false;
return readPin(descA_);
}
bool UnitDualButton::isButtonBPressed() const {
if (!descB_) return false;
return readPin(descB_);
}
@@ -0,0 +1,65 @@
#include <UnitJoystick2.h>
#include <esp_log.h>
#include <cstring>
static constexpr auto* TAG = "UnitJoystick2";
bool UnitJoystick2::begin(Device* dev, uint8_t addr) {
if (!dev || !device_is_ready(dev)) return false;
if (!unitProbe(dev, addr)) {
ESP_LOGW(TAG, "Joystick2 not found at 0x%02X", addr);
return false;
}
dev_ = dev;
addr_ = addr;
ESP_LOGI(TAG, "Joystick2 ready at 0x%02X", addr_);
return true;
}
bool UnitJoystick2::readXY12(int16_t* x, int16_t* y) {
if (!dev_ || !x || !y) return false;
uint8_t buf[4] = {};
if (!unitReadReg(dev_, addr_, REG_OFFSET_12BIT, buf, 4)) return false;
memcpy(x, &buf[0], 2);
memcpy(y, &buf[2], 2);
return true;
}
bool UnitJoystick2::readXY12Raw(uint16_t* x, uint16_t* y) {
if (!dev_ || !x || !y) return false;
uint8_t buf[4] = {};
if (!unitReadReg(dev_, addr_, REG_ADC_12BIT, buf, 4)) return false;
memcpy(x, &buf[0], 2);
memcpy(y, &buf[2], 2);
return true;
}
bool UnitJoystick2::readXY8(int8_t* x, int8_t* y) {
if (!dev_ || !x || !y) return false;
uint8_t buf[2] = {};
if (!unitReadReg(dev_, addr_, REG_OFFSET_8BIT, buf, 2)) return false;
*x = (int8_t)buf[0];
*y = (int8_t)buf[1];
return true;
}
bool UnitJoystick2::isPressed() const {
if (!dev_) return false;
uint8_t val = 1;
if (!unitReadReg(dev_, addr_, REG_BUTTON, &val, 1)) {
ESP_LOGW(TAG, "button read failed at 0x%02X", addr_);
}
return val == 0; // hardware: 0=pressed, 1=released
}
bool UnitJoystick2::setLed(uint32_t rgb) {
if (!dev_) return false;
// LE uint32_t: 0x00RRGGBB → wire bytes [BB, GG, RR, 00]
uint8_t buf[4] = {
(uint8_t)( rgb & 0xFF),
(uint8_t)((rgb >> 8) & 0xFF),
(uint8_t)((rgb >> 16) & 0xFF),
0x00
};
return unitWriteReg(dev_, addr_, REG_RGB, buf, 4);
}
+603
View File
@@ -0,0 +1,603 @@
#include <UnitLcd.h>
#include <esp_log.h>
#include <cstring>
#include <cmath>
#include <algorithm>
static constexpr auto* TAG = "UnitLcd";
static constexpr uint8_t CMD_SET_BRIGHTNESS = 0x22;
static constexpr uint8_t CMD_SET_ROTATION = 0x36;
static constexpr uint8_t CMD_FILL_RECT = 0x6A;
static constexpr uint8_t CMD_DRAW_PIXEL = 0x62;
static constexpr uint8_t CMD_SET_COL_RANGE = 0x2A;
static constexpr uint8_t CMD_SET_ROW_RANGE = 0x2B;
static constexpr uint8_t CMD_WRITE_RAW = 0x42;
static constexpr uint8_t CMD_READ_BUFCOUNT = 0x09;
// ---------------------------------------------------------------------------
// Transport
// ---------------------------------------------------------------------------
bool UnitLcd::sendCmd(const uint8_t* data, uint16_t len) {
return i2c_controller_write(dev_, addr_, data, len,
pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE;
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
bool UnitLcd::begin(Device* dev, uint8_t addr) {
if (!dev || !device_is_ready(dev)) return false;
if (!unitProbe(dev, addr)) {
ESP_LOGW(TAG, "LCD unit not found at 0x%02X", addr);
return false;
}
dev_ = dev;
addr_ = addr;
rotation_ = 0;
uint8_t brCmd[2] = { CMD_SET_BRIGHTNESS, 128 };
if (!sendCmd(brCmd, 2)) {
ESP_LOGE(TAG, "LCD setBrightness failed at 0x%02X", addr_);
dev_ = nullptr;
return false;
}
uint8_t rotCmd[2] = { CMD_SET_ROTATION, 0x00 };
if (!sendCmd(rotCmd, 2)) {
ESP_LOGE(TAG, "LCD setRotation failed at 0x%02X", addr_);
dev_ = nullptr;
return false;
}
ESP_LOGI(TAG, "LCD unit ready at 0x%02X", addr_);
return true;
}
// ---------------------------------------------------------------------------
// Control
// ---------------------------------------------------------------------------
void UnitLcd::setBrightness(uint8_t brightness) {
if (!dev_) return;
uint8_t cmd[2] = { CMD_SET_BRIGHTNESS, brightness };
if (!sendCmd(cmd, 2))
ESP_LOGW(TAG, "setBrightness cmd failed");
}
void UnitLcd::setRotation(uint8_t rot) {
if (!dev_) return;
rotation_ = rot & 0x03;
uint8_t cmd[2] = { CMD_SET_ROTATION, (uint8_t)(rotation_ & 0x07) };
if (!sendCmd(cmd, 2))
ESP_LOGW(TAG, "setRotation cmd failed");
}
// ---------------------------------------------------------------------------
// Filled primitives
// ---------------------------------------------------------------------------
void UnitLcd::fillRect(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint16_t rgb565) {
if (!dev_) return;
uint8_t cmd[7] = {
CMD_FILL_RECT,
x0, y0, x1, y1,
(uint8_t)(rgb565 >> 8),
(uint8_t)(rgb565 & 0xFF)
};
sendCmd(cmd, 7);
}
void UnitLcd::fillScreen(uint16_t rgb565) {
if (!dev_) return;
fillRect(0, 0, (uint8_t)(width() - 1), (uint8_t)(height() - 1), rgb565);
}
void UnitLcd::drawPixel(uint8_t x, uint8_t y, uint16_t rgb565) {
if (!dev_) return;
uint8_t cmd[5] = {
CMD_DRAW_PIXEL,
x, y,
(uint8_t)(rgb565 >> 8),
(uint8_t)(rgb565 & 0xFF)
};
sendCmd(cmd, 5);
}
// ---------------------------------------------------------------------------
// Raw pixel streaming
// ---------------------------------------------------------------------------
bool UnitLcd::setWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) {
if (!dev_) return false;
uint8_t caset[3] = { CMD_SET_COL_RANGE, x0, x1 };
uint8_t raset[3] = { CMD_SET_ROW_RANGE, y0, y1 };
return sendCmd(caset, 3) && sendCmd(raset, 3);
}
void UnitLcd::writePixels(const uint16_t* pixels, uint32_t len) {
if (!dev_ || !pixels || len == 0) return;
uint32_t offset = 0;
while (offset < len) {
uint32_t chunk = std::min((uint32_t)CHUNK_PIXELS, len - offset);
uint8_t pkt[1 + CHUNK_PIXELS * 2];
pkt[0] = CMD_WRITE_RAW;
for (uint32_t i = 0; i < chunk; i++) {
uint16_t px = pixels[offset + i];
pkt[1 + i*2 + 0] = (uint8_t)(px >> 8);
pkt[1 + i*2 + 1] = (uint8_t)(px & 0xFF);
}
sendCmd(pkt, (uint16_t)(1 + chunk * 2));
offset += chunk;
}
}
// ---------------------------------------------------------------------------
// Status
// ---------------------------------------------------------------------------
uint8_t UnitLcd::bufferRemaining() {
if (!dev_) return UINT8_MAX;
uint8_t cmd = CMD_READ_BUFCOUNT;
if (i2c_controller_write(dev_, addr_, &cmd, 1,
pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) != ERROR_NONE)
return UINT8_MAX;
uint8_t val = 0;
if (i2c_controller_read(dev_, addr_, &val, 1,
pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) != ERROR_NONE)
return UINT8_MAX;
return val;
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
void UnitLcd::plotPixel(int16_t x, int16_t y, uint16_t rgb565) {
if (x < 0 || y < 0 || x >= (int16_t)width() || y >= (int16_t)height()) return;
drawPixel((uint8_t)x, (uint8_t)y, rgb565);
}
void UnitLcd::hspan(int16_t x, int16_t y, int16_t len, uint16_t rgb565) {
if (y < 0 || y >= (int16_t)height() || len <= 0) return;
int16_t x1 = x + len - 1;
if (x < 0) x = 0;
if (x1 >= (int16_t)width()) x1 = (int16_t)width() - 1;
if (x > x1) return;
fillRect((uint8_t)x, (uint8_t)y, (uint8_t)x1, (uint8_t)y, rgb565);
}
// ---------------------------------------------------------------------------
// Lines
// ---------------------------------------------------------------------------
void UnitLcd::drawHLine(uint8_t x, uint8_t y, uint8_t len, uint16_t rgb565) {
if (!dev_ || len == 0) return;
if (x >= width()) return;
uint16_t x1 = (uint16_t)x + len - 1;
if (x1 >= width()) x1 = width() - 1;
fillRect(x, y, (uint8_t)x1, y, rgb565);
}
void UnitLcd::drawVLine(uint8_t x, uint8_t y, uint8_t len, uint16_t rgb565) {
if (!dev_ || len == 0) return;
if (y >= height()) return;
uint16_t y1 = (uint16_t)y + len - 1;
if (y1 >= height()) y1 = height() - 1;
fillRect(x, y, x, (uint8_t)y1, rgb565);
}
void UnitLcd::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t rgb565) {
if (!dev_) return;
// Fast paths
if (y0 == y1) { hspan(std::min(x0, x1), y0, (int16_t)std::abs(x1 - x0) + 1, rgb565); return; }
if (x0 == x1) {
int16_t ylo = std::min(y0, y1), yhi = std::max(y0, y1);
for (int16_t y = ylo; y <= yhi; y++) plotPixel(x0, y, rgb565);
return;
}
// Bresenham
int16_t dx = std::abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int16_t dy = -std::abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int16_t err = dx + dy;
while (true) {
plotPixel(x0, y0, rgb565);
if (x0 == x1 && y0 == y1) break;
int16_t e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
// ---------------------------------------------------------------------------
// Rectangle outline
// ---------------------------------------------------------------------------
void UnitLcd::drawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t rgb565) {
if (!dev_ || w == 0 || h == 0) return;
drawHLine(x, y, w, rgb565);
drawHLine(x, y + h - 1, w, rgb565);
drawVLine(x, y, h, rgb565);
drawVLine(x + w - 1, y, h, rgb565);
}
// ---------------------------------------------------------------------------
// Circle helpers
// ---------------------------------------------------------------------------
void UnitLcd::circleOctants(int16_t cx, int16_t cy, int16_t xi, int16_t yi,
uint16_t rgb565, bool fill) {
if (fill) {
hspan(cx - xi, cy + yi, 2 * xi + 1, rgb565);
hspan(cx - xi, cy - yi, 2 * xi + 1, rgb565);
hspan(cx - yi, cy + xi, 2 * yi + 1, rgb565);
hspan(cx - yi, cy - xi, 2 * yi + 1, rgb565);
} else {
plotPixel(cx + xi, cy + yi, rgb565); plotPixel(cx - xi, cy + yi, rgb565);
plotPixel(cx + xi, cy - yi, rgb565); plotPixel(cx - xi, cy - yi, rgb565);
plotPixel(cx + yi, cy + xi, rgb565); plotPixel(cx - yi, cy + xi, rgb565);
plotPixel(cx + yi, cy - xi, rgb565); plotPixel(cx - yi, cy - xi, rgb565);
}
}
static void midpointCircle(int16_t r, int16_t& xi, int16_t& yi, int16_t& d) {
xi = 0; yi = r; d = 1 - r;
}
void UnitLcd::fillCircle(int16_t cx, int16_t cy, int16_t r, uint16_t rgb565) {
if (!dev_ || r < 0) return;
int16_t xi, yi, d;
midpointCircle(r, xi, yi, d);
while (xi <= yi) {
circleOctants(cx, cy, xi, yi, rgb565, true);
xi++;
if (d < 0) { d += 2 * xi + 1; }
else { yi--; d += 2 * (xi - yi) + 1; }
}
}
void UnitLcd::drawCircle(int16_t cx, int16_t cy, int16_t r, uint16_t rgb565) {
if (!dev_ || r < 0) return;
int16_t xi, yi, d;
midpointCircle(r, xi, yi, d);
while (xi <= yi) {
circleOctants(cx, cy, xi, yi, rgb565, false);
xi++;
if (d < 0) { d += 2 * xi + 1; }
else { yi--; d += 2 * (xi - yi) + 1; }
}
}
// ---------------------------------------------------------------------------
// Rounded rectangles
// ---------------------------------------------------------------------------
void UnitLcd::fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h,
int16_t r, uint16_t rgb565) {
if (!dev_) return;
if (r <= 0 || 2*r > w || 2*r > h) {
fillRect((uint8_t)x, (uint8_t)y, (uint8_t)(x+w-1), (uint8_t)(y+h-1), rgb565);
return;
}
// Two vertical rectangles covering the centre + top/bottom straight sections
fillRect((uint8_t)(x + r), (uint8_t)y, (uint8_t)(x + w - r - 1), (uint8_t)(y + h - 1), rgb565);
fillRect((uint8_t)x, (uint8_t)(y + r),(uint8_t)(x + r - 1), (uint8_t)(y + h - r - 1), rgb565);
fillRect((uint8_t)(x+w-r), (uint8_t)(y + r),(uint8_t)(x + w - 1), (uint8_t)(y + h - r - 1), rgb565);
// Corner arc spans
int16_t xi = 0, yi = r, d = 1 - r;
while (xi <= yi) {
// Top-left / top-right arcs
hspan(x + r - xi, y + r - yi, w - 2*(r - xi), rgb565);
// Bottom-left / bottom-right arcs
hspan(x + r - xi, y + h - 1 - r + yi, w - 2*(r - xi), rgb565);
if (xi != yi) {
hspan(x + r - yi, y + r - xi, w - 2*(r - yi), rgb565);
hspan(x + r - yi, y + h - 1 - r + xi, w - 2*(r - yi), rgb565);
}
xi++;
if (d < 0) d += 2*xi + 1; else { yi--; d += 2*(xi-yi)+1; }
}
}
void UnitLcd::drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h,
int16_t r, uint16_t rgb565) {
if (!dev_) return;
if (r <= 0 || 2*r > w || 2*r > h) { drawRect((uint8_t)x, (uint8_t)y, (uint8_t)w, (uint8_t)h, rgb565); return; }
// Straight edges
drawHLine((uint8_t)(x+r), (uint8_t)y, (uint8_t)(w - 2*r), rgb565);
drawHLine((uint8_t)(x+r), (uint8_t)(y+h-1), (uint8_t)(w - 2*r), rgb565);
drawVLine((uint8_t)x, (uint8_t)(y+r), (uint8_t)(h - 2*r), rgb565);
drawVLine((uint8_t)(x+w-1),(uint8_t)(y+r), (uint8_t)(h - 2*r), rgb565);
// Corner arcs
int16_t xi = 0, yi = r, d = 1 - r;
while (xi <= yi) {
plotPixel(x+r-xi, y+r-yi, rgb565); plotPixel(x+w-r+xi-1, y+r-yi, rgb565);
plotPixel(x+r-xi, y+h-r+yi-1,rgb565); plotPixel(x+w-r+xi-1, y+h-r+yi-1,rgb565);
plotPixel(x+r-yi, y+r-xi, rgb565); plotPixel(x+w-r+yi-1, y+r-xi, rgb565);
plotPixel(x+r-yi, y+h-r+xi-1,rgb565); plotPixel(x+w-r+yi-1, y+h-r+xi-1,rgb565);
xi++;
if (d < 0) d += 2*xi+1; else { yi--; d += 2*(xi-yi)+1; }
}
}
// ---------------------------------------------------------------------------
// Triangles
// ---------------------------------------------------------------------------
void UnitLcd::drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1,
int16_t x2, int16_t y2, uint16_t rgb565) {
drawLine(x0, y0, x1, y1, rgb565);
drawLine(x1, y1, x2, y2, rgb565);
drawLine(x2, y2, x0, y0, rgb565);
}
void UnitLcd::fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1,
int16_t x2, int16_t y2, uint16_t rgb565) {
if (!dev_) return;
// Sort vertices by Y (bubble sort, 3 elements)
if (y0 > y1) { std::swap(x0,x1); std::swap(y0,y1); }
if (y1 > y2) { std::swap(x1,x2); std::swap(y1,y2); }
if (y0 > y1) { std::swap(x0,x1); std::swap(y0,y1); }
if (y0 == y2) { // degenerate horizontal line
int16_t xlo = std::min({x0,x1,x2}), xhi = std::max({x0,x1,x2});
hspan(xlo, y0, xhi - xlo + 1, rgb565);
return;
}
// Scan-line fill using integer fixed-point slopes (×16 precision)
int32_t dx02 = ((int32_t)(x2 - x0) << 4) / (y2 - y0);
int32_t xa = ((int32_t)x0 << 4);
if (y1 == y0) {
// Flat top
int32_t dx12 = ((int32_t)(x2 - x1) << 4) / (y2 - y1);
int32_t xb = ((int32_t)x1 << 4);
for (int16_t y = y0; y <= y2; y++) {
int16_t xlo = (int16_t)(xa >> 4), xhi = (int16_t)(xb >> 4);
if (xlo > xhi) std::swap(xlo, xhi);
hspan(xlo, y, xhi - xlo + 1, rgb565);
xa += dx02; xb += dx12;
}
} else if (y1 == y2) {
// Flat bottom
int32_t dx01 = ((int32_t)(x1 - x0) << 4) / (y1 - y0);
int32_t xb = ((int32_t)x0 << 4);
for (int16_t y = y0; y <= y1; y++) {
int16_t xlo = (int16_t)(xa >> 4), xhi = (int16_t)(xb >> 4);
if (xlo > xhi) std::swap(xlo, xhi);
hspan(xlo, y, xhi - xlo + 1, rgb565);
xa += dx02; xb += dx01;
}
} else {
// General: upper half then lower half
int32_t dx01 = ((int32_t)(x1 - x0) << 4) / (y1 - y0);
int32_t xb = ((int32_t)x0 << 4);
for (int16_t y = y0; y < y1; y++) {
int16_t xlo = (int16_t)(xa >> 4), xhi = (int16_t)(xb >> 4);
if (xlo > xhi) std::swap(xlo, xhi);
hspan(xlo, y, xhi - xlo + 1, rgb565);
xa += dx02; xb += dx01;
}
int32_t dx12 = ((int32_t)(x2 - x1) << 4) / (y2 - y1);
xb = ((int32_t)x1 << 4);
for (int16_t y = y1; y <= y2; y++) {
int16_t xlo = (int16_t)(xa >> 4), xhi = (int16_t)(xb >> 4);
if (xlo > xhi) std::swap(xlo, xhi);
hspan(xlo, y, xhi - xlo + 1, rgb565);
xa += dx02; xb += dx12;
}
}
}
// ---------------------------------------------------------------------------
// Arc (filled annular wedge / outline)
// ---------------------------------------------------------------------------
// Implemented by scanning every pixel in the bounding box of the outer circle
// and testing (a) whether it falls within the annular ring r1..r0, and
// (b) whether the pixel's angle falls within startDeg..endDeg.
// For a 135×240 display this is at most 135*135 ≈ 18k pixels per call -
// slow compared to hardware fill, but correct and free of floating-point
// arc-length accumulation errors.
static constexpr float ARC_DEG2RAD = 3.14159265f / 180.0f;
void UnitLcd::arcImpl(int16_t cx, int16_t cy, int16_t r0, int16_t r1,
float startDeg, float endDeg, uint16_t rgb565, bool fill) {
if (!dev_ || r0 <= 0) return;
if (r1 < 0) r1 = 0;
if (r1 > r0) std::swap(r0, r1);
// Normalise angles to [0, 360)
startDeg = fmodf(startDeg, 360.0f);
if (startDeg < 0) startDeg += 360.0f;
endDeg = fmodf(endDeg, 360.0f);
if (endDeg < 0) endDeg += 360.0f;
bool wraps = (endDeg <= startDeg); // arc crosses 0°
int32_t r0sq = (int32_t)r0 * r0;
int32_t r1sq = (int32_t)r1 * r1;
int16_t W = (int16_t)width(), H = (int16_t)height();
for (int16_t y = -r0; y <= r0; y++) {
int16_t py = cy + y;
if (py < 0 || py >= H) continue;
for (int16_t x = -r0; x <= r0; x++) {
int16_t px = cx + x;
if (px < 0 || px >= W) continue;
int32_t d2 = (int32_t)x * x + (int32_t)y * y;
if (d2 > r0sq) continue;
if (fill) {
if (d2 < r1sq) continue;
} else {
// Outline: only pixels on the outer ring edge or radial endpoints
// outer ring: r0-1 < dist <= r0
bool onOuter = (d2 > (int32_t)(r0-1)*(r0-1));
bool onInner = (r1 > 0) && (d2 >= r1sq) && (d2 < (int32_t)(r1+1)*(r1+1));
if (!onOuter && !onInner) continue;
}
// Angle check (atan2 returns -π..π, convert to 0..360)
float ang = atan2f((float)y, (float)x) / ARC_DEG2RAD;
if (ang < 0) ang += 360.0f;
bool inSweep;
if (!wraps) inSweep = (ang >= startDeg && ang <= endDeg);
else inSweep = (ang >= startDeg || ang <= endDeg);
if (!inSweep) continue;
drawPixel((uint8_t)px, (uint8_t)py, rgb565);
}
}
}
void UnitLcd::fillArc(int16_t cx, int16_t cy, int16_t r0, int16_t r1,
float startDeg, float endDeg, uint16_t rgb565) {
arcImpl(cx, cy, r0, r1, startDeg, endDeg, rgb565, true);
}
void UnitLcd::drawArc(int16_t cx, int16_t cy, int16_t r0, int16_t r1,
float startDeg, float endDeg, uint16_t rgb565) {
arcImpl(cx, cy, r0, r1, startDeg, endDeg, rgb565, false);
}
// ---------------------------------------------------------------------------
// Text rendering - minimal 5×7 bitmap font (ASCII 32-126)
// Each entry is 5 bytes: one byte per column (bit 0 = top row).
// ---------------------------------------------------------------------------
static const uint8_t FONT5X7[][5] = {
{0x00,0x00,0x00,0x00,0x00}, // ' '
{0x00,0x00,0x5F,0x00,0x00}, // '!'
{0x00,0x07,0x00,0x07,0x00}, // '"'
{0x14,0x7F,0x14,0x7F,0x14}, // '#'
{0x24,0x2A,0x7F,0x2A,0x12}, // '$'
{0x23,0x13,0x08,0x64,0x62}, // '%'
{0x36,0x49,0x55,0x22,0x50}, // '&'
{0x00,0x05,0x03,0x00,0x00}, // '\''
{0x00,0x1C,0x22,0x41,0x00}, // '('
{0x00,0x41,0x22,0x1C,0x00}, // ')'
{0x08,0x2A,0x1C,0x2A,0x08}, // '*'
{0x08,0x08,0x3E,0x08,0x08}, // '+'
{0x00,0x50,0x30,0x00,0x00}, // ','
{0x08,0x08,0x08,0x08,0x08}, // '-'
{0x00,0x60,0x60,0x00,0x00}, // '.'
{0x20,0x10,0x08,0x04,0x02}, // '/'
{0x3E,0x51,0x49,0x45,0x3E}, // '0'
{0x00,0x42,0x7F,0x40,0x00}, // '1'
{0x42,0x61,0x51,0x49,0x46}, // '2'
{0x21,0x41,0x45,0x4B,0x31}, // '3'
{0x18,0x14,0x12,0x7F,0x10}, // '4'
{0x27,0x45,0x45,0x45,0x39}, // '5'
{0x3C,0x4A,0x49,0x49,0x30}, // '6'
{0x01,0x71,0x09,0x05,0x03}, // '7'
{0x36,0x49,0x49,0x49,0x36}, // '8'
{0x06,0x49,0x49,0x29,0x1E}, // '9'
{0x00,0x36,0x36,0x00,0x00}, // ':'
{0x00,0x56,0x36,0x00,0x00}, // ';'
{0x00,0x08,0x14,0x22,0x41}, // '<'
{0x14,0x14,0x14,0x14,0x14}, // '='
{0x41,0x22,0x14,0x08,0x00}, // '>'
{0x02,0x01,0x51,0x09,0x06}, // '?'
{0x32,0x49,0x79,0x41,0x3E}, // '@'
{0x7E,0x11,0x11,0x11,0x7E}, // 'A'
{0x7F,0x49,0x49,0x49,0x36}, // 'B'
{0x3E,0x41,0x41,0x41,0x22}, // 'C'
{0x7F,0x41,0x41,0x22,0x1C}, // 'D'
{0x7F,0x49,0x49,0x49,0x41}, // 'E'
{0x7F,0x09,0x09,0x09,0x01}, // 'F'
{0x3E,0x41,0x49,0x49,0x7A}, // 'G'
{0x7F,0x08,0x08,0x08,0x7F}, // 'H'
{0x00,0x41,0x7F,0x41,0x00}, // 'I'
{0x20,0x40,0x41,0x3F,0x01}, // 'J'
{0x7F,0x08,0x14,0x22,0x41}, // 'K'
{0x7F,0x40,0x40,0x40,0x40}, // 'L'
{0x7F,0x02,0x04,0x02,0x7F}, // 'M'
{0x7F,0x04,0x08,0x10,0x7F}, // 'N'
{0x3E,0x41,0x41,0x41,0x3E}, // 'O'
{0x7F,0x09,0x09,0x09,0x06}, // 'P'
{0x3E,0x41,0x51,0x21,0x5E}, // 'Q'
{0x7F,0x09,0x19,0x29,0x46}, // 'R'
{0x46,0x49,0x49,0x49,0x31}, // 'S'
{0x01,0x01,0x7F,0x01,0x01}, // 'T'
{0x3F,0x40,0x40,0x40,0x3F}, // 'U'
{0x1F,0x20,0x40,0x20,0x1F}, // 'V'
{0x3F,0x40,0x38,0x40,0x3F}, // 'W'
{0x63,0x14,0x08,0x14,0x63}, // 'X'
{0x07,0x08,0x70,0x08,0x07}, // 'Y'
{0x61,0x51,0x49,0x45,0x43}, // 'Z'
{0x00,0x7F,0x41,0x41,0x00}, // '['
{0x02,0x04,0x08,0x10,0x20}, // '\\'
{0x00,0x41,0x41,0x7F,0x00}, // ']'
{0x04,0x02,0x01,0x02,0x04}, // '^'
{0x40,0x40,0x40,0x40,0x40}, // '_'
{0x00,0x01,0x02,0x04,0x00}, // '`'
{0x20,0x54,0x54,0x54,0x78}, // 'a'
{0x7F,0x48,0x44,0x44,0x38}, // 'b'
{0x38,0x44,0x44,0x44,0x20}, // 'c'
{0x38,0x44,0x44,0x48,0x7F}, // 'd'
{0x38,0x54,0x54,0x54,0x18}, // 'e'
{0x08,0x7E,0x09,0x01,0x02}, // 'f'
{0x08,0x14,0x54,0x54,0x3C}, // 'g'
{0x7F,0x08,0x04,0x04,0x78}, // 'h'
{0x00,0x44,0x7D,0x40,0x00}, // 'i'
{0x20,0x40,0x44,0x3D,0x00}, // 'j'
{0x7F,0x10,0x28,0x44,0x00}, // 'k'
{0x00,0x41,0x7F,0x40,0x00}, // 'l'
{0x7C,0x04,0x18,0x04,0x78}, // 'm'
{0x7C,0x08,0x04,0x04,0x78}, // 'n'
{0x38,0x44,0x44,0x44,0x38}, // 'o'
{0x7C,0x14,0x14,0x14,0x08}, // 'p'
{0x08,0x14,0x14,0x18,0x7C}, // 'q'
{0x7C,0x08,0x04,0x04,0x08}, // 'r'
{0x48,0x54,0x54,0x54,0x20}, // 's'
{0x04,0x3F,0x44,0x40,0x20}, // 't'
{0x3C,0x40,0x40,0x40,0x7C}, // 'u'
{0x1C,0x20,0x40,0x20,0x1C}, // 'v'
{0x3C,0x40,0x30,0x40,0x3C}, // 'w'
{0x44,0x28,0x10,0x28,0x44}, // 'x'
{0x0C,0x50,0x50,0x50,0x3C}, // 'y'
{0x44,0x64,0x54,0x4C,0x44}, // 'z'
{0x00,0x08,0x36,0x41,0x00}, // '{'
{0x00,0x00,0x7F,0x00,0x00}, // '|'
{0x00,0x41,0x36,0x08,0x00}, // '}'
{0x08,0x04,0x08,0x10,0x08}, // '~'
};
void UnitLcd::drawChar(uint8_t x, uint8_t y, char ch, uint16_t fg, uint16_t bg, uint8_t scale) {
if (!dev_ || scale == 0) return;
if (ch < 32 || ch > 126) ch = '?';
const uint8_t* glyph = FONT5X7[ch - 32];
uint16_t W = width(), H = height();
for (uint8_t col = 0; col < 5; col++) {
uint8_t bits = glyph[col];
for (uint8_t row = 0; row < 7; row++) {
uint16_t color = (bits & (1u << row)) ? fg : bg;
uint16_t px = (uint16_t)x + col * scale;
uint16_t py = (uint16_t)y + row * scale;
if (px >= W || py >= H) continue;
if (scale == 1) {
drawPixel((uint8_t)px, (uint8_t)py, color);
} else {
uint16_t px1 = std::min((uint16_t)(px + scale - 1), (uint16_t)(W - 1));
uint16_t py1 = std::min((uint16_t)(py + scale - 1), (uint16_t)(H - 1));
fillRect((uint8_t)px, (uint8_t)py, (uint8_t)px1, (uint8_t)py1, color);
}
}
}
// Trailing gap column in background colour
uint16_t gx = (uint16_t)x + 5 * scale;
if (gx < W) {
uint16_t gx1 = std::min((uint16_t)(gx + scale - 1), (uint16_t)(W - 1));
uint16_t gy1 = std::min((uint16_t)(y + 7 * scale - 1), (uint16_t)(H - 1));
fillRect(gx, y, (uint8_t)gx1, (uint8_t)gy1, bg);
}
}
void UnitLcd::drawText(uint8_t x, uint8_t y, const char* str, uint16_t fg, uint16_t bg, uint8_t scale) {
if (!dev_ || !str) return;
uint8_t cx = x;
uint16_t charWidth = 6 * scale;
while (*str) {
if (cx + charWidth > width()) break; // Stop if next char would be off-screen
drawChar(cx, y, *str++, fg, bg, scale);
cx += charWidth;
}
}
@@ -0,0 +1,93 @@
#include <UnitMidi.h>
#include <tactility/drivers/uart_controller.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
static constexpr auto* TAG = "UnitMidi";
static constexpr uint32_t BAUD = 31250;
static constexpr uint32_t SEND_TIMEOUT_MS = 20;
bool UnitMidi::begin(Device* dev) {
if (!dev) return false;
UartConfig cfg = {
BAUD,
UART_CONTROLLER_DATA_8_BITS,
UART_CONTROLLER_PARITY_DISABLE,
UART_CONTROLLER_STOP_BITS_1,
};
if (uart_controller_set_config(dev, &cfg) != ERROR_NONE) {
ESP_LOGW(TAG, "uart_controller_set_config failed");
return false;
}
if (uart_controller_open(dev) != ERROR_NONE) {
ESP_LOGW(TAG, "uart_controller_open failed");
return false;
}
dev_ = dev;
ready_ = true;
// Give SAM2695 time to power up, then reset to known state
vTaskDelay(pdMS_TO_TICKS(100));
reset();
ESP_LOGI(TAG, "UnitMidi ready at %" PRIu32 " bps", BAUD);
return true;
}
void UnitMidi::end() {
if (dev_ && ready_) {
allNotesOff(0xFF);
uart_controller_close(dev_);
}
ready_ = false;
dev_ = nullptr;
}
void UnitMidi::send(const uint8_t* data, size_t len) {
if (!ready_ || !dev_) return;
uart_controller_write_bytes(dev_, data, len, pdMS_TO_TICKS(SEND_TIMEOUT_MS));
}
void UnitMidi::reset() {
uint8_t msg[] = { 0xFF };
send(msg, 1);
}
void UnitMidi::noteOn(uint8_t channel, uint8_t note, uint8_t velocity) {
uint8_t msg[] = { (uint8_t)(0x90 | (channel & 0x0F)), (uint8_t)(note & 0x7F), (uint8_t)(velocity & 0x7F) };
send(msg, 3);
}
void UnitMidi::noteOff(uint8_t channel, uint8_t note) {
uint8_t msg[] = { (uint8_t)(0x80 | (channel & 0x0F)), (uint8_t)(note & 0x7F), 0x00 };
send(msg, 3);
}
void UnitMidi::programChange(uint8_t channel, uint8_t program) {
uint8_t msg[] = { (uint8_t)(0xC0 | (channel & 0x0F)), (uint8_t)(program & 0x7F) };
send(msg, 2);
}
void UnitMidi::controlChange(uint8_t channel, uint8_t controller, uint8_t value) {
uint8_t msg[] = { (uint8_t)(0xB0 | (channel & 0x0F)), (uint8_t)(controller & 0x7F), (uint8_t)(value & 0x7F) };
send(msg, 3);
}
void UnitMidi::pitchBend(uint8_t channel, int16_t value) {
// value range -8192..+8191 → offset by 8192, split into 7-bit LSB/MSB
uint16_t v = (uint16_t)(value + 8192);
uint8_t msg[] = {
(uint8_t)(0xE0 | (channel & 0x0F)),
(uint8_t)(v & 0x7F),
(uint8_t)((v >> 7) & 0x7F),
};
send(msg, 3);
}
void UnitMidi::allNotesOff(uint8_t channel) {
if (channel == 0xFF) {
for (uint8_t ch = 0; ch < 16; ch++)
controlChange(ch, 123, 0);
} else {
controlChange(channel, 123, 0);
}
}
@@ -0,0 +1,42 @@
#include <UnitPaHub.h>
#include <esp_log.h>
static constexpr auto* TAG = "UnitPaHub";
bool UnitPaHub::begin(Device* dev, uint8_t addr) {
if (!dev || !device_is_ready(dev)) return false;
if (!unitProbe(dev, addr)) {
ESP_LOGW(TAG, "PaHub not found at 0x%02X", addr);
return false;
}
dev_ = dev;
addr_ = addr;
// Disable all channels on init
if (!deselect()) {
ESP_LOGE(TAG, "PaHub deselect failed at 0x%02X", addr_);
dev_ = nullptr;
addr_ = 0;
return false;
}
ESP_LOGI(TAG, "PaHub ready at 0x%02X", addr_);
return true;
}
bool UnitPaHub::select(uint8_t channel) {
if (!dev_ || channel >= NUM_CHANNELS) return false;
// TCA9548A: write one byte - bit i set = channel i enabled
uint8_t mask = (uint8_t)(1u << channel);
bool ok = i2c_controller_write(dev_, addr_, &mask, 1,
pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE;
if (ok) channel_ = channel;
return ok;
}
bool UnitPaHub::deselect() {
if (!dev_) return false;
uint8_t mask = 0x00;
bool ok = i2c_controller_write(dev_, addr_, &mask, 1,
pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE;
if (ok) channel_ = NO_CHANNEL;
return ok;
}
@@ -0,0 +1,657 @@
#include <UnitRfid2.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <cstring>
static constexpr auto* TAG = "UnitRfid2";
const UnitRfid2::MifareKey UnitRfid2::KEY_DEFAULT = {{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }};
// 15 commonly-found MIFARE Classic keys (from Bruce firmware / public key databases)
const UnitRfid2::MifareKey UnitRfid2::KNOWN_KEYS[15] = {
{{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }}, // factory default
{{ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 }},
{{ 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5 }},
{{ 0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD }},
{{ 0x1A, 0x98, 0x2C, 0x7E, 0x45, 0x9A }},
{{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }},
{{ 0x71, 0x4C, 0x5C, 0x88, 0x6E, 0x97 }},
{{ 0x58, 0x7E, 0xE5, 0xF9, 0x35, 0x0F }},
{{ 0xA0, 0x47, 0x8C, 0xC3, 0x90, 0x91 }},
{{ 0x53, 0x3C, 0xB6, 0xC7, 0x23, 0xF6 }},
{{ 0x8F, 0xD0, 0xA4, 0xF2, 0x56, 0xE9 }},
{{ 0xA6, 0x45, 0x98, 0xA7, 0x74, 0x78 }},
{{ 0x26, 0x94, 0x0B, 0x21, 0xFF, 0x5D }},
{{ 0xFC, 0x00, 0x01, 0x87, 0x78, 0xF7 }},
{{ 0x00, 0x00, 0x0F, 0xFE, 0x24, 0x88 }},
};
// ---------------------------------------------------------------------------
// Low-level register access
// ---------------------------------------------------------------------------
void UnitRfid2::writeReg(uint8_t reg, uint8_t val) {
unitWriteReg(dev_, addr_, reg, &val, 1);
}
uint8_t UnitRfid2::readReg(uint8_t reg) {
uint8_t val = 0;
unitReadReg(dev_, addr_, reg, &val, 1);
return val;
}
void UnitRfid2::setBitMask(uint8_t reg, uint8_t mask) {
writeReg(reg, readReg(reg) | mask);
}
void UnitRfid2::clearBitMask(uint8_t reg, uint8_t mask) {
writeReg(reg, readReg(reg) & ~mask);
}
void UnitRfid2::writeFifo(const uint8_t* buf, uint8_t len) {
for (uint8_t i = 0; i < len; i++)
writeReg(REG_FIFO_DATA, buf[i]);
}
// ---------------------------------------------------------------------------
// Initialisation
// ---------------------------------------------------------------------------
bool UnitRfid2::softReset() {
writeReg(REG_COMMAND, CMD_SOFT_RESET);
vTaskDelay(pdMS_TO_TICKS(50));
for (int i = 0; i < 10; i++) {
if (!(readReg(REG_COMMAND) & 0x10)) return true;
vTaskDelay(pdMS_TO_TICKS(10));
}
return false;
}
void UnitRfid2::antennaOn() {
uint8_t val = readReg(REG_TX_CONTROL);
if (!(val & 0x03))
setBitMask(REG_TX_CONTROL, 0x03);
}
bool UnitRfid2::begin(Device* dev, uint8_t addr) {
if (!dev || !device_is_ready(dev)) return false;
if (!unitProbe(dev, addr)) {
ESP_LOGW(TAG, "RFID2 not found at 0x%02X", addr);
return false;
}
dev_ = dev;
addr_ = addr;
if (!softReset()) {
ESP_LOGE(TAG, "RFID2 soft reset failed at 0x%02X", addr_);
dev_ = nullptr;
return false;
}
// Timer: auto mode, prescaler for ~25ms timeout
writeReg(REG_TMODE, 0x80);
writeReg(REG_TPRESCALER, 0xA9);
writeReg(REG_TRELOAD_H, 0x03);
writeReg(REG_TRELOAD_L, 0xE8);
// 100% ASK modulation; CRC preset 0x6363
writeReg(REG_TX_ASK, 0x40);
writeReg(REG_MODE, 0x3D);
antennaOn();
ESP_LOGI(TAG, "RFID2 ready at 0x%02X", addr_);
return true;
}
// ---------------------------------------------------------------------------
// Hardware CRC
// ---------------------------------------------------------------------------
bool UnitRfid2::calcCRC(const uint8_t* data, uint8_t len, uint8_t result[2]) {
writeReg(REG_COMMAND, CMD_IDLE);
writeReg(REG_DIV_IRQ, 0x04); // clear CRCIRq
setBitMask(REG_FIFO_LEVEL, 0x80); // flush FIFO
writeFifo(data, len);
writeReg(REG_COMMAND, CMD_CALC_CRC);
for (int i = 0; i < 500; i++) {
if (readReg(REG_DIV_IRQ) & 0x04) {
writeReg(REG_COMMAND, CMD_IDLE);
result[0] = readReg(REG_CRC_RESULT_L);
result[1] = readReg(REG_CRC_RESULT_H);
return true;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
return false;
}
// ---------------------------------------------------------------------------
// Transceive
// ---------------------------------------------------------------------------
uint8_t UnitRfid2::transceive(const uint8_t* txBuf, uint8_t txLen,
uint8_t* rxBuf, uint8_t rxMaxLen,
uint8_t* rxValidBits) {
writeReg(REG_COM_IRQ, 0x7F);
setBitMask(REG_FIFO_LEVEL, 0x80);
writeReg(REG_COMMAND, CMD_IDLE);
writeFifo(txBuf, txLen);
writeReg(REG_COMMAND, CMD_TRANSCEIVE);
setBitMask(REG_BIT_FRAMING, 0x80);
uint8_t irq = 0;
for (int i = 0; i < 200; i++) {
irq = readReg(REG_COM_IRQ);
if (irq & 0x31) break; // RxIRq | IdleIRq | TimerIRq
vTaskDelay(pdMS_TO_TICKS(1));
}
clearBitMask(REG_BIT_FRAMING, 0x80);
if (!(irq & 0x01) && (irq & 0x30) == 0) return 0;
if (readReg(REG_ERROR) & 0x1B) return 0;
uint8_t n = readReg(REG_FIFO_LEVEL) & 0x7F;
if (n > rxMaxLen) n = rxMaxLen;
if (!rxBuf || rxMaxLen == 0) return 0;
for (uint8_t i = 0; i < n; i++)
rxBuf[i] = readReg(REG_FIFO_DATA);
if (rxValidBits)
*rxValidBits = readReg(REG_CONTROL) & 0x07;
return n;
}
bool UnitRfid2::transceiveCRC(const uint8_t* txBuf, uint8_t txLen,
uint8_t* rxBuf, uint8_t rxMaxLen, uint8_t* rxLen) {
uint8_t crc[2];
if (!calcCRC(txBuf, txLen, crc)) return false;
uint8_t buf[34];
if ((size_t)txLen + 2 > sizeof(buf)) return false;
memcpy(buf, txBuf, txLen);
buf[txLen] = crc[0];
buf[txLen + 1] = crc[1];
uint8_t n = transceive(buf, txLen + 2, rxBuf, rxMaxLen);
if (rxLen) *rxLen = n;
// Strip and verify CRC on response (last 2 bytes)
if (n >= 2) {
uint8_t rxCrc[2];
if (!calcCRC(rxBuf, n - 2, rxCrc)) return false;
if (rxCrc[0] != rxBuf[n - 2] || rxCrc[1] != rxBuf[n - 1]) return false;
if (rxLen) *rxLen = n - 2;
}
return n > 0;
}
// ---------------------------------------------------------------------------
// ISO 14443A - REQA / WUPA and full anticollision/SELECT
// ---------------------------------------------------------------------------
// Send a 7-bit short frame command (REQA=0x26 or WUPA=0x52).
// REQA only wakes IDLE cards; WUPA wakes both IDLE and HALT cards.
bool UnitRfid2::requestA(uint8_t atqa[2], uint8_t cmd) {
writeReg(REG_BIT_FRAMING, 0x07);
uint8_t rx[2] = {};
uint8_t rxLen = transceive(&cmd, 1, rx, 2);
writeReg(REG_BIT_FRAMING, 0x00);
if (rxLen != 2) return false;
if (atqa) memcpy(atqa, rx, 2);
return true;
}
// Full ISO 14443-3 anticollision + SELECT; handles 4-byte and 7-byte UIDs.
bool UnitRfid2::select(Uid* uid) {
if (!uid) return false;
static constexpr uint8_t selCmd[3] = { PICC_SEL_CL1, PICC_SEL_CL2, PICC_SEL_CL3 };
uint8_t uidBytes[10] = {};
uint8_t uidSize = 0;
for (int cascade = 0; cascade < 3; cascade++) {
writeReg(REG_COLL, 0x80); // ValuesAfterColl: don't clear bits on collision
writeReg(REG_BIT_FRAMING, 0x00);
uint8_t anticollCmd[2] = { selCmd[cascade], PICC_ANTICOLL };
uint8_t rx[5] = {};
uint8_t rxLen = transceive(anticollCmd, 2, rx, 5);
writeReg(REG_COLL, 0x00);
if (rxLen < 5) return false;
// Verify BCC
uint8_t bcc = rx[0] ^ rx[1] ^ rx[2] ^ rx[3];
if (bcc != rx[4]) return false;
bool hasCT = (rx[0] == PICC_CT);
// SELECT: NVB=0x70 means all 40 bits follow
uint8_t selBuf[9];
selBuf[0] = selCmd[cascade];
selBuf[1] = 0x70;
memcpy(selBuf + 2, rx, 5); // 4 UID bytes + BCC
uint8_t crc[2];
if (!calcCRC(selBuf, 7, crc)) return false;
selBuf[7] = crc[0];
selBuf[8] = crc[1];
uint8_t selRx[3] = {};
uint8_t selRxLen = transceive(selBuf, 9, selRx, 3);
// Expect SAK (1 byte) + 2 CRC bytes
if (selRxLen < 3) return false;
uint8_t sakCrc[2];
if (!calcCRC(selRx, 1, sakCrc)) return false;
if (sakCrc[0] != selRx[1] || sakCrc[1] != selRx[2]) return false;
uint8_t sak = selRx[0];
if (hasCT) {
// Cascade tag byte: skip CT, copy next 3 bytes as partial UID
memcpy(uidBytes + uidSize, rx + 1, 3);
uidSize += 3;
} else {
memcpy(uidBytes + uidSize, rx, 4);
uidSize += 4;
}
if (!(sak & 0x04)) {
// UID complete (cascade bit not set)
uid->size = uidSize;
memcpy(uid->bytes, uidBytes, uidSize);
uid->sak = sak;
return true;
}
// Continue to next cascade level
}
return false;
}
// ---------------------------------------------------------------------------
// Public card read
// ---------------------------------------------------------------------------
bool UnitRfid2::readCard(Uid* uid) {
if (!dev_ || !uid) return false;
uint8_t atqa[2] = {};
// Use WUPA (0x52) so we also wake cards left in HALT state (e.g. after haltCard()).
if (!requestA(atqa, PICC_WUPA)) return false;
uid->atqa[0] = atqa[0];
uid->atqa[1] = atqa[1];
return select(uid);
}
// ---------------------------------------------------------------------------
// Legacy compatibility
// ---------------------------------------------------------------------------
bool UnitRfid2::isCardPresent() {
if (!dev_) return false;
uint8_t atqa[2];
// WUPA wakes both IDLE and HALT cards, giving a reliable presence signal.
return requestA(atqa, PICC_WUPA);
}
uint8_t UnitRfid2::readUID(uint8_t* uid, uint8_t maxLen) {
if (!dev_ || !uid || maxLen < 4) return 0;
Uid u;
if (!readCard(&u)) return 0;
uint8_t n = u.size < maxLen ? u.size : maxLen;
memcpy(uid, u.bytes, n);
return n;
}
// ---------------------------------------------------------------------------
// Card type detection
// ---------------------------------------------------------------------------
UnitRfid2::CardType UnitRfid2::getCardType(const Uid& uid) {
switch (uid.sak & 0x7F) {
case 0x09: return CardType::MifareClassicMini;
case 0x08: return CardType::MifareClassic1K;
case 0x18: return CardType::MifareClassic4K;
case 0x00: break;
default: return CardType::Unknown;
}
// SAK=0x00 → Ultralight/NTAG: probe capability container at page 3
uint8_t cc[4] = {};
if (!ulReadPage(3, cc)) return CardType::MifareUltralight;
switch (cc[2]) {
case 0x12: return CardType::NTAG213;
case 0x3E: return CardType::NTAG215;
case 0x6D: return CardType::NTAG216;
default: return CardType::MifareUltralight;
}
}
const char* UnitRfid2::cardTypeName(CardType t) {
switch (t) {
case CardType::MifareClassicMini: return "MIFARE Mini";
case CardType::MifareClassic1K: return "MIFARE Classic 1K";
case CardType::MifareClassic4K: return "MIFARE Classic 4K";
case CardType::MifareUltralight: return "MIFARE Ultralight";
case CardType::NTAG213: return "NTAG213";
case CardType::NTAG215: return "NTAG215";
case CardType::NTAG216: return "NTAG216";
default: return "Unknown";
}
}
uint8_t UnitRfid2::ultralightPageCount(CardType t) {
switch (t) {
case CardType::MifareUltralight: return 12; // pages 4-15
case CardType::NTAG213: return 41; // pages 4-44
case CardType::NTAG215: return 131; // pages 4-134
case CardType::NTAG216: return 227; // pages 4-230
default: return 0;
}
}
// ---------------------------------------------------------------------------
// MIFARE Classic - authentication
// ---------------------------------------------------------------------------
bool UnitRfid2::mfAuthenticate(uint8_t authCmd, uint8_t block,
const MifareKey& key, const Uid& uid) {
stopCrypto1();
writeReg(REG_COM_IRQ, 0x7F);
setBitMask(REG_FIFO_LEVEL, 0x80);
writeReg(REG_COMMAND, CMD_IDLE);
// FIFO payload: [authCmd, block, key[6], uid_last4[4]]
uint8_t buf[12];
buf[0] = authCmd;
buf[1] = block;
memcpy(buf + 2, key.k, 6);
uint8_t uidOffset = (uid.size > 4) ? uid.size - 4 : 0;
memcpy(buf + 8, uid.bytes + uidOffset, 4);
writeFifo(buf, 12);
writeReg(REG_COMMAND, CMD_MF_AUTHENT);
for (int i = 0; i < 200; i++) {
uint8_t irq = readReg(REG_COM_IRQ);
if (irq & 0x10) break; // IdleIRq
if (irq & 0x01) { ESP_LOGW(TAG, "auth timeout block=%u", block); return false; }
vTaskDelay(pdMS_TO_TICKS(1));
}
if (!(readReg(REG_STATUS2) & 0x08)) {
ESP_LOGW(TAG, "MFCrypto1On not set (block=%u)", block);
return false;
}
return true;
}
void UnitRfid2::stopCrypto1() {
clearBitMask(REG_STATUS2, 0x08);
}
// ---------------------------------------------------------------------------
// MIFARE Classic - block read/write (internal, assumes auth already done)
// ---------------------------------------------------------------------------
bool UnitRfid2::mfReadBlock16(uint8_t block, uint8_t out[16]) {
uint8_t cmd[2] = { PICC_MF_READ, block };
uint8_t rx[18] = {};
uint8_t rxLen = 0;
if (!transceiveCRC(cmd, 2, rx, 18, &rxLen)) return false;
if (rxLen < 16) return false;
memcpy(out, rx, 16);
return true;
}
bool UnitRfid2::mfWriteBlock16(uint8_t block, const uint8_t data[16]) {
// Phase 1: send WRITE + block address, expect 4-bit ACK
uint8_t cmd[2] = { PICC_MF_WRITE, block };
uint8_t crc[2];
if (!calcCRC(cmd, 2, crc)) return false;
uint8_t phase1[4] = { cmd[0], cmd[1], crc[0], crc[1] };
uint8_t ack = 0;
uint8_t validBits = 0;
uint8_t n = transceive(phase1, 4, &ack, 1, &validBits);
if (n == 0 || (ack & 0x0F) != 0x0A) return false;
// Phase 2: send 16 bytes + CRC
if (!calcCRC(data, 16, crc)) return false;
uint8_t phase2[18];
memcpy(phase2, data, 16);
phase2[16] = crc[0];
phase2[17] = crc[1];
ack = 0; validBits = 0;
n = transceive(phase2, 18, &ack, 1, &validBits);
return (n > 0 && (ack & 0x0F) == 0x0A);
}
// ---------------------------------------------------------------------------
// Public MIFARE Classic API
// ---------------------------------------------------------------------------
bool UnitRfid2::mfReadBlock(uint8_t block, const Uid& uid,
const MifareKey& keyA, uint8_t out[16]) {
if (!mfAuthenticate(PICC_MF_AUTH_KEY_A, block, keyA, uid)) return false;
return mfReadBlock16(block, out);
}
bool UnitRfid2::mfWriteBlock(uint8_t block, const Uid& uid,
const MifareKey& keyA, const uint8_t data[16]) {
if (!mfAuthenticate(PICC_MF_AUTH_KEY_A, block, keyA, uid)) return false;
return mfWriteBlock16(block, data);
}
uint8_t UnitRfid2::mfSectorBlockCount(uint8_t sector) {
if (sector >= 40) return 0;
return (sector < 32) ? 4 : 16;
}
bool UnitRfid2::mfReadSector(uint8_t sector, const Uid& uid,
const MifareKey& keyA, uint8_t* out, uint8_t* blockCountOut) {
uint8_t count = mfSectorBlockCount(sector);
if (count == 0) return false;
// Sectors 0-31: 4-block layout; sectors 32-39: 16-block layout (4K).
uint8_t firstBlock = (sector < 32) ? (sector * 4) : (128 + (sector - 32) * 16);
if (blockCountOut) *blockCountOut = count;
if (!mfAuthenticate(PICC_MF_AUTH_KEY_A, firstBlock, keyA, uid)) return false;
for (uint8_t b = 0; b < count; b++) {
if (!mfReadBlock16(firstBlock + b, out + b * 16)) return false;
}
return true;
}
bool UnitRfid2::mfReadBlockKeyAB(uint8_t block, const Uid& uid,
const MifareKey& key, uint8_t out[16]) {
if (mfAuthenticate(PICC_MF_AUTH_KEY_A, block, key, uid) && mfReadBlock16(block, out))
return true;
stopCrypto1();
if (mfAuthenticate(PICC_MF_AUTH_KEY_B, block, key, uid) && mfReadBlock16(block, out))
return true;
stopCrypto1();
return false;
}
bool UnitRfid2::mfReadBlockAuto(uint8_t block, const Uid& uid,
uint8_t out[16], MifareKey* keyUsedOut) {
for (uint8_t i = 0; i < KNOWN_KEY_COUNT; i++) {
if (mfAuthenticate(PICC_MF_AUTH_KEY_A, block, KNOWN_KEYS[i], uid)) {
if (mfReadBlock16(block, out)) {
if (keyUsedOut) *keyUsedOut = KNOWN_KEYS[i];
return true;
}
}
stopCrypto1();
if (mfAuthenticate(PICC_MF_AUTH_KEY_B, block, KNOWN_KEYS[i], uid)) {
if (mfReadBlock16(block, out)) {
if (keyUsedOut) *keyUsedOut = KNOWN_KEYS[i];
return true;
}
}
stopCrypto1();
}
return false;
}
// ---------------------------------------------------------------------------
// Magic card UID write (gen1a backdoor)
// ---------------------------------------------------------------------------
bool UnitRfid2::mfMagicOpen() {
// gen1a backdoor: send 0x40 (7-bit), then 0x43
writeReg(REG_BIT_FRAMING, 0x07);
uint8_t cmd1 = 0x40;
uint8_t rx[1] = {};
transceive(&cmd1, 1, rx, 1);
writeReg(REG_BIT_FRAMING, 0x00);
uint8_t cmd2 = 0x43;
uint8_t rx2[1] = {};
uint8_t n = transceive(&cmd2, 1, rx2, 1);
return (n > 0 && rx2[0] == 0x0A);
}
bool UnitRfid2::mfWriteUid(const uint8_t newUid[4], const Uid& uid) {
if (!dev_) return false;
// Must wake and re-select to get into ACTIVE state
uint8_t atqa[2];
if (!requestA(atqa, PICC_WUPA)) return false;
Uid localUid = uid;
if (!select(&localUid)) return false;
if (!mfMagicOpen()) {
ESP_LOGW(TAG, "Magic backdoor open failed - not a gen1a card?");
return false;
}
// Build block 0: UID[0..3] + BCC + SAK + ATQA[0] + ATQA[1] + 0x00*8
uint8_t block0[16] = {};
memcpy(block0, newUid, 4);
block0[4] = newUid[0] ^ newUid[1] ^ newUid[2] ^ newUid[3]; // BCC
block0[5] = uid.sak;
block0[6] = uid.atqa[0];
block0[7] = uid.atqa[1];
bool ok = mfWriteBlock16(0, block0);
haltCard();
return ok;
}
uint8_t UnitRfid2::mfErase(const Uid& uid, const MifareKey& keyA) {
if (!dev_) return 0;
static const uint8_t zeros[16] = {};
uint8_t erased = 0;
CardType t = getCardType(uid);
uint8_t maxBlock = 63;
if (t == CardType::MifareClassicMini) maxBlock = 19;
else if (t == CardType::MifareClassic4K) maxBlock = 255;
// Initial select to put the card into ACTIVE state for sector 0 auth.
{ Uid tmp = {}; if (!readCard(&tmp)) return 0; }
for (uint8_t block = 1; block <= maxBlock; block++) {
// 4K: sectors 0-31 have 4 blocks (trailer at %4==3),
// sectors 32-39 have 16 blocks (trailer at %16==15, block >= 128).
bool isTrailer = (block < 128) ? (block % 4 == 3) : (block % 16 == 15);
if (isTrailer) continue;
// Re-select card before each new sector's first data block so AUTHENT succeeds.
bool isFirstInSector = (block < 128) ? (block % 4 == 0) : (block % 16 == 0);
if (isFirstInSector) {
Uid tmp = {};
if (!readCard(&tmp)) return erased; // card removed
}
if (mfWriteBlock(block, uid, keyA, zeros)) erased++;
}
return erased;
}
// ---------------------------------------------------------------------------
// MIFARE Ultralight / NTAG
// ---------------------------------------------------------------------------
bool UnitRfid2::ulReadPage(uint8_t page, uint8_t out4[4]) {
// READ returns 16 bytes (4 pages); we take the first page
uint8_t cmd[2] = { PICC_MF_READ, page };
uint8_t rx[18] = {};
uint8_t rxLen = 0;
if (!transceiveCRC(cmd, 2, rx, 18, &rxLen)) return false;
if (rxLen < 4) return false;
memcpy(out4, rx, 4);
return true;
}
bool UnitRfid2::ulReadPages(uint8_t startPage, uint8_t count, uint8_t* out) {
for (uint8_t i = 0; i < count; i++) {
if (!ulReadPage(startPage + i, out + i * 4)) return false;
}
return true;
}
bool UnitRfid2::ulWritePage(uint8_t page, const uint8_t data4[4], bool force) {
// Pages 0-1: UID (factory-locked). Page 2: lock bytes. Page 3: OTP (one-time).
// Guard against accidental writes unless the caller explicitly opts in.
if (page < 4 && !force) {
ESP_LOGW(TAG, "ulWritePage: page %u is UID/lock/OTP - use force=true to override", page);
return false;
}
uint8_t cmd[6] = { PICC_UL_WRITE, page, data4[0], data4[1], data4[2], data4[3] };
uint8_t crc[2];
if (!calcCRC(cmd, 6, crc)) return false;
uint8_t txBuf[8];
memcpy(txBuf, cmd, 6);
txBuf[6] = crc[0];
txBuf[7] = crc[1];
uint8_t ack = 0;
uint8_t validBits = 0;
uint8_t n = transceive(txBuf, 8, &ack, 1, &validBits);
return (n > 0 && (ack & 0x0F) == 0x0A);
}
// ---------------------------------------------------------------------------
// HALT
// ---------------------------------------------------------------------------
bool UnitRfid2::haltA() {
uint8_t cmd[2] = { PICC_HLTA, 0x00 };
uint8_t crc[2];
if (!calcCRC(cmd, 2, crc)) return false;
uint8_t txBuf[4] = { cmd[0], cmd[1], crc[0], crc[1] };
uint8_t rx[1];
transceive(txBuf, 4, rx, 1); // no response expected
return true;
}
uint8_t UnitRfid2::ulErase(CardType t) {
if (!dev_) return 0;
static const uint8_t zeros[4] = {};
uint8_t count = ultralightPageCount(t);
if (count == 0) return 0;
uint8_t erased = 0;
for (uint8_t page = 4; page < 4 + count; page++) {
if (ulWritePage(page, zeros)) erased++;
}
return erased;
}
void UnitRfid2::haltCard() {
if (!dev_) return;
haltA();
stopCrypto1();
}
@@ -0,0 +1,58 @@
#include <UnitScroll.h>
#include <esp_log.h>
static constexpr auto* TAG = "UnitScroll";
bool UnitScroll::begin(Device* dev, uint8_t addr) {
if (!dev || !device_is_ready(dev)) return false;
if (!unitProbe(dev, addr)) {
ESP_LOGW(TAG, "Scroll not found at 0x%02X", addr);
return false;
}
dev_ = dev;
addr_ = addr;
ESP_LOGI(TAG, "Scroll ready at 0x%02X", addr_);
return true;
}
int16_t UnitScroll::readDelta() {
if (!dev_) return 0;
int16_t val = 0;
if (!unitReadReg(dev_, addr_, REG_INC_ENCODER, (uint8_t*)&val, 2))
ESP_LOGW(TAG, "readDelta failed at 0x%02X", addr_);
return val;
}
int16_t UnitScroll::readAbsolute() const {
if (!dev_) return 0;
int16_t val = 0;
if (!unitReadReg(dev_, addr_, REG_ENCODER, (uint8_t*)&val, 2))
ESP_LOGW(TAG, "readAbsolute failed at 0x%02X", addr_);
return val;
}
bool UnitScroll::isPressed() const {
if (!dev_) return false;
uint8_t val = 1;
if (!unitReadReg(dev_, addr_, REG_BUTTON, &val, 1))
ESP_LOGW(TAG, "isPressed read failed at 0x%02X", addr_);
return val == 0; // hardware: 0=pressed, 1=released
}
void UnitScroll::setLed(uint32_t rgb) {
if (!dev_) return;
// Wire format: [index=0, R, G, B]
uint8_t buf[4] = {
0x00,
(uint8_t)((rgb >> 16) & 0xFF), // R
(uint8_t)((rgb >> 8) & 0xFF), // G
(uint8_t)( rgb & 0xFF), // B
};
unitWriteReg(dev_, addr_, REG_LED, buf, 4);
}
void UnitScroll::resetEncoder() {
if (!dev_) return;
uint8_t one = 1;
unitWriteReg(dev_, addr_, REG_RESET, &one, 1);
}
+340
View File
@@ -0,0 +1,340 @@
# M5Unit Module References
Quick reference for all M5Stack units supported by the M5UnitModules library.
Each entry links to the official M5Stack docs and the upstream Arduino library.
---
## PaHub v2.1 (I2C Hub)
| Property | Value |
|---|---|
| Interface | I2C |
| I2C Address | 0x70 (default), configurable |
| Channels | 6 (CH0-CH5) |
| Driver class | `UnitPaHub` |
PCA9548A-based I2C multiplexer. Allows up to 6 I2C devices with the same
address to share a single bus. Select a channel before talking to a device
behind it; deselect (mask = 0) when done.
- Docs: https://docs.m5stack.com/en/unit/Unit-PaHub%20v2.1
- Repo: https://github.com/m5stack/M5Unit-HUB
**Register map:**
| Addr | Operation | Description |
|------|-----------|-------------|
| 0x70 | W: bitmask | Enable channels (bit i = channel i). Write 0x00 to deselect all. |
---
## CardKB2 (Keyboard)
| Property | Value |
|---|---|
| Interface | I2C (default) or UART |
| I2C Address | 0x5F |
| UART | 115200-8N-1, via GROVE port |
| MCU | STM32 |
| Driver class | `UnitCardKB2` |
Full QWERTY keyboard in a credit-card form factor. The STM32 firmware
auto-repeats keys (300 ms initial delay, 50 ms repeat). Mode switching
(I2C/UART/ESP-NOW/BLE-HID) is done via Fn+Sym+[1-4] and persists across
reboots.
- Docs: https://docs.m5stack.com/en/unit/Unit_CardKB2
- Repo: https://github.com/m5stack/M5Unit-KEYBOARD
**I2C protocol:** single I2C read (no register write) returns current key ASCII
value. 0x00 = no key or I2C error (indistinguishable by design).
**UART protocol:** 5-byte frames: `AA 03 [KEY_ID] [KEY_STATE] [checksum]`
- `KEY_ID` = row × 11 + col (043, rows 03, cols 010)
- `KEY_STATE` = 0x01 pressed, 0x02 released
- `checksum` = (0x03 + KEY_ID + KEY_STATE) & 0xFF
- The driver tracks Aa/Fn/Sym modifier state internally and translates KEY_ID to ASCII.
**Special keys:**
- `Aa` key: tap = one-shot uppercase, double-tap = caps lock
- `Sym` key: toggles symbol mode (Aa ineffective while active)
- `Fn+D/X/Z/C` = cursor up/down/left/right (0x1E/0x1F/0x1D/0x1C), `Fn+1` = Esc (0x1B)
**Mode behaviour for Fn combos:**
- **UART mode:** Fully supported. The firmware sends the raw KEY_ID in the UART frame; our driver translates it via `fnCombo()` to the private ASCII codes above.
- **I2C mode:** No output. The firmware's I2C slave queue only holds regular ASCII; Fn-combo presses route to the BLE HID path and are not queued for I2C reads.
- **BLE HID mode:** Standard USB HID keycodes (ESC=0x29, Up=0x52, Down=0x51, Left=0x50, Right=0x4F), mapped to `LV_KEY_*` by `BluetoothHidHost`.
---
## LCD Unit (Color LCD)
| Property | Value |
|---|---|
| Interface | I2C |
| I2C Address | 0x3E |
| MCU | ESP32-PICO (inside) |
| Display | ST7789V2, 135x240 IPS |
| Driver class | `UnitLcd` |
Internal ESP32 bridges I2C commands to the ST7789V2. Commands are
fire-and-forget (the ESP32 processes them asynchronously). For bulk pixel
writes, poll `bufferRemaining()` (register 0x09) to avoid overflow.
- Docs: https://docs.m5stack.com/en/unit/lcd
- M5GFX repo: https://github.com/m5stack/M5GFX
**Key commands:**
| Cmd | Bytes | Description |
|-----|-------|-------------|
| 0x22 | [brightness] | Set backlight (0=off, 255=max) |
| 0x36 | [rotation] | Set rotation (0=portrait, 1=landscape, 2=portrait flip, 3=landscape flip) |
| 0x6A | [x0][y0][x1][y1][hi][lo] | Fill rect with RGB565 inline |
| 0x62 | [x][y][hi][lo] | Draw pixel RGB565 |
| 0x42 | [pixels...] | Write raw RGB565 stream |
| 0x09 | (read 1 byte) | Buffer bytes remaining |
| 0x68 | [x0][y0][x1][y1] | Fill rect with stored colour |
Physical dimensions: 135 wide x 240 tall (portrait). Rotation 1/3 swaps to 240 wide x 135 tall.
Max I2C clock: 400 kHz.
---
## Scroll Unit (Encoder + LED)
| Property | Value |
|---|---|
| Interface | I2C |
| I2C Address | 0x40 |
| MCU | STM32 |
| Driver class | `UnitScroll` |
Single rotary encoder with push button and one RGB LED.
- Docs: https://docs.m5stack.com/en/unit/UNIT-Scroll
- Repo: https://github.com/m5stack/M5Unit-Scroll
**Register map:**
| Addr | R/W | Width | Description |
|------|-----|-------|-------------|
| 0x10 | R/W | int16_t | Absolute encoder value |
| 0x20 | R | uint8_t | Button state (0=pressed, 1=released - inverted!) |
| 0x30 | R/W | 4 bytes | LED: [index(0), R, G, B] |
| 0x40 | W | uint8_t | Write 1 to reset encoder to 0 |
| 0x50 | R | int16_t | Delta since last read (resets after read) |
| 0xFE | R | uint8_t | Firmware version |
| 0xFF | R/W | uint8_t | I2C address |
---
## Joystick 2 Unit
| Property | Value |
|---|---|
| Interface | I2C |
| I2C Address | 0x63 |
| MCU | STM32 |
| Driver class | `UnitJoystick2` |
Dual-axis thumbstick with push button and one RGB LED. 12-bit ADC resolution.
- Docs: https://docs.m5stack.com/en/unit/Unit-JoyStick2
- Repo: https://github.com/m5stack/M5Unit-Joystick2
**Register map (key registers):**
| Addr | R/W | Width | Description |
|------|-----|-------|-------------|
| 0x00 | R | uint8_t | X axis (8-bit, 0-255) |
| 0x01 | R | uint8_t | Y axis (8-bit, 0-255) |
| 0x02 | R | uint8_t | Button (0=pressed, 1=released) |
| 0x10 | R | int16_t | X axis 12-bit signed |
| 0x12 | R | int16_t | Y axis 12-bit signed |
| 0x20 | R/W | 3 bytes | LED [R, G, B] |
| 0xFE | R | uint8_t | Firmware version |
---
## Dual Button Unit
| Property | Value |
|---|---|
| Interface | GPIO |
| Signals | 2x digital input (active LOW) |
| Colors | Red (Button A), Blue (Button B) |
| Driver class | `UnitDualButton` |
Simple two-button unit. Both buttons are active-LOW (pressed = 0V). The
default GPIO pins depend on the Grove port used:
- Docs: https://docs.m5stack.com/en/unit/dual_button
- Repo: https://github.com/m5stack/M5Stack/tree/master/examples/Unit/DUAL_BUTTON
**Pin assignment (Port B):** GPIO varies by device. The driver takes pin
numbers at `begin()` - pass the actual GPIO numbers for the target hardware.
---
## 8Encoder Unit
| Property | Value |
|---|---|
| Interface | I2C |
| I2C Address | 0x41 |
| MCU | STM32 |
| Driver class | `Unit8Encoder` |
8 rotary encoders with push buttons, 8 individual RGB LEDs, one toggle switch,
and one RGB LED for the switch. Total 9 addressable LEDs.
- Docs: https://docs.m5stack.com/en/unit/8Encoder
- Repo: https://github.com/m5stack/M5Unit-8Encoder
**Register map (V1 firmware, 2022-11-08):**
| Addr | R/W | Width | Description |
|------|-----|-------|-------------|
| 0x00 + i*4 | R/W | int32_t | Absolute counter for encoder i (0-3) |
| 0x10 + i*4 | R/W | int32_t | Absolute counter for encoder i (4-7) |
| 0x20 + i*4 | R | int32_t | Delta for encoder i (0-3), resets after read |
| 0x30 + i*4 | R | int32_t | Delta for encoder i (4-7), resets after read |
| 0x40 + i | W | uint8_t | Write 1 to reset counter i (i = 0-7) |
| 0x50 + i | R | uint8_t | Button state for encoder i (0=released, 1=pressed) |
| 0x60 | R | uint8_t | Toggle switch state (0=off, 1=on) |
| 0x70 + i*3 | R/W | 3 bytes | LED [R, G, B] for encoder i (i = 0-7) |
| 0x88 | R/W | 3 bytes | LED [R, G, B] for toggle switch (LED8) |
| 0xF0 | R | uint8_t | Firmware version |
| 0xFF | R/W | uint8_t | I2C address |
Hardware counts 4 pulses per detent; the driver divides by 4 to give
detent-level deltas. Counter range: -2,147,483,648 to +2,147,483,647.
**LED indices in `Unit8Encoder`:** 0-7 = encoder LEDs, 8 = switch LED.
---
## ByteButton Unit
| Property | Value |
|---|---|
| Interface | I2C |
| I2C Address | 0x47 |
| MCU | STM32 |
| Driver class | `UnitByteButton` |
8 momentary buttons with individual RGB LEDs, each with independently
controllable brightness.
- Docs: https://docs.m5stack.com/en/unit/Unit%20ByteButton
- Repo: https://github.com/m5stack/M5Unit-ByteButton
**Register map:**
| Addr | R/W | Width | Description |
|------|-----|-------|-------------|
| 0x00 | R | uint8_t | All 8 buttons as bitmask (bit i = button i, 1=pressed) |
| 0x10 + i | R/W | uint8_t | LED brightness for button i (0-255) |
| 0x19 | R/W | uint8_t | LED show mode (0=user-defined, 1=system default) |
| 0x20 + i*4 | R/W | uint32_t | LED RGB colour (LE: bytes = [B, G, R, 0x00]) |
| 0x50 + i | R/W | uint8_t | LED colour RGB233 compressed |
| 0x60 + i | R | uint8_t | Individual button state (0=released, non-zero=pressed) |
| 0x70 + i*4 | R/W | uint32_t | System "off" colour for button i |
| 0x90 + i*4 | R/W | uint32_t | System "on" colour for button i |
| 0xFE | R | uint8_t | Firmware version |
| 0xFF | R/W | uint8_t | I2C address |
**LED colour format:** write `uint32_t 0x00RRGGBB` as little-endian - wire
sees [BB, GG, RR, 0x00]. Set show mode to 0 (user-defined) before
`setLed`/`flushLeds` to take effect.
---
## MIDI Unit / Synth Unit
| Property | Value |
|---|---|
| Interface | UART |
| Baud rate | 31250 bps, 8N1 |
| IC | SAM2695 |
| Polyphony | 64 voices (38 with effects) |
| Channels | 16 MIDI channels |
| Driver class | `UnitMidi` |
Both the MIDI Unit and Synth Unit use the SAM2695 connected via UART. Pass a
pre-opened `Uart` instance to `begin()`. The driver issues a system reset
(0xFF) on init.
- Synth Unit docs: https://docs.m5stack.com/en/unit/Unit-Synth
- MIDI Unit docs: https://docs.m5stack.com/en/unit/Unit-MIDI
- Repo: https://github.com/m5stack/M5Unit-Synth
**MIDI 1.0 message formats:**
| Message | Bytes | Description |
|---------|-------|-------------|
| 0x80\|ch | note, 0 | Note Off |
| 0x90\|ch | note, velocity | Note On (velocity 0 = Note Off) |
| 0xB0\|ch | controller, value | Control Change |
| 0xC0\|ch | program | Program Change |
| 0xE0\|ch | LSB, MSB | Pitch Bend (-8192 to +8191, centre = 0x2000) |
| 0xFF | - | System Reset |
| 0xB0\|ch | 123, 0 | All Notes Off (CC 123) |
**General MIDI program numbers (partial):**
| Range | Category |
|-------|----------|
| 0-7 | Piano |
| 8-15 | Chromatic Percussion |
| 24-31 | Guitar |
| 32-39 | Bass |
| 40-47 | Strings |
| 56-63 | Brass |
| 73-79 | Flute/Recorder/Pipe |
| 80-87 | Synth Lead |
| 88-95 | Synth Pad |
---
## RFID 2 Unit
| Property | Value |
|---|---|
| Interface | I2C |
| I2C Address | 0x28 |
| IC | MFRC522 (I2C mode) |
| Standards | ISO/IEC 14443 A/B, MIFARE |
| Frequency | 13.56 MHz |
| Driver class | `UnitRfid2` |
MFRC522 contactless card reader in I2C mode. Supports UID reading for MIFARE
Classic, MIFARE Ultralight, and ISO 14443-A cards.
- Docs: https://docs.m5stack.com/en/unit/rfid2
- MFRC522 I2C lib: https://github.com/kkloesener/MFRC522_I2C
**Notes:**
- The MFRC522 supports SPI, I2C, and UART; this unit hardwires I2C mode.
- UID length: 4 bytes (single-size) or 7 bytes (double-size) depending on card.
- Always check `readUID()` return length; 0 = no card present or read error.
---
## Driver Common Notes
All drivers use `UnitCommon.h` helpers:
- `unitProbe(dev, addr)` - checks for device ACK at address
- `unitReadReg(dev, addr, reg, buf, len)` - STM32-style: write reg byte, then read
- `unitWriteReg(dev, addr, reg, buf, len)` - write reg byte followed by data
- `UNIT_I2C_TIMEOUT_MS` = 20 ms default timeout
The STM32-based units (8Encoder, ByteButton, Scroll, Joystick2, CardKB2)
require a 2 ms delay between write (reg select) and read in some cases. This
is handled internally by the I2C controller driver.
When using the PaHub, always call `selectIfNeeded()` **before** `isPresent()`
to ensure the correct channel is active for the ACK check.