Add Waveshare ESP32-S3-Touch-AMOLED-2.06 board port + Windows daemon fixes

- New board firmware/src/boards/waveshare_amoled_206/ (CO5300 410x502,
  FT3168 touch, AXP2101 PMU, QMI8658 IMU). PlatformIO env
  waveshare_amoled_206. CO5300_COL_OFFSET=22, fixed orientation,
  PWR button on GPIO10, FocalTech inline touch reader.
- Windows daemon: fix advertised device name (Claude Controller ->
  Clawdmeter) to match firmware ble.cpp, and add CLAWDMETER_ADDRESS env
  fallback so it connects by address when the bonded HID device is not
  advertising (Windows keeps it HID-connected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 12:46:46 +03:00
co-authored by Claude Opus 4.8
parent c49fb52981
commit c610e3af48
10 changed files with 457 additions and 3 deletions
+57
View File
@@ -111,6 +111,63 @@ lib_deps =
h2zero/NimBLE-Arduino@^2.1.1
[env:waveshare_amoled_206]
; Waveshare ESP32-S3-Touch-AMOLED-2.06 watch kit. 410x502 CO5300 + FT3168
; touch + AXP2101 PMU + QMI8658 IMU. 32MB flash / 8MB OPI PSRAM. No IO
; expander (reset lines are direct GPIOs), fixed orientation.
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
board = esp32-s3-devkitc-1
framework = arduino
board_build.arduino.memory_type = qio_opi
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
board_build.partitions = default_16MB.csv
upload_speed = 921600
monitor_speed = 115200
build_src_filter =
+<*>
-<boards/>
+<boards/waveshare_amoled_206/>
build_flags =
-DBOARD_AMOLED_206
-DARDUINO_USB_CDC_ON_BOOT=1
-DBOARD_HAS_PSRAM
-DXPOWERS_CHIP_AXP2101
; NimBLE config — peripheral, 2 simultaneous connections so the OS can
; hold the HID link while the daemon holds the data-service connection.
-DCONFIG_BT_NIMBLE_ROLE_BROADCASTER=1
-DCONFIG_BT_NIMBLE_ROLE_PERIPHERAL=1
-DCONFIG_BT_NIMBLE_ROLE_CENTRAL=0
-DCONFIG_BT_NIMBLE_ROLE_OBSERVER=0
-DCONFIG_BT_NIMBLE_MAX_CONNECTIONS=2
; LVGL config via build flags
-DLV_CONF_SKIP
-DLV_COLOR_DEPTH=16
-DLV_USE_LOG=0
-DLV_USE_BAR=1
-DLV_USE_LABEL=1
-DLV_USE_ARC=1
-DLV_USE_BTN=1
-DLV_USE_LINE=1
-DLV_USE_OBJ=1
-DLV_USE_IMG=1
-DLV_USE_IMAGE=1
-DLV_USE_ANIMIMG=0
-DLV_TICK_CUSTOM=1
-DLV_USE_SNAPSHOT=1
lib_deps =
; 1.6.4+ ships Arduino_CO5300 in mainline
moononournation/GFX Library for Arduino@^1.6.4
lewisxhe/SensorLib@^0.2.6
lewisxhe/XPowersLib@^0.2.7
lvgl/lvgl@^9.2.0
bblanchon/ArduinoJson@^7.0.0
h2zero/NimBLE-Arduino@^2.1.1
[env:waveshare_amoled_216_c6]
; C6 sibling of the AMOLED-2.16. Same panel/touch/PMU/IMU, no PSRAM,
; single-core RISC-V, BLE 5.3 only (no classic BT). If the build fails
@@ -0,0 +1,55 @@
#pragma once
// Waveshare ESP32-S3-Touch-AMOLED-2.06 — rectangular AMOLED smartwatch kit.
// 410x502 CO5300 + FT3168 touch + AXP2101 PMU + QMI8658 IMU + PCF85063 RTC.
// No IO expander: display/touch reset lines are direct ESP32 GPIOs.
// IMU is present (initialized for I2C bus health) but rotation is disabled —
// the panel is non-square and the kit mounts in a fixed orientation.
#define BOARD_NAME "Waveshare AMOLED 2.06"
// ---- Display geometry (portrait, non-square) ----
#define LCD_WIDTH 410
#define LCD_HEIGHT 502
// ---- QSPI display pins (CO5300) ----
// Same data lines as the 2.16 board, but SCLK and RESET differ.
#define LCD_CS 12
#define LCD_SCLK 11 // 2.06: GPIO11 (2.16 board used GPIO38)
#define LCD_SDIO0 4
#define LCD_SDIO1 5
#define LCD_SDIO2 6
#define LCD_SDIO3 7
#define LCD_RESET 8 // direct GPIO (2.16 used GPIO2)
#define LCD_TE 13 // tearing-effect line (unused by GFX driver)
// CO5300 GRAM is wider than the 410px active area, so the visible image needs
// a horizontal start offset. 22 verified for this panel (matches the value used
// by other 2.06 firmwares; eliminates the uninitialized green band on the right).
#define CO5300_COL_OFFSET 22
// ---- I2C bus (touch + PMU + IMU + RTC all share one bus) ----
#define IIC_SDA 15
#define IIC_SCL 14
// ---- Touch (FT3168 via minimal inline FocalTech-style I2C reader) ----
#define TP_INT 38
#define TP_RST 9 // direct GPIO (active LOW)
#define FT3168_ADDR 0x38
// ---- PMU ----
#define AXP2101_ADDR 0x34
// ---- Buttons ----
#define BTN_BACK_GPIO 0 // BOOT — primary, Space (PTT)
#define BTN_PWR_GPIO 10 // PWR side button — cycle screens / animations
// PWR polarity: assumed active LOW with internal pull-up (button to GND, like
// BOOT). If the screen-cycle button never registers, flip BTN_PWR_ACTIVE_LOW.
#define BTN_PWR_ACTIVE_LOW 1
// ---- Capability flags ----
#define BOARD_HAS_SECONDARY_BUTTON 0 // only BOOT via input_hal; PWR via power_hal
#define BOARD_HAS_ROTATION 0 // non-square panel, fixed orientation
#define BOARD_HAS_IMU 1 // present + initialized, rotation off
#define BOARD_HAS_BATTERY 1
#define BOARD_HAS_IO_EXPANDER 0
@@ -0,0 +1,24 @@
#include "board.h"
#include <Arduino.h>
#include <Wire.h>
// AMOLED-2.06 has no IO expander — display and touch reset lines are direct
// ESP32 GPIOs. We bring up the shared I2C bus and release both reset lines
// before any display/touch HAL call runs.
//
// Display power: on this kit the AMOLED rail is supplied by the AXP2101 PMU
// (the "DSI_PWR_EN" net on the schematic sits on the PMIC, not an ESP32 pin),
// matching the AMOLED-2.16 sibling which lights up with no explicit rail
// poke after pmu.begin(). If the panel stays dark on first flash, enable the
// AXP rails in power.cpp (see the commented block there).
extern "C" void board_init(void) {
Wire.begin(IIC_SDA, IIC_SCL);
// Release the touch controller from reset (active LOW). The display's own
// reset is driven by the Arduino_GFX CO5300 driver via LCD_RESET.
pinMode(TP_RST, OUTPUT);
digitalWrite(TP_RST, LOW);
delay(5);
digitalWrite(TP_RST, HIGH);
delay(50);
}
@@ -0,0 +1,14 @@
#include "../../hal/board_caps.h"
#include "board.h"
static const BoardCaps caps = {
.name = BOARD_NAME,
.width = LCD_WIDTH,
.height = LCD_HEIGHT,
.button_count = 1,
.has_rotation = false,
.has_battery = true,
.has_imu = true,
};
const BoardCaps& board_caps(void) { return caps; }
@@ -0,0 +1,55 @@
#include "../../hal/display_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Arduino_GFX_Library.h>
// AMOLED-2.06 is fixed at 0° (non-square 410x502 panel — the 2.16 board's CPU
// strip-rotation assumes a square panel and can't be reused here). No rot_buf.
//
// Display driver is the CO5300 (same controller family as the 2.16 board).
// Reset is a direct GPIO (LCD_RESET), so the GFX driver owns it. The CO5300's
// active area starts partway into the controller GRAM, so it gets a column
// offset to center the image (CO5300_COL_OFFSET — verify on hardware).
static Arduino_DataBus* bus = nullptr;
static Arduino_CO5300* gfx = nullptr;
void display_hal_init(void) {
bus = new Arduino_ESP32QSPI(
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
// CO5300: (bus, rst, rotation, w, h, col_off1, row_off1, col_off2, row_off2)
gfx = new Arduino_CO5300(
bus, LCD_RESET, 0 /* no rotation */,
LCD_WIDTH, LCD_HEIGHT, CO5300_COL_OFFSET, 0, 0, 0);
}
void display_hal_begin(void) {
gfx->begin();
gfx->fillScreen(0x0000);
gfx->setBrightness(200);
}
void display_hal_set_brightness(uint8_t level) {
if (gfx) gfx->setBrightness(level);
}
void display_hal_fill_screen(uint16_t color) {
if (gfx) gfx->fillScreen(color);
}
void display_hal_draw_bitmap(int32_t x, int32_t y, int32_t w, int32_t h,
const uint16_t* pixels) {
if (gfx) gfx->draw16bitRGBBitmap(x, y, (uint16_t*)pixels, w, h);
}
void display_hal_tick(void) {
// No rotation handling needed on this board.
}
// CO5300 requires even-aligned flush regions.
void display_hal_round_area(int32_t* x1, int32_t* y1, int32_t* x2, int32_t* y2) {
*x1 = *x1 & ~1;
*y1 = *y1 & ~1;
*x2 = *x2 | 1;
*y2 = *y2 | 1;
}
@@ -0,0 +1,25 @@
#include "../../hal/imu_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Wire.h>
#include <SensorQMI8658.hpp>
// AMOLED-2.06 ships with QMI8658 populated, but the panel is non-square and
// mounts in a fixed orientation, so rotation is disabled. We initialize the
// device anyway to keep the shared I2C bus healthy, but always report 0.
static SensorQMI8658 imu;
void imu_hal_init(void) {
if (!imu.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {
Serial.println("QMI8658 init failed");
return;
}
Serial.println("QMI8658 init OK (rotation disabled on this board)");
}
void imu_hal_tick(void) {
// No-op — rotation is disabled.
}
uint8_t imu_hal_rotation_quadrant(void) { return 0; }
@@ -0,0 +1,20 @@
#include "../../hal/input_hal.h"
#include "board.h"
#include <Arduino.h>
// AMOLED-2.06 exposes the BOOT button as the primary HID input. The PWR side
// button is handled in power_hal (BTN_PWR_GPIO). No secondary button.
void input_hal_init(void) {
pinMode(BTN_BACK_GPIO, INPUT_PULLUP);
}
bool input_hal_is_held(InputButton btn) {
switch (btn) {
case INPUT_BTN_PRIMARY:
return digitalRead(BTN_BACK_GPIO) == LOW;
case INPUT_BTN_SECONDARY:
return false; // not present on this board
}
return false;
}
@@ -0,0 +1,119 @@
#include "../../hal/power_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Wire.h>
#include <XPowersLib.h>
// AXP2101 provides battery monitoring. The PWR side button on this kit is a
// direct ESP32 GPIO (BTN_PWR_GPIO), not the AXP PKEY, so we poll it and
// synthesize the same three edges the AXP IRQ path produces on the 2.16 board:
// short — fired on release if the hold was shorter than PWR_LONG_MS
// long — fired once when a hold crosses PWR_LONG_MS
// release — fired on every release edge
// This keeps the hold-to-pair gesture logic in main.cpp board-agnostic.
#define BATTERY_POLL_MS 2000
#define CHARGING_POLL_MS 500
#define PWR_POLL_MS 50
#define PWR_LONG_MS 1500 // hold threshold, mirrors the AXP LONG IRQ
#if BTN_PWR_ACTIVE_LOW
#define PWR_IS_DOWN(level) ((level) == LOW)
#else
#define PWR_IS_DOWN(level) ((level) == HIGH)
#endif
static XPowersPMU pmu;
static int cached_pct = -1;
static bool cached_charging = false;
static bool cached_vbus = false;
static bool pwr_pressed_flag = false;
static bool pwr_long_flag = false;
static bool pwr_released_flag = false;
static bool last_pwr_down = false; // edge detector for BTN_PWR_GPIO
static uint32_t pwr_press_started_ms = 0;
static bool pwr_long_fired = false;
static uint32_t last_battery_ms = 0;
static uint32_t last_charging_ms = 0;
static uint32_t last_pwr_ms = 0;
void power_hal_init(void) {
if (!pmu.begin(Wire, AXP2101_ADDR, IIC_SDA, IIC_SCL)) {
Serial.println("AXP2101 init failed");
return;
}
Serial.println("AXP2101 init OK");
pmu.enableBattDetection();
pmu.enableBattVoltageMeasure();
// ---- OPTIONAL: explicit AMOLED rail enable ----------------------------
// The 2.16 sibling lights up without this, so it's left off by default.
// If the panel stays DARK on first flash, the display rail isn't powered.
// Uncomment and set the channels/voltages to match the Waveshare 2.06
// schematic (typical AXP2101 AMOLED kits use the ALDO/BLDO rails):
//
// pmu.setALDO1Voltage(1800); pmu.enableALDO1(); // panel logic 1.8V
// pmu.setBLDO1Voltage(3300); pmu.enableBLDO1(); // panel/touch 3.3V
// pmu.setBLDO2Voltage(3300); pmu.enableBLDO2();
// delay(50);
// -----------------------------------------------------------------------
// PWR button is a direct GPIO on this board.
pinMode(BTN_PWR_GPIO, BTN_PWR_ACTIVE_LOW ? INPUT_PULLUP : INPUT_PULLDOWN);
cached_charging = pmu.isCharging();
cached_vbus = pmu.isVbusIn();
cached_pct = pmu.getBatteryPercent();
}
void power_hal_tick(void) {
uint32_t now = millis();
if (now - last_charging_ms >= CHARGING_POLL_MS) {
last_charging_ms = now;
cached_charging = pmu.isCharging();
cached_vbus = pmu.isVbusIn();
}
if (now - last_battery_ms >= BATTERY_POLL_MS) {
last_battery_ms = now;
cached_pct = pmu.getBatteryPercent();
}
if (now - last_pwr_ms >= PWR_POLL_MS) {
last_pwr_ms = now;
bool pwr_down = PWR_IS_DOWN(digitalRead(BTN_PWR_GPIO));
if (pwr_down && !last_pwr_down) { // press begins
pwr_press_started_ms = now;
pwr_long_fired = false;
} else if (pwr_down && last_pwr_down) { // held
if (!pwr_long_fired && (now - pwr_press_started_ms >= PWR_LONG_MS)) {
pwr_long_flag = true;
pwr_long_fired = true;
}
} else if (!pwr_down && last_pwr_down) { // release
pwr_released_flag = true;
if (!pwr_long_fired) pwr_pressed_flag = true; // short press
}
last_pwr_down = pwr_down;
}
}
int power_hal_battery_pct(void) { return cached_pct; }
bool power_hal_is_charging(void) { return cached_charging; }
bool power_hal_is_vbus_in(void) { return cached_vbus; }
bool power_hal_pwr_pressed(void) {
if (pwr_pressed_flag) { pwr_pressed_flag = false; return true; }
return false;
}
bool power_hal_pwr_long_pressed(void) {
if (pwr_long_flag) { pwr_long_flag = false; return true; }
return false;
}
bool power_hal_pwr_released(void) {
if (pwr_released_flag) { pwr_released_flag = false; return true; }
return false;
}
@@ -0,0 +1,74 @@
#include "../../hal/touch_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Wire.h>
// Minimal capacitive-touch reader for the FT3168 (FocalTech). Same data layout
// as the FT3168 on the AMOLED-1.8 board, so we reuse the inline reader instead
// of vendoring the GPLv3 Arduino_DriveBus library.
// reg 0x02: low nibble = active finger count
// reg 0x03 / 0x04: X1 high (low nibble) + X1 low
// reg 0x05 / 0x06: Y1 high (low nibble) + Y1 low
//
// Axis orientation: started with no swap/mirror (matches the portrait FT3168
// on the 1.8 board). If touch coordinates come out flipped/swapped on this
// panel, mirror/swap them in touch_read_into_shared_state() below.
static volatile bool touch_data_ready = false;
static volatile bool touch_pressed = false;
static volatile uint16_t touch_x = 0;
static volatile uint16_t touch_y = 0;
static void IRAM_ATTR touch_isr(void) {
touch_data_ready = true;
}
static void touch_read_into_shared_state(void) {
Wire.beginTransmission(FT3168_ADDR);
Wire.write(0x02);
if (Wire.endTransmission(false) != 0) { touch_pressed = false; return; }
if (Wire.requestFrom((uint8_t)FT3168_ADDR, (uint8_t)5) != 5) { touch_pressed = false; return; }
uint8_t fingers = Wire.read() & 0x0F;
uint8_t xH = Wire.read();
uint8_t xL = Wire.read();
uint8_t yH = Wire.read();
uint8_t yL = Wire.read();
if (fingers == 0 || fingers > 5) {
touch_pressed = false;
return;
}
touch_x = ((uint16_t)(xH & 0x0F) << 8) | xL;
touch_y = ((uint16_t)(yH & 0x0F) << 8) | yL;
touch_pressed = true;
}
void touch_hal_init(void) {
// FT3168 power-mode register 0xA5 = 0x00: active scanning.
Wire.beginTransmission(FT3168_ADDR);
Wire.write(0xA5);
Wire.write(0x00);
Wire.endTransmission();
// Verify the controller answers (FT3168 chip-id is reg 0xA0).
Wire.beginTransmission(FT3168_ADDR);
Wire.write(0xA0);
if (Wire.endTransmission(false) == 0 && Wire.requestFrom((uint8_t)FT3168_ADDR, (uint8_t)1) == 1) {
Serial.printf("Touch FT3168 ID=0x%02X (addr 0x%02X)\n", Wire.read(), FT3168_ADDR);
} else {
Serial.printf("Touch FT3168 ID read failed (addr 0x%02X)\n", FT3168_ADDR);
}
pinMode(TP_INT, INPUT_PULLUP);
attachInterrupt(TP_INT, touch_isr, FALLING);
Serial.println("Touch attached on INT pin");
}
void touch_hal_read(uint16_t* x, uint16_t* y, bool* pressed) {
if (touch_data_ready) {
touch_data_ready = false;
touch_read_into_shared_state();
}
*x = touch_x;
*y = touch_y;
*pressed = touch_pressed;
}