Cardputer beta (#10063)

* Cardputer Kit
BMI270 WIP

* BMI270 support

* verify that the number of bytes read matches the requested length

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* trunk'd

* remove excessive logging

* Kick the screen when unsleeping

* Update the st7789 library, and enable displayon and displayoff

* Battery detection

* Default to arrow keys and enter, while in menus.

* Enable Backlight control

* Update src/detect/ScanI2CTwoWire.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix cardputer sound

* Add I2S BLEEBLEs

* MenuHandler ifdefs

* add generic tone -> I2S RTTTL conversion

* not needed

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
This commit is contained in:
Jonathan Bennett
2026-04-10 14:32:59 -05:00
committed by GitHub
co-authored by Copilot Thomas Göttgens Ben Meadors Manuel
parent c4c4fe7082
commit 64cda52920
22 changed files with 1242 additions and 61 deletions
+55 -47
View File
@@ -60,7 +60,7 @@ CannedMessageModule::CannedMessageModule()
if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE &&
!CANNED_MESSAGE_MODULE_ENABLE) {
LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled");
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
this->updateState(CANNED_MESSAGE_RUN_STATE_DISABLED);
disable();
} else {
LOG_INFO("CannedMessageModule is enabled");
@@ -94,8 +94,7 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan
currentMessageIndex = selectDestination;
// This triggers the canned message list
runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
requestFocus();
updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE);
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e);
@@ -123,8 +122,7 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t
lastChannel = channel;
lastDestSet = true;
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
requestFocus();
updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT);
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e);
@@ -324,11 +322,10 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
// Matrix keypad: If matrix key, trigger action select for canned message
if (event->inputEvent == INPUT_BROKER_MATRIXKEY) {
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT);
payload = INPUT_BROKER_MATRIXKEY;
currentMessageIndex = event->kbchar - 1;
lastTouchMillis = millis();
requestFocus();
return 1;
}
@@ -375,8 +372,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
}
// Printable char (ASCII) opens free text compose
if (event->kbchar >= 32 && event->kbchar <= 126) {
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
requestFocus();
updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT);
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e);
@@ -399,6 +395,21 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
return 0;
}
void CannedMessageModule::updateState(cannedMessageModuleRunState newState)
{
runState = newState;
if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
inputBroker->menuMode =
false; // Allow any key input to be sent to the message composer instead of being interpreted as menu navigation
} else {
inputBroker->menuMode = true; // Re-enable menu navigation for destination selection
}
if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_FREETEXT ||
runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) {
requestFocus();
}
}
bool CannedMessageModule::isUpEvent(const InputEvent *event)
{
return event->inputEvent == INPUT_BROKER_UP ||
@@ -423,15 +434,14 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event)
if (event->kbchar != 0x09)
return false;
runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT
: CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
updateState((runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT
: CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION);
destIndex = 0;
scrollIndex = 0;
// RESTORE THIS!
if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
updateDestinationSelectionList();
requestFocus();
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
@@ -537,7 +547,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
}
}
runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT;
updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT);
returnToCannedList = false;
screen->forceDisplay(true);
return 1;
@@ -545,7 +555,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
// CANCEL
if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) {
runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT;
updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT);
returnToCannedList = false;
searchQuery = "";
@@ -576,7 +586,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
// === Handle Cancel key: go inactive, clear UI state ===
if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE &&
(event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) {
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
freetext = "";
cursor = 0;
payload = 0;
@@ -594,10 +604,10 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
// Handle up/down navigation
if (isUp && messagesCount > 0) {
runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP;
updateState(CANNED_MESSAGE_RUN_STATE_ACTION_UP);
handled = true;
} else if (isDown && messagesCount > 0) {
runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN;
updateState(CANNED_MESSAGE_RUN_STATE_ACTION_DOWN);
handled = true;
} else if (isSelect) {
const char *current = messages[currentMessageIndex];
@@ -605,7 +615,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
// === [Select Destination] triggers destination selection UI ===
if (strcmp(current, "[Select Destination]") == 0) {
returnToCannedList = true;
runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
updateState(CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION);
destIndex = 0;
scrollIndex = 0;
updateDestinationSelectionList(); // Make sure list is fresh
@@ -616,7 +626,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
// === [Exit] returns to the main/inactive screen ===
if (strcmp(current, "[Exit]") == 0) {
// Set runState to inactive so we return to main UI
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
currentMessageIndex = -1;
// Notify UI to regenerate frame set and redraw
@@ -630,8 +640,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
// === [Free Text] triggers the free text input (virtual keyboard) ===
#if defined(USE_VIRTUAL_KEYBOARD)
if (strcmp(current, "[-- Free Text --]") == 0) {
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
requestFocus();
updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT);
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e);
@@ -650,7 +659,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
if (!text.empty()) {
this->freetext = text.c_str();
this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE);
currentMessageIndex = -1;
UIFrameEvent e;
@@ -667,7 +676,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
graphics::NotificationRenderer::resetBanner();
// Return to inactive state
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
@@ -699,7 +708,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
graphics::menuHandler::showConfirmationBanner("Send message?", [this, destNode, chan, current]() {
this->sendText(destNode, chan, current, false);
payload = runState;
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
currentMessageIndex = -1;
// Notify UI to regenerate frame set and redraw
@@ -710,7 +719,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
});
#else
payload = runState;
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT);
#endif
// Do not immediately set runState; wait for confirmation
handled = true;
@@ -736,7 +745,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
#if defined(USE_VIRTUAL_KEYBOARD)
// Cancel (dismiss freetext screen)
if (event->inputEvent == INPUT_BROKER_LEFT) {
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
freetext = "";
cursor = 0;
payload = 0;
@@ -782,7 +791,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
}
// Touch enter/submit
else if (keyTapped == "↵") {
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message!
updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); // Send the message!
payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
currentMessageIndex = -1;
shift = false;
@@ -808,8 +817,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
// ---- All hardware keys fall through to here (CardKB, physical, etc.) ----
if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) {
runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER;
requestFocus();
updateState(CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER);
screen->forceDisplay();
return true;
}
@@ -826,7 +834,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
currentMessageIndex = -1;
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT);
lastTouchMillis = millis();
runOnce();
return true;
@@ -858,7 +866,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
// Cancel (dismiss freetext screen)
if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG ||
(event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) {
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
freetext = "";
cursor = 0;
payload = 0;
@@ -926,14 +934,14 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event)
freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor);
}
cursor += emoteInsert.length();
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT);
screen->forceDisplay();
return 1;
}
// Cancel returns to freetext
if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) {
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT);
screen->forceDisplay();
return 1;
}
@@ -1044,7 +1052,7 @@ int32_t CannedMessageModule::runOnce()
} else {
// Empty message, just go inactive
LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state");
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
}
UIFrameEvent e;
@@ -1061,7 +1069,7 @@ int32_t CannedMessageModule::runOnce()
this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) ||
(this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) ||
(this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) {
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
temporaryMessage = "";
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->currentMessageIndex = -1;
@@ -1073,7 +1081,7 @@ int32_t CannedMessageModule::runOnce()
else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) {
// This happens after virtual keyboard message sending is complete
LOG_INFO("Virtual keyboard message sending completed, returning to inactive state");
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
temporaryMessage = "";
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->currentMessageIndex = -1;
@@ -1087,7 +1095,7 @@ int32_t CannedMessageModule::runOnce()
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
// Clean up virtual keyboard if it exists during timeout
if (graphics::NotificationRenderer::virtualKeyboard) {
@@ -1103,17 +1111,17 @@ int32_t CannedMessageModule::runOnce()
if (this->payload == 0) {
// [Exit] button pressed - return to inactive state
LOG_INFO("Processing [Exit] action - returning to inactive state");
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
} else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
if (this->freetext.length() > 0) {
sendText(this->dest, this->channel, this->freetext.c_str(), true);
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE);
} else {
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
}
} else {
if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) {
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE);
return INT32_MAX;
}
if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) {
@@ -1122,9 +1130,9 @@ int32_t CannedMessageModule::runOnce()
} else {
sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true);
}
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE);
} else {
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
}
}
this->currentMessageIndex = -1;
@@ -1144,13 +1152,13 @@ int32_t CannedMessageModule::runOnce()
}
this->currentMessageIndex = selectDestination;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE);
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) {
if (this->messagesCount > 0) {
this->currentMessageIndex = getPrevIndex();
this->freetext = "";
this->cursor = 0;
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE);
LOG_DEBUG("MOVE UP (%d):%s", this->currentMessageIndex, this->getCurrentMessage());
}
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) {
@@ -1158,7 +1166,7 @@ int32_t CannedMessageModule::runOnce()
this->currentMessageIndex = this->getNextIndex();
this->freetext = "";
this->cursor = 0;
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE);
LOG_DEBUG("MOVE DOWN (%d):%s", this->currentMessageIndex, this->getCurrentMessage());
}
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) {
@@ -2156,7 +2164,7 @@ ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
requestFocus();
this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED;
this->updateState(CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED);
// Decode the routing response
meshtastic_Routing decoded = meshtastic_Routing_init_default;