Files
xiaozhi-esp32/main/boards/m5stack-cardputer-adv/m5stack_cardputer_adv.cc
tkpdx01 6074fdeb71 feat(cardputer-adv): add TCA8418 keyboard and WiFi config UI (#1929)
Add full keyboard support and keyboard-based WiFi configuration for
M5Stack Cardputer Adv:

- TCA8418 I2C keyboard driver with 56-key matrix, interrupt-driven
  key events, and debounce handling
- Keyboard WiFi config UI: scan/select/input SSID and password
  directly on the device without needing a phone
- Volume control (up/down arrows) and brightness control (left/right)
  via keyboard with fine-step adjustment near bounds
- Enter key to toggle chat state
- Display offset and backlight fixes for ST7789V2
- README with flash parameters and hardware specs

Co-authored-by: bot <bot@localhost>
2026-04-14 17:02:00 +08:00

359 lines
13 KiB
C++

#include "wifi_board.h"
#include "wifi_config_ui.h"
#include "codecs/es8311_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "i2c_device.h"
#include "tca8418_keyboard.h"
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <driver/i2s_common.h>
#include <driver/spi_common.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_vendor.h>
#include <wifi_manager.h>
#include <ssid_manager.h>
#include <algorithm>
#include <memory>
#define TAG "CardputerAdv"
// Backlight uses percentage scale (0-100). Keep a minimum of 30% to avoid a too-dim screen.
#define MIN_BRIGHTNESS 30
class M5StackCardputerAdvBoard : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
LcdDisplay* display_;
Button boot_button_;
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
esp_lcd_panel_handle_t panel_ = nullptr;
Tca8418Keyboard* keyboard_ = nullptr;
std::unique_ptr<WifiConfigUI> wifi_config_ui_;
bool wifi_config_mode_ = false;
void InitializeI2c() {
ESP_LOGI(TAG, "Initialize I2C bus");
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
}
void I2cDetect() {
uint8_t address;
ESP_LOGI(TAG, "I2C device scan:");
printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n");
for (int i = 0; i < 128; i += 16) {
printf("%02x: ", i);
for (int j = 0; j < 16; j++) {
fflush(stdout);
address = i + j;
esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200));
if (ret == ESP_OK) {
printf("%02x ", address);
} else if (ret == ESP_ERR_TIMEOUT) {
printf("UU ");
} else {
printf("-- ");
}
}
printf("\r\n");
}
}
void InitializeSpi() {
ESP_LOGI(TAG, "Initialize SPI bus");
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = DISPLAY_SPI_SCLK_PIN;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeSt7789Display() {
ESP_LOGI(TAG, "Initialize ST7789V2 display");
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
io_config.dc_gpio_num = DISPLAY_DC_PIN;
io_config.spi_mode = 0;
io_config.pclk_hz = 40 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
io_config.flags.sio_mode = 1; // 3-wire SPI mode (M5GFX uses spi_3wire = true)
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io_));
ESP_LOGI(TAG, "Install ST7789 panel driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = DISPLAY_RST_PIN;
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io_, &panel_config, &panel_));
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_, true));
ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_, DISPLAY_SWAP_XY));
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y));
display_ = new SpiLcdDisplay(panel_io_, panel_,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
}
void InitializeKeyboard() {
ESP_LOGI(TAG, "Initialize TCA8418 keyboard");
keyboard_ = new Tca8418Keyboard(i2c_bus_, KEYBOARD_TCA8418_ADDR, KEYBOARD_INT_PIN);
keyboard_->Initialize();
// Set legacy callback for volume/brightness control
keyboard_->SetKeyCallback([this](LegacyKeyCode key) {
HandleLegacyKeyPress(key);
});
// Set full key event callback for WiFi config and text input
keyboard_->SetKeyEventCallback([this](const KeyEvent& event) {
HandleKeyEvent(event);
});
}
void HandleKeyEvent(const KeyEvent& event) {
// Handle WiFi config mode
if (wifi_config_mode_ && wifi_config_ui_) {
auto result = wifi_config_ui_->HandleKeyEvent(event);
if (result == WifiConfigResult::Connected) {
ESP_LOGI(TAG, "WiFi connected via keyboard config");
ExitWifiConfigMode();
} else if (result == WifiConfigResult::Cancelled) {
ESP_LOGI(TAG, "WiFi config cancelled");
ExitWifiConfigMode();
}
return;
}
// Handle W and S keys during WiFi configuring state (scanning screen)
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateWifiConfiguring && event.pressed) {
if (event.key_code == KC_W) {
ESP_LOGI(TAG, "W key pressed - entering keyboard WiFi config");
StartKeyboardWifiConfig();
} else if (event.key_code == KC_S) {
ESP_LOGI(TAG, "S key pressed - showing saved WiFi list");
StartKeyboardWifiConfigSaved();
}
}
}
void HandleLegacyKeyPress(LegacyKeyCode key) {
// Skip if in WiFi config mode
if (wifi_config_mode_) {
return;
}
auto& app = Application::GetInstance();
auto* codec = GetAudioCodec();
auto* backlight = GetBacklight();
switch (key) {
case KEY_UP: {
// Volume up
int current_vol = codec->output_volume();
int step = (current_vol <= 20 || current_vol >= 80) ? 1 : 10;
int new_vol = std::min(100, current_vol + step);
codec->SetOutputVolume(new_vol);
char msg[32];
snprintf(msg, sizeof(msg), "Volume: %d%%", new_vol);
display_->ShowNotification(msg, 1500);
ESP_LOGI(TAG, "Volume up: %d%%", new_vol);
break;
}
case KEY_DOWN: {
// Volume down
int current_vol = codec->output_volume();
int step = (current_vol <= 20 || current_vol >= 80) ? 1 : 10;
int new_vol = std::max(0, current_vol - step);
codec->SetOutputVolume(new_vol);
char msg[32];
snprintf(msg, sizeof(msg), "Volume: %d%%", new_vol);
display_->ShowNotification(msg, 1500);
ESP_LOGI(TAG, "Volume down: %d%%", new_vol);
break;
}
case KEY_RIGHT: {
// Brightness up
uint8_t current_br = backlight->brightness();
int step = (current_br <= (MIN_BRIGHTNESS + 20) || current_br >= 80) ? 1 : 10;
int new_br = std::min(100, (int)current_br + step);
backlight->SetBrightness(new_br, true);
char msg[32];
snprintf(msg, sizeof(msg), "Brightness: %d%%", new_br);
display_->ShowNotification(msg, 1500);
ESP_LOGI(TAG, "Brightness up: %d%%", new_br);
break;
}
case KEY_LEFT: {
// Brightness down (minimum 30%)
uint8_t current_br = backlight->brightness();
int step = (current_br <= (MIN_BRIGHTNESS + 20) || current_br >= 80) ? 1 : 10;
int new_br = std::max((int)MIN_BRIGHTNESS, (int)current_br - step);
backlight->SetBrightness(new_br, true);
char msg[32];
snprintf(msg, sizeof(msg), "Brightness: %d%%", new_br);
display_->ShowNotification(msg, 1500);
ESP_LOGI(TAG, "Brightness down: %d%%", new_br);
break;
}
case KEY_ENTER: {
// Match boot button behavior (start/stop chat depending on current state).
if (app.GetDeviceState() != kDeviceStateStarting) {
app.ToggleChatState();
ESP_LOGI(TAG, "Enter key: Toggle chat state");
}
break;
}
default:
break;
}
}
void StartKeyboardWifiConfig() {
ESP_LOGI(TAG, "Starting keyboard WiFi config UI");
wifi_config_mode_ = true;
wifi_config_ui_ = std::make_unique<WifiConfigUI>(display_);
wifi_config_ui_->SetConnectCallback([this](const std::string& ssid, const std::string& password) {
AttemptWifiConnection(ssid, password);
});
wifi_config_ui_->Start();
}
void StartKeyboardWifiConfigSaved() {
ESP_LOGI(TAG, "Starting keyboard WiFi config UI (saved list)");
wifi_config_mode_ = true;
wifi_config_ui_ = std::make_unique<WifiConfigUI>(display_);
wifi_config_ui_->SetConnectCallback([this](const std::string& ssid, const std::string& password) {
AttemptWifiConnection(ssid, password);
});
wifi_config_ui_->StartWithSavedList();
}
void AttemptWifiConnection(const std::string& ssid, const std::string& password) {
ESP_LOGI(TAG, "Attempting WiFi connection to: %s", ssid.c_str());
// Add to SSID manager (will be saved and used for connection)
auto& ssid_manager = SsidManager::GetInstance();
ssid_manager.AddSsid(ssid, password);
// Stop config AP mode and trigger reconnection with new credentials
auto& wifi_manager = WifiManager::GetInstance();
if (wifi_manager.IsConfigMode()) {
wifi_manager.StopConfigAp();
}
// Start station mode to connect
wifi_manager.StartStation();
// Wait for connection result (with timeout)
bool connected = false;
for (int i = 0; i < 100; i++) { // 10 second timeout
vTaskDelay(pdMS_TO_TICKS(100));
if (wifi_manager.IsConnected()) {
connected = true;
break;
}
}
if (wifi_config_ui_) {
wifi_config_ui_->OnConnectResult(connected);
}
}
void ExitWifiConfigMode() {
ESP_LOGI(TAG, "Exiting keyboard WiFi config mode");
wifi_config_mode_ = false;
wifi_config_ui_.reset();
// Restart normal WiFi connection flow
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateWifiConfiguring) {
// Try to connect with saved credentials
TryWifiConnect();
}
}
public:
M5StackCardputerAdvBoard() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c();
I2cDetect();
InitializeSpi();
InitializeSt7789Display();
InitializeButtons();
InitializeKeyboard();
GetBacklight()->RestoreBrightness();
}
virtual AudioCodec* GetAudioCodec() override {
// Cardputer Adv (no MCLK, internal clocking) needs I2S channels
// disabled after construction so esp_codec_dev_open can configure
// the ES8311 codec before channels start running.
static struct CardputerAdvEs8311 : public Es8311AudioCodec {
CardputerAdvEs8311(void* i2c, i2c_port_t port, int in_rate, int out_rate,
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws,
gpio_num_t dout, gpio_num_t din, gpio_num_t pa,
uint8_t addr, bool use_mclk)
: Es8311AudioCodec(i2c, port, in_rate, out_rate,
mclk, bclk, ws, dout, din, pa, addr, use_mclk) {
i2s_channel_disable(tx_handle_);
i2s_channel_disable(rx_handle_);
}
} audio_codec(
i2c_bus_, I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN,
AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR,
false); // use_mclk = false
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
// M5GFX uses 256Hz PWM frequency for Cardputer backlight
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT, 256);
return &backlight;
}
};
DECLARE_BOARD(M5StackCardputerAdvBoard);