feat: 开机确认窗口 + 快捷回复/九宫格导航 + 充电检测加速

This commit is contained in:
2026-03-29 16:29:38 +08:00
parent e236c951cc
commit eacbbc08dc
6 changed files with 89 additions and 28 deletions
+17 -2
View File
@@ -36,8 +36,8 @@
- `tca9535PowerEn(bool on)` — read-modify-write P1.2static inline - `tca9535PowerEn(bool on)` — read-modify-write P1.2static inline
- P1.3 = POWER_BOOT 输入,低电平有效(按键按下接地) - P1.3 = POWER_BOOT 输入,低电平有效(按键按下接地)
- `tca9535ReadPowerBoot()` — 读取 P1.3 状态,static inline - `tca9535ReadPowerBoot()` — 读取 P1.3 状态,static inline
- 开机流程:物理按键 → MOS 导通 → ESP32 得电 → init() 检测 P1.3 持续按住 2 秒 → POWER_EN 拉高维持供电 - 开机流程:物理按键 → MOS 导通 → ESP32 得电 → main.cpp 立即锁 POWER_EN → 等待 P1.3 持续按住 2 秒确认启动系统
- 未按满 2 秒松开 不拉高 POWER_EN → MOS 断开 → 自动断电 - 3 秒内未按满 2 秒 → POWER_EN 拉低 → MOS 断开 → 自动断电
- 关机流程:运行中 P1.3 持续按住 2 秒 → 清空屏幕 → POWER_EN 拉低 → 用户松手后 MOS 断开断电 - 关机流程:运行中 P1.3 持续按住 2 秒 → 清空屏幕 → POWER_EN 拉低 → 用户松手后 MOS 断开断电
- 电源状态机:`BOOT_PENDING``RUNNING``SHUTDOWN_PENDING` - 电源状态机:`BOOT_PENDING``RUNNING``SHUTDOWN_PENDING`
- P1 口配置:`0x8B`P1.2=输出, P1.3=输入, P1.4=输出, P1.5=输出, P1.6=输出, P1.7=输出) - P1 口配置:`0x8B`P1.2=输出, P1.3=输入, P1.4=输出, P1.5=输出, P1.6=输出, P1.7=输出)
@@ -63,6 +63,21 @@
### Changed ### Changed
#### 开机流程改为 early-lock + 确认窗口
- `main.cpp``Wire.begin()` 后立即 `tca9535PowerEn(true)` 锁住供电,防止初始化途中掉电
- 新增开机确认窗口:等待 P1.3 持续按住 2 秒确认开机,最多等 3 秒,超时则断电关机
- `TCA9535ButtonThread::init()` 不再负责开机确认,只设置状态机为 RUNNING
#### 快捷回复 ↔ 九宫格输入导航(esp32c3_moonshine_travelers
- **INACTIVE**UP/DOWN 进入快捷回复列表(恢复原始行为)
- **ACTIVE**(快捷回复列表):LEFT/RIGHT 进入九宫格文本输入(FREETEXT),不再映射为上下滚动
- **FREETEXT**(九宫格输入):LEFT/RIGHT 返回快捷回复列表,保留已输入文字
- `*` 号键映射为退格键(backspace)
- `isUpEvent()` / `isDownEvent()` 移除 ACTIVE 状态对 LEFT/RIGHT 的映射
#### 充电检测轮询间隔缩短
- TCA9535 CHARGE_DET 轮询间隔从 2000ms 缩短到 500ms,加快充电状态响应
#### 按键映射更新(key3/key7/key11/key15 = 方向键) #### 按键映射更新(key3/key7/key11/key15 = 方向键)
- 矩阵按键映射从 `key1=UP, key2=DOWN, key3=LEFT, key4=RIGHT` 改为 `key3=UP, key7=DOWN, key11=LEFT, key15=RIGHT` - 矩阵按键映射从 `key1=UP, key2=DOWN, key3=LEFT, key4=RIGHT` 改为 `key3=UP, key7=DOWN, key11=LEFT, key15=RIGHT`
- 方向键全部位于 COL3 列(key3=ROW0·COL3, key7=ROW1·COL3, key11=ROW2·COL3, key15=ROW3·COL3 - 方向键全部位于 COL3 列(key3=ROW0·COL3, key7=ROW1·COL3, key11=ROW2·COL3, key15=ROW3·COL3
Binary file not shown.
@@ -116,13 +116,11 @@ bool TCA9535ButtonThread::init()
tca9535GpsEn(true); tca9535GpsEn(true);
// =================================================================== // ===================================================================
// 第步:POWER_EN 已由 main.cpp 在 Wire.begin() 后立即拉高(早期锁定) // 第步:POWER_EN 已由 main.cpp 在 Wire.begin() 后立即拉高
// 此处只需确认状态机进入 RUNNING,不再需要等待 P1.3 按住 2 秒。 // 等待 P1.3 持续按住 2 秒确认开机(超时 3 秒则断电关机)
// 原因:系统从 Wire.begin() 到 tca9535ButtonThread::init() 之间需要 // 此处只需确认状态机进入 RUNNING。
// 数秒的初始化时间(LoRa/WiFi/BLE/GPS 等),用户早已松开按键,
// 无法在此处等待。开机供电维持已在 main.cpp 最早期完成。
// =================================================================== // ===================================================================
LOG_INFO("TCA9535: POWER_EN already latched in early boot, skipping boot-hold wait"); LOG_INFO("TCA9535: Boot already confirmed in early boot, state=RUNNING");
_powerState = TCA9535PowerState::RUNNING; _powerState = TCA9535PowerState::RUNNING;
// =================================================================== // ===================================================================
@@ -211,7 +209,7 @@ int32_t TCA9535ButtonThread::runOnce()
// 充电检测:轮询 P1.1 (CHARGE_DET),高电平=正在充电 // 充电检测:轮询 P1.1 (CHARGE_DET),高电平=正在充电
// =================================================================== // ===================================================================
#ifdef TCA9535_CHARGE_DET_PIN #ifdef TCA9535_CHARGE_DET_PIN
if (millis() - _chargeDetLastMs >= 2000) { if (millis() - _chargeDetLastMs >= 500) {
_chargeDetLastMs = millis(); _chargeDetLastMs = millis();
bool charging = tca9535ReadChargeDet(); bool charging = tca9535ReadChargeDet();
if (charging != tca9535IsCharging) { if (charging != tca9535IsCharging) {
@@ -16,8 +16,8 @@
* *
* 电源管理逻辑: * 电源管理逻辑:
* 开机:物理按键按下 → MOS 导通 → ESP32/TCA9535 得电 * 开机:物理按键按下 → MOS 导通 → ESP32/TCA9535 得电
* init() 读 P1.3,持续按住 2 秒 → tca9535PowerEn(true) 维持供电 * main.cpp Wire.begin() 后立即拉高 POWER_EN 锁住供电
* 未按满 2 秒松开 → 不拉高 POWER_EN → MOS 断开 → 断电 * 然后等待 P1.3 持续按住 2 秒确认开机(超时 3 秒则断电关机)
* 关机:运行中 P1.3 持续低电平 2 秒 → tca9535PowerEn(false) → 断电 * 关机:运行中 P1.3 持续低电平 2 秒 → tca9535PowerEn(false) → 断电
* *
* 寄存器布局: * 寄存器布局:
+35 -2
View File
@@ -586,10 +586,43 @@ void setup()
Wire.begin(I2C_SDA, I2C_SCL); Wire.begin(I2C_SDA, I2C_SCL);
#ifdef HAS_TCA9535_BUTTON #ifdef HAS_TCA9535_BUTTON
// TCA9535 POWER_EN 必须在 I²C 初始化完成后立即拉高,否则用户松开按键后 // TCA9535 POWER_EN 必须在 I²C 初始化完成后立即拉高,否则用户松开按键后
// MOS 断电,系统在 setup() 中途就会掉电。此处无条件锁住供电, // MOS 断电,系统在 setup() 中途就会掉电。
// 后续 tca9535ButtonThread::init() 只负责键盘和状态机初始化。
tca9535PowerEn(true); tca9535PowerEn(true);
LOG_INFO("TCA9535: POWER_EN latched HIGH (early boot)"); LOG_INFO("TCA9535: POWER_EN latched HIGH (early boot)");
// 开机确认窗口:检测 P1.3 是否持续按住 2 秒,防止意外通电卡死
// 最多等待 3 秒,3 秒内未连续按满 2 秒则断电关机
{
bool bootConfirmed = false;
uint32_t pressStart = 0;
uint32_t deadline = millis() + 3000;
LOG_INFO("TCA9535: Waiting for 2s button hold to confirm boot (timeout 3s)...");
while (millis() < deadline) {
bool pressed = tca9535ReadPowerBoot();
if (pressed && pressStart == 0) {
pressStart = millis();
} else if (pressed && pressStart != 0) {
if (millis() - pressStart >= 2000) {
bootConfirmed = true;
LOG_INFO("TCA9535: Boot confirmed (button held 2s)");
break;
}
} else if (!pressed && pressStart != 0) {
// 松手重置计时
pressStart = 0;
}
delay(50); // 50ms 轮询
}
if (!bootConfirmed) {
LOG_WARN("TCA9535: Boot not confirmed, shutting down");
tca9535PowerEn(false);
// 等待 MOS 断开
delay(500);
doDeepSleep(0, false, false);
}
}
#endif #endif
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
if (portduino_config.i2cdev != "") { if (portduino_config.i2cdev != "") {
@@ -368,6 +368,18 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE:
return 1; return 1;
// Canned message list: LEFT/RIGHT enters free text input
case CANNED_MESSAGE_RUN_STATE_ACTIVE:
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT) {
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
requestFocus();
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e);
return 1;
}
break;
// If sending, block all input except global/system (handled above) // If sending, block all input except global/system (handled above)
case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER:
return handleEmotePickerInput(event); return handleEmotePickerInput(event);
@@ -380,7 +392,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT) { if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT) {
break; break;
} }
// Handle UP/DOWN: activate canned message list! // Handle UP/DOWN: activate canned message list
if (event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN || if (event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN ||
event->inputEvent == INPUT_BROKER_ALT_LONG) { event->inputEvent == INPUT_BROKER_ALT_LONG) {
LaunchWithDestination(NODENUM_BROADCAST); LaunchWithDestination(NODENUM_BROADCAST);
@@ -415,14 +427,14 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
bool CannedMessageModule::isUpEvent(const InputEvent *event) bool CannedMessageModule::isUpEvent(const InputEvent *event)
{ {
return event->inputEvent == INPUT_BROKER_UP || return event->inputEvent == INPUT_BROKER_UP ||
((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || ((runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER ||
runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) &&
(event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS)); (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS));
} }
bool CannedMessageModule::isDownEvent(const InputEvent *event) bool CannedMessageModule::isDownEvent(const InputEvent *event)
{ {
return event->inputEvent == INPUT_BROKER_DOWN || return event->inputEvent == INPUT_BROKER_DOWN ||
((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || ((runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER ||
runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) &&
(event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS)); (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS));
} }
@@ -853,18 +865,13 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
return true; return true;
} }
// Move cursor left // LEFT/RIGHT in FREETEXT: go back to canned message list (ACTIVE), preserving input
if (event->inputEvent == INPUT_BROKER_LEFT) { if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT) {
payload = INPUT_BROKER_LEFT; runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
lastTouchMillis = millis(); UIFrameEvent e;
runOnce(); e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
return true; notifyObservers(&e);
} screen->forceDisplay();
// Move cursor right
if (event->inputEvent == INPUT_BROKER_RIGHT) {
payload = INPUT_BROKER_RIGHT;
lastTouchMillis = millis();
runOnce();
return true; return true;
} }
@@ -890,6 +897,14 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
return handleTabSwitch(event); // Reuse tab logic return handleTabSwitch(event); // Reuse tab logic
} }
// '*' key from TCA9535 numpad acts as backspace
if (event->kbchar == '*') {
payload = 0x08;
lastTouchMillis = millis();
runOnce();
return true;
}
// Printable ASCII (add char to draft) // Printable ASCII (add char to draft)
if (event->kbchar >= 32 && event->kbchar <= 126) { if (event->kbchar >= 32 && event->kbchar <= 126) {
payload = event->kbchar; payload = event->kbchar;