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,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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user