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:
@@ -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 (4–16 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.
|
||||
Reference in New Issue
Block a user