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>
This commit is contained in:
@ -1,20 +1,30 @@
|
||||
#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_;
|
||||
@ -22,6 +32,9 @@ private:
|
||||
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");
|
||||
@ -117,6 +130,185 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@ -124,23 +316,31 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeSt7789Display();
|
||||
InitializeButtons();
|
||||
InitializeKeyboard();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
static Es8311AudioCodec 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, Cardputer Adv has no MCLK pin
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user