diff --git a/boards/t_echo_lite/include/boards/t_echo_lite/t_echo_lite_board.h b/boards/t_echo_lite/include/boards/t_echo_lite/t_echo_lite_board.h index ab955f77..b7a41113 100644 --- a/boards/t_echo_lite/include/boards/t_echo_lite/t_echo_lite_board.h +++ b/boards/t_echo_lite/include/boards/t_echo_lite/t_echo_lite_board.h @@ -195,7 +195,7 @@ class TEchoLiteBoard final : public BoardBase void applyKeyboardBacklight(); bool ensureKeyboardBacklightReady(); bool ensureMessageAudioReady(); - bool playMessageToneStep(unsigned frequency_hz, uint16_t duration_ms); + bool playMessageToneStep(unsigned start_frequency_hz, unsigned end_frequency_hz, uint16_t duration_ms); bool initialized_ = false; bool i2c_initialized_ = false; diff --git a/boards/t_echo_lite/src/t_echo_lite_board.cpp b/boards/t_echo_lite/src/t_echo_lite_board.cpp index bec8d719..133a27e7 100644 --- a/boards/t_echo_lite/src/t_echo_lite_board.cpp +++ b/boards/t_echo_lite/src/t_echo_lite_board.cpp @@ -235,18 +235,29 @@ bool applyAw21009Brightness(TEchoLiteBoard& board, uint16_t brightness) return ok; } -void fillSquareToneBuffer(unsigned frequency_hz, uint8_t volume_percent) +void fillCooToneBuffer(unsigned start_frequency_hz, unsigned end_frequency_hz, uint8_t volume_percent) { - const unsigned safe_frequency = frequency_hz == 0U ? 1U : frequency_hz; - const uint32_t half_period_samples = - std::max(1U, kMessageToneSampleRateHz / (safe_frequency * 2U)); const uint8_t volume = volume_percent > 100U ? 100U : volume_percent; - const int16_t amplitude = static_cast(600 + ((static_cast(volume) * 11000U) / 100U)); + const double amplitude = 500.0 + (static_cast(volume) * 80.0); + constexpr double kPi = 3.14159265358979323846; + const unsigned safe_start = start_frequency_hz == 0U ? 1U : start_frequency_hz; + const unsigned safe_end = end_frequency_hz == 0U ? safe_start : end_frequency_hz; + double phase = 0.0; for (uint16_t index = 0; index < kMessageToneI2sWords; ++index) { - const bool positive = ((index / half_period_samples) & 0x01U) == 0U; - const int16_t sample = positive ? amplitude : static_cast(-amplitude); + const double t = static_cast(index) / + static_cast(kMessageToneI2sWords > 1U ? kMessageToneI2sWords - 1U : 1U); + const double glide = static_cast(safe_start) + + (static_cast(safe_end) - static_cast(safe_start)) * t; + const double vibrato = std::sin(2.0 * kPi * 5.0 * t) * 7.0; + const double frequency = std::max(1.0, glide + vibrato); + const double attack = std::min(1.0, t / 0.12); + const double release = std::min(1.0, (1.0 - t) / 0.18); + const double envelope = std::max(0.0, std::min(attack, release)); + phase += (2.0 * kPi * frequency) / static_cast(kMessageToneSampleRateHz); + const double voice = std::sin(phase) + 0.22 * std::sin(phase * 2.0); + const int16_t sample = static_cast(voice * amplitude * envelope); const uint16_t packed_sample = static_cast(sample); s_message_tone_i2s_buffer[index] = static_cast(packed_sample) | (static_cast(packed_sample) << 16); @@ -581,7 +592,9 @@ bool TEchoLiteBoard::ensureMessageAudioReady() return audio_codec_ready_; } -bool TEchoLiteBoard::playMessageToneStep(unsigned frequency_hz, uint16_t duration_ms) +bool TEchoLiteBoard::playMessageToneStep(unsigned start_frequency_hz, + unsigned end_frequency_hz, + uint16_t duration_ms) { const auto& audio = kBoardProfile.audio; if (audio.dac_data < 0 || audio.bit_clock < 0 || audio.word_select < 0) @@ -589,7 +602,7 @@ bool TEchoLiteBoard::playMessageToneStep(unsigned frequency_hz, uint16_t duratio return false; } - fillSquareToneBuffer(frequency_hz, message_tone_volume_); + fillCooToneBuffer(start_frequency_hz, end_frequency_hz, message_tone_volume_); nrf_i2s_disable(NRF_I2S); nrf_i2s_event_clear(NRF_I2S, NRF_I2S_EVENT_TXPTRUPD); @@ -625,8 +638,6 @@ bool TEchoLiteBoard::playMessageToneStep(unsigned frequency_hz, uint16_t duratio void TEchoLiteBoard::playMessageTone() { - pulseNotificationLed(25); - if (message_tone_volume_ == 0) { return; @@ -634,22 +645,25 @@ void TEchoLiteBoard::playMessageTone() struct ToneStep { - unsigned frequency_hz; + unsigned start_frequency_hz; + unsigned end_frequency_hz; uint16_t duration_ms; uint16_t gap_ms; }; const bool audio_ready = ensureMessageAudioReady(); static constexpr ToneStep kMessageTone[] = { - {1760U, 70U, 25U}, - {2093U, 110U, 0U}, + {430U, 340U, 135U, 45U}, + {360U, 295U, 150U, 0U}, }; for (const ToneStep& step : kMessageTone) { if (audio_ready) { - (void)playMessageToneStep(step.frequency_hz, step.duration_ms); + (void)playMessageToneStep(step.start_frequency_hz, + step.end_frequency_hz, + step.duration_ms); } else { diff --git a/modules/ui_mono/include/ui/mono/runtime.h b/modules/ui_mono/include/ui/mono/runtime.h index 73b2df6d..818d9000 100644 --- a/modules/ui_mono/include/ui/mono/runtime.h +++ b/modules/ui_mono/include/ui/mono/runtime.h @@ -164,6 +164,8 @@ class Runtime : public chat::ChatService::IncomingTextObserver None = 0, Message, MeshCoreChannelName, + MeshtasticPrimaryKey, + MeshCoreChannelKey, }; void handleInput(InputAction action); @@ -223,7 +225,7 @@ class Runtime : public chat::ChatService::IncomingTextObserver void appendComposeWord(const char* text); void appendComposeRawText(const char* text); void activateComposeAction(); - void saveEditedTextToConfig(); + bool saveEditedTextToConfig(); void formatTime(char* out_time, size_t out_len, char* out_date, size_t date_len) const; void formatTimestamp(char* out, size_t out_len, uint32_t timestamp_s) const; void formatConversationTime(char* out, size_t out_len, uint32_t timestamp_s, bool expanded = false) const; @@ -242,6 +244,10 @@ class Runtime : public chat::ChatService::IncomingTextObserver const char* text, bool selected, bool align_right); void refreshGnssSnapshot(bool force = false); bool editUsesHexCharset() const; + bool editUsesKeyCharset() const; + bool editUsesDirectTextEntry() const; + bool editAllowsChineseInput() const; + bool editAcceptsTextInput() const; bool usesSmartCompose() const; bool usesPhysicalTextInput() const; bool usesFullVirtualKeyboard() const; @@ -348,7 +354,7 @@ class Runtime : public chat::ChatService::IncomingTextObserver size_t message_info_count_ = 0; EditTarget edit_target_ = EditTarget::None; - static constexpr size_t kComposeMax = 64; + static constexpr size_t kComposeMax = 96; char compose_buffer_[kComposeMax] = {}; size_t compose_len_ = 0; size_t compose_charset_index_ = 0; diff --git a/modules/ui_mono/src/runtime.cpp b/modules/ui_mono/src/runtime.cpp index ac30f018..dc5adf28 100644 --- a/modules/ui_mono/src/runtime.cpp +++ b/modules/ui_mono/src/runtime.cpp @@ -4,6 +4,7 @@ #include "ble/ble_manager.h" #include "chat/infra/mesh_protocol_utils.h" #include "chat/infra/meshcore/mc_region_presets.h" +#include "chat/infra/meshtastic/mt_protocol_helpers.h" #include "chat/infra/meshtastic/mt_region.h" #include "chat/runtime/self_identity_policy.h" #include "chat/usecase/chat_service.h" @@ -113,6 +114,7 @@ enum class RadioSettingItem MtCodingRate, MtTxPower, MtChannelSlot, + MtPrimaryKey, MtOverrideFrequency, MtFrequencyOffset, McRegion, @@ -123,6 +125,7 @@ enum class RadioSettingItem McTxPower, McChannelSlot, McChannelName, + McChannelKey, Encrypt, }; @@ -142,6 +145,7 @@ constexpr RadioSettingDef kRadioItems[] = { {RadioSettingItem::MtCodingRate, "MT CR"}, {RadioSettingItem::MtTxPower, "MT TX"}, {RadioSettingItem::MtChannelSlot, "MT SLOT"}, + {RadioSettingItem::MtPrimaryKey, "MT PSK"}, {RadioSettingItem::MtOverrideFrequency, "MT FREQ"}, {RadioSettingItem::MtFrequencyOffset, "MT OFFSET"}, {RadioSettingItem::McRegion, "MC REGION"}, @@ -152,6 +156,7 @@ constexpr RadioSettingDef kRadioItems[] = { {RadioSettingItem::McTxPower, "MC TX"}, {RadioSettingItem::McChannelSlot, "MC SLOT"}, {RadioSettingItem::McChannelName, "MC NAME"}, + {RadioSettingItem::McChannelKey, "MC KEY"}, {RadioSettingItem::Encrypt, "ENCRYPT"}, }; @@ -261,19 +266,19 @@ constexpr size_t kNodeActionItemCount = 7; constexpr const char* kWeekdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; constexpr const char* kComposeCharset = - " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?-_/:#@+*=()[]"; + " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?-_/:#@+*=%&$~()[]{}<>\\|;\"'`"; constexpr const char* kHexCharset = "0123456789ABCDEF"; constexpr const char* kComposeAlphaKeys = "QWERTYUIOPASDFGHJKLZXCVBNM"; constexpr const char* kComposeAbcKeys = " ETAOINSHRDLUCMFWYPVBGKQJXZ"; constexpr const char* kComposeNumKeys = "0123456789"; -constexpr const char* kComposeSymKeys = " .,!?/-:@#()[]+="; +constexpr const char* kComposeSymKeys = " .,!?/-:@#()[]{}+=_*%&$~\\|;\"'`<>"; constexpr const char* kComposeActionLabels[] = {"ABC", "SP", "BACK", "DEL", "SEND"}; constexpr size_t kVirtualKeyboardCols = 3; constexpr size_t kVirtualKeyboardRows = 3; constexpr size_t kVirtualKeyboardPageSize = kVirtualKeyboardCols * kVirtualKeyboardRows; constexpr const char* kFullAlphaRows[] = {"QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"}; constexpr const char* kFullNumRows[] = {"1234567890"}; -constexpr const char* kFullSymRows[] = {".,!?/-:@#", "()[]+="}; +constexpr const char* kFullSymRows[] = {".,!?/-:@#", "()[]{}+=", "_*%&$~\\|"}; struct PinyinCandidateEntry { @@ -287,6 +292,7 @@ constexpr uint32_t kBootMinMs = 1800; constexpr uint32_t kComposeMultiTapWindowMs = 1500; constexpr uint32_t kLargeScreensaverRefreshMs = 60000; constexpr uint32_t kGnssSnapshotRefreshMs = 5000; +constexpr size_t kKeyTextMax = 96; constexpr int kTimezoneMin = -12 * 60; constexpr int kTimezoneMax = 14 * 60; constexpr int kTimezoneStep = 60; @@ -742,6 +748,7 @@ const char* composeModeLabel(ui::mono::Runtime::ComposeMode mode) switch (mode) { case ui::mono::Runtime::ComposeMode::AbcLower: + return "en"; case ui::mono::Runtime::ComposeMode::AbcUpper: return "EN"; case ui::mono::Runtime::ComposeMode::Num: @@ -761,6 +768,8 @@ const char* physicalComposeModeLabel(ui::mono::Runtime::ComposeMode mode) { case ui::mono::Runtime::ComposeMode::Num: return "123"; + case ui::mono::Runtime::ComposeMode::Sym: + return "SYN"; case ui::mono::Runtime::ComposeMode::Cn: return "CN"; case ui::mono::Runtime::ComposeMode::AbcLower: @@ -827,6 +836,35 @@ const char* phoneCycleCharsForKey(char ch) } } +const char* phoneSymbolCycleCharsForKey(char ch) +{ + switch (ch) + { + case '1': + return ".,?!1"; + case '2': + return "@/:;2"; + case '3': + return "#+-=3"; + case '4': + return "_$%&4"; + case '5': + return "()[]5"; + case '6': + return "{}<>6"; + case '7': + return "'\"`~7"; + case '8': + return "\\|*^8"; + case '9': + return "-+=/9"; + case '0': + return " 0"; + default: + return ""; + } +} + char applyPhoneCycleCase(ui::mono::Runtime::ComposeMode mode, char ch) { if (std::isalpha(static_cast(ch)) == 0) @@ -849,7 +887,9 @@ void formatPhoneCycleHint(char* out, size_t out_len, } out[0] = '\0'; - const char* chars = phoneCycleCharsForKey(key); + const char* chars = mode == ui::mono::Runtime::ComposeMode::Sym + ? phoneSymbolCycleCharsForKey(key) + : phoneCycleCharsForKey(key); const size_t count = std::strlen(chars); if (count == 0) { @@ -1340,6 +1380,447 @@ void formatOffset(char* out, size_t out_len, float offset_mhz) std::snprintf(out, out_len, "%+.3f", static_cast(offset_mhz)); } +bool textEqualsIgnoreCase(const char* lhs, const char* rhs) +{ + if (!lhs || !rhs) + { + return false; + } + while (*lhs && *rhs) + { + if (upperAscii(*lhs) != upperAscii(*rhs)) + { + return false; + } + ++lhs; + ++rhs; + } + return *lhs == '\0' && *rhs == '\0'; +} + +void compactKeyText(const char* input, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + out[0] = '\0'; + if (!input) + { + return; + } + size_t pos = 0; + for (size_t i = 0; input[i] != '\0' && pos + 1 < out_len; ++i) + { + const unsigned char ch = static_cast(input[i]); + if (std::isspace(ch) != 0) + { + continue; + } + out[pos++] = static_cast(ch); + } + out[pos] = '\0'; +} + +int hexValue(char ch) +{ + if (ch >= '0' && ch <= '9') + { + return ch - '0'; + } + if (ch >= 'a' && ch <= 'f') + { + return ch - 'a' + 10; + } + if (ch >= 'A' && ch <= 'F') + { + return ch - 'A' + 10; + } + return -1; +} + +bool decodeHexBytes(const char* text, uint8_t* out, size_t out_capacity, size_t* out_len) +{ + if (out_len) + { + *out_len = 0; + } + if (!text || !out || out_capacity == 0) + { + return false; + } + + char digits[chat::kMeshtasticChannelKeyMaxLen * 2 + 1] = {}; + size_t digit_count = 0; + for (size_t i = 0; text[i] != '\0'; ++i) + { + const char ch = text[i]; + if (hexValue(ch) >= 0) + { + if (digit_count + 1 >= sizeof(digits)) + { + return false; + } + digits[digit_count++] = ch; + continue; + } + if (ch == ':' || ch == '-' || ch == '_') + { + continue; + } + return false; + } + if (digit_count == 0 || (digit_count % 2U) != 0U) + { + return false; + } + + const size_t byte_count = digit_count / 2U; + if (byte_count > out_capacity) + { + return false; + } + for (size_t i = 0; i < byte_count; ++i) + { + const int hi = hexValue(digits[i * 2U]); + const int lo = hexValue(digits[i * 2U + 1U]); + if (hi < 0 || lo < 0) + { + return false; + } + out[i] = static_cast((hi << 4) | lo); + } + if (out_len) + { + *out_len = byte_count; + } + return true; +} + +int base64Value(char ch) +{ + if (ch >= 'A' && ch <= 'Z') + { + return ch - 'A'; + } + if (ch >= 'a' && ch <= 'z') + { + return ch - 'a' + 26; + } + if (ch >= '0' && ch <= '9') + { + return ch - '0' + 52; + } + if (ch == '+') + { + return 62; + } + if (ch == '/') + { + return 63; + } + return -1; +} + +bool decodeBase64Bytes(const char* text, uint8_t* out, size_t out_capacity, size_t* out_len) +{ + if (out_len) + { + *out_len = 0; + } + if (!text || !out) + { + return false; + } + + size_t out_pos = 0; + int quartet[4] = {}; + size_t quartet_len = 0; + bool saw_padding = false; + for (size_t i = 0;; ++i) + { + const char ch = text[i]; + if (ch == '\0' || std::isspace(static_cast(ch)) != 0) + { + if (ch == '\0') + { + break; + } + continue; + } + + int value = 0; + if (ch == '=') + { + value = -2; + saw_padding = true; + } + else + { + if (saw_padding) + { + return false; + } + value = base64Value(ch); + if (value < 0) + { + return false; + } + } + quartet[quartet_len++] = value; + if (quartet_len < 4) + { + continue; + } + + if (quartet[0] < 0 || quartet[1] < 0) + { + return false; + } + const uint32_t packed = (static_cast(quartet[0]) << 18) | + (static_cast(quartet[1]) << 12) | + (static_cast(quartet[2] < 0 ? 0 : quartet[2]) << 6) | + static_cast(quartet[3] < 0 ? 0 : quartet[3]); + const size_t emit_count = quartet[2] < 0 ? 1U : (quartet[3] < 0 ? 2U : 3U); + if (out_pos + emit_count > out_capacity) + { + return false; + } + out[out_pos++] = static_cast((packed >> 16) & 0xFFU); + if (emit_count >= 2U) + { + out[out_pos++] = static_cast((packed >> 8) & 0xFFU); + } + if (emit_count >= 3U) + { + out[out_pos++] = static_cast(packed & 0xFFU); + } + quartet_len = 0; + } + + if (quartet_len != 0) + { + return false; + } + if (out_len) + { + *out_len = out_pos; + } + return out_pos > 0; +} + +void encodeBase64Bytes(const uint8_t* data, size_t len, char* out, size_t out_len) +{ + static constexpr char kAlphabet[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + if (!out || out_len == 0) + { + return; + } + out[0] = '\0'; + if (!data || len == 0) + { + return; + } + + size_t pos = 0; + for (size_t i = 0; i < len && pos + 4 < out_len; i += 3U) + { + const size_t remain = std::min(3U, len - i); + const uint32_t packed = (static_cast(data[i]) << 16) | + (remain > 1U ? (static_cast(data[i + 1U]) << 8) : 0U) | + (remain > 2U ? static_cast(data[i + 2U]) : 0U); + out[pos++] = kAlphabet[(packed >> 18) & 0x3FU]; + out[pos++] = kAlphabet[(packed >> 12) & 0x3FU]; + out[pos++] = remain > 1U ? kAlphabet[(packed >> 6) & 0x3FU] : '='; + out[pos++] = remain > 2U ? kAlphabet[packed & 0x3FU] : '='; + } + out[pos] = '\0'; +} + +bool meshtasticDefaultPsk(uint8_t* out, size_t out_capacity, size_t* out_len) +{ + if (out_len) + { + *out_len = 0; + } + if (!out || out_capacity < chat::kMeshtasticChannelKeyDefaultLen) + { + return false; + } + size_t expanded_len = 0; + chat::meshtastic::expandShortPsk(1U, out, &expanded_len); + if (expanded_len == 0 || expanded_len > out_capacity) + { + return false; + } + if (out_len) + { + *out_len = expanded_len; + } + return true; +} + +bool isMeshtasticDefaultPsk(const uint8_t* key, size_t len) +{ + uint8_t default_key[chat::kMeshtasticChannelKeyDefaultLen] = {}; + size_t default_len = 0; + return meshtasticDefaultPsk(default_key, sizeof(default_key), &default_len) && + len == default_len && + std::memcmp(key, default_key, default_len) == 0; +} + +uint8_t storedMeshtasticKeyLen(const chat::MeshConfig& mesh) +{ + return chat::normalizeMeshtasticChannelKeyLen(mesh.primary_key, + sizeof(mesh.primary_key), + mesh.primary_key_len); +} + +void formatMeshtasticPrimaryKeySummary(const chat::MeshConfig& mesh, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + const uint8_t key_len = storedMeshtasticKeyLen(mesh); + if (key_len == 0 || isMeshtasticDefaultPsk(mesh.primary_key, key_len)) + { + std::snprintf(out, out_len, "AQ=="); + return; + } + std::snprintf(out, out_len, "B64 %uB", static_cast(key_len)); +} + +void formatMeshtasticPrimaryKeyForEdit(const chat::MeshConfig& mesh, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + const uint8_t key_len = storedMeshtasticKeyLen(mesh); + if (key_len == 0 || isMeshtasticDefaultPsk(mesh.primary_key, key_len)) + { + std::snprintf(out, out_len, "AQ=="); + return; + } + encodeBase64Bytes(mesh.primary_key, key_len, out, out_len); +} + +void formatMeshCoreChannelKeySummary(const chat::MeshConfig& mesh, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + if (chat::isAllZeroKeyBytes(mesh.secondary_key, chat::kMeshCoreChannelKeyLen)) + { + std::snprintf(out, out_len, "PUBLIC"); + return; + } + std::snprintf(out, out_len, "B64 16B"); +} + +void formatMeshCoreChannelKeyForEdit(const chat::MeshConfig& mesh, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + if (chat::isAllZeroKeyBytes(mesh.secondary_key, chat::kMeshCoreChannelKeyLen)) + { + out[0] = '\0'; + return; + } + encodeBase64Bytes(mesh.secondary_key, chat::kMeshCoreChannelKeyLen, out, out_len); +} + +bool applyMeshtasticPrimaryKeyText(chat::MeshConfig& mesh, const char* input) +{ + char compact[kKeyTextMax] = {}; + compactKeyText(input, compact, sizeof(compact)); + + std::memset(mesh.primary_key, 0, sizeof(mesh.primary_key)); + mesh.primary_key_len = 0; + if (compact[0] == '\0' || + textEqualsIgnoreCase(compact, "DEFAULT") || + textEqualsIgnoreCase(compact, "PUBLIC")) + { + return true; + } + + uint8_t decoded[chat::kMeshtasticChannelKeyMaxLen] = {}; + size_t decoded_len = 0; + if (decodeHexBytes(compact, decoded, sizeof(decoded), &decoded_len)) + { + if (decoded_len != chat::kMeshtasticChannelKeyDefaultLen && + decoded_len != chat::kMeshtasticChannelKeyMaxLen) + { + return false; + } + std::memcpy(mesh.primary_key, decoded, decoded_len); + mesh.primary_key_len = static_cast(decoded_len); + return true; + } + + if (!decodeBase64Bytes(compact, decoded, sizeof(decoded), &decoded_len)) + { + return false; + } + if (decoded_len == 1U) + { + size_t expanded_len = 0; + if (!meshtasticDefaultPsk(mesh.primary_key, sizeof(mesh.primary_key), &expanded_len)) + { + return false; + } + if (decoded[0] != 1U) + { + chat::meshtastic::expandShortPsk(decoded[0], mesh.primary_key, &expanded_len); + } + mesh.primary_key_len = static_cast(expanded_len); + return expanded_len == chat::kMeshtasticChannelKeyDefaultLen; + } + if (decoded_len != chat::kMeshtasticChannelKeyDefaultLen && + decoded_len != chat::kMeshtasticChannelKeyMaxLen) + { + return false; + } + std::memcpy(mesh.primary_key, decoded, decoded_len); + mesh.primary_key_len = static_cast(decoded_len); + return true; +} + +bool applyMeshCoreChannelKeyText(chat::MeshConfig& mesh, const char* input) +{ + char compact[kKeyTextMax] = {}; + compactKeyText(input, compact, sizeof(compact)); + + std::memset(mesh.secondary_key, 0, sizeof(mesh.secondary_key)); + mesh.secondary_key_len = 0; + if (compact[0] == '\0' || + textEqualsIgnoreCase(compact, "PUBLIC") || + textEqualsIgnoreCase(compact, "NONE")) + { + return true; + } + + uint8_t decoded[chat::kMeshtasticChannelKeyMaxLen] = {}; + size_t decoded_len = 0; + if (!decodeHexBytes(compact, decoded, sizeof(decoded), &decoded_len) && + !decodeBase64Bytes(compact, decoded, sizeof(decoded), &decoded_len)) + { + return false; + } + if (decoded_len != chat::kMeshCoreChannelKeyLen) + { + return false; + } + std::memcpy(mesh.secondary_key, decoded, chat::kMeshCoreChannelKeyLen); + mesh.secondary_key_len = static_cast(chat::kMeshCoreChannelKeyLen); + return true; +} + void formatRadioSettingValue(const app::AppConfig& cfg, RadioSettingItem item, char* out, size_t out_len) { if (!out || out_len == 0) @@ -1381,6 +1862,9 @@ void formatRadioSettingValue(const app::AppConfig& cfg, RadioSettingItem item, c case RadioSettingItem::MtChannelSlot: formatMeshtasticChannelSlot(out, out_len, mt.channel_num); return; + case RadioSettingItem::MtPrimaryKey: + formatMeshtasticPrimaryKeySummary(mt, out, out_len); + return; case RadioSettingItem::MtOverrideFrequency: if (mt.override_frequency_mhz <= 0.0f) { @@ -1421,6 +1905,9 @@ void formatRadioSettingValue(const app::AppConfig& cfg, RadioSettingItem item, c case RadioSettingItem::McChannelName: std::snprintf(out, out_len, "%s", mc.meshcore_channel_name); return; + case RadioSettingItem::McChannelKey: + formatMeshCoreChannelKeySummary(mc, out, out_len); + return; } } @@ -2354,10 +2841,27 @@ void Runtime::handleInput(InputAction action) } else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) { - if (radioSettingItem(radio_index_) == RadioSettingItem::McChannelName) + const RadioSettingItem item = radioSettingItem(radio_index_); + if (item == RadioSettingItem::McChannelName) { openCompose(EditTarget::MeshCoreChannelName, app()->getConfig().meshcore_config.meshcore_channel_name); } + else if (item == RadioSettingItem::MtPrimaryKey) + { + char key_text[kKeyTextMax] = {}; + formatMeshtasticPrimaryKeyForEdit(app()->getConfig().meshtastic_config, + key_text, + sizeof(key_text)); + openCompose(EditTarget::MeshtasticPrimaryKey, key_text); + } + else if (item == RadioSettingItem::McChannelKey) + { + char key_text[kKeyTextMax] = {}; + formatMeshCoreChannelKeyForEdit(app()->getConfig().meshcore_config, + key_text, + sizeof(key_text)); + openCompose(EditTarget::MeshCoreChannelKey, key_text); + } else { beginSettingPopup(Page::RadioSettings, radio_index_); @@ -3438,11 +3942,31 @@ void Runtime::renderCompose() } const int line_h = text_renderer_.lineHeight(); - char target[16] = {}; - formatComposeTarget(target, sizeof(target)); - char to_line[24] = {}; - std::snprintf(to_line, sizeof(to_line), "TO:%s", target[0] != '\0' ? target : "-"); - drawTextClipped(0, 0, display_.width(), to_line, false); + if (edit_target_ == EditTarget::Message) + { + char target[16] = {}; + formatComposeTarget(target, sizeof(target)); + char to_line[24] = {}; + std::snprintf(to_line, sizeof(to_line), "TO:%s", target[0] != '\0' ? target : "-"); + drawTextClipped(0, 0, display_.width(), to_line, false); + } + else + { + const char* title = "EDIT"; + if (edit_target_ == EditTarget::MeshtasticPrimaryKey) + { + title = "MT PSK"; + } + else if (edit_target_ == EditTarget::MeshCoreChannelKey) + { + title = "MC KEY"; + } + else if (edit_target_ == EditTarget::MeshCoreChannelName) + { + title = "MC NAME"; + } + drawTitleBar(title, composeModeLabel(compose_mode_)); + } constexpr size_t kBodyVisiblePerLine = 18; char body_text[kComposeMax + 1] = {}; @@ -3587,7 +4111,11 @@ void Runtime::renderCompose() const int action_y = line_h * 7; const int action_x[5] = {0, 24, 48, 72, 96}; - const char* action_labels[5] = {composeModeLabel(compose_mode_), " ", "\xE2\x86\x90", "\xE2\x87\xA4", "SEND"}; + const char* action_labels[5] = {composeModeLabel(compose_mode_), + " ", + "\xE2\x86\x90", + "\xE2\x87\xA4", + edit_target_ == EditTarget::Message ? "SEND" : "OK"}; for (size_t i = 0; i < 5; ++i) { char action_cell[10] = {}; @@ -4293,9 +4821,18 @@ void Runtime::openCompose(EditTarget target, const char* seed_text) compose_buffer_[0] = '\0'; compose_len_ = 0; compose_charset_index_ = 0; - compose_mode_ = usesPhysicalTextInput() - ? (target == EditTarget::Message ? ComposeMode::Cn : ComposeMode::AbcLower) - : ComposeMode::AbcLower; + if (target == EditTarget::Message && usesPhysicalTextInput()) + { + compose_mode_ = ComposeMode::Cn; + } + else if (editUsesKeyCharset()) + { + compose_mode_ = ComposeMode::AbcUpper; + } + else + { + compose_mode_ = ComposeMode::AbcLower; + } compose_focus_ = ComposeFocus::Body; compose_action_index_ = 0; compose_abc_group_index_ = 0; @@ -4323,9 +4860,9 @@ void Runtime::openCompose(EditTarget target, const char* seed_text) void Runtime::finishTextEdit(bool accept) { - if (accept) + if (accept && !saveEditedTextToConfig()) { - saveEditedTextToConfig(); + return; } edit_target_ = EditTarget::None; enterPage(page_before_compose_); @@ -5008,6 +5545,10 @@ void Runtime::confirmSettingPopup() app()->setBleEnabled(setting_popup_ble_enabled_); #endif app()->saveConfig(); + if (setting_popup_owner_ == Page::RadioSettings) + { + app()->applyMeshConfig(); + } if (setting_popup_owner_ == Page::DeviceSettings) { app()->applyPositionConfig(); @@ -5103,6 +5644,8 @@ void Runtime::adjustSettingPopup(int delta) current + delta, 0, static_cast(kMeshtasticChannelNumMax))); break; } + case RadioSettingItem::MtPrimaryKey: + break; case RadioSettingItem::MtOverrideFrequency: if (cfg.meshtastic_config.override_frequency_mhz <= 0.0f) { @@ -5176,6 +5719,7 @@ void Runtime::adjustSettingPopup(int delta) static_cast(cfg.meshcore_config.meshcore_channel_slot) + delta, 0, 15)); break; case RadioSettingItem::McChannelName: + case RadioSettingItem::McChannelKey: default: break; } @@ -5458,26 +6002,19 @@ void Runtime::cycleComposeMode() if (usesPhysicalTextInput()) { (void)commitPhysicalComposePreedit(true); - if (compose_mode_ == ComposeMode::Num) - { - compose_mode_ = ComposeMode::AbcLower; - } - else if (compose_mode_ == ComposeMode::AbcLower || compose_mode_ == ComposeMode::AbcUpper) - { - compose_mode_ = ComposeMode::Cn; - } - else - { - compose_mode_ = ComposeMode::Num; - } } - else if (compose_mode_ == ComposeMode::Num) + + if (compose_mode_ == ComposeMode::Num) { compose_mode_ = ComposeMode::AbcLower; } - else if (compose_mode_ == ComposeMode::AbcLower || compose_mode_ == ComposeMode::AbcUpper) + else if (compose_mode_ == ComposeMode::AbcLower) { - compose_mode_ = ComposeMode::Cn; + compose_mode_ = ComposeMode::AbcUpper; + } + else if (compose_mode_ == ComposeMode::AbcUpper) + { + compose_mode_ = editAllowsChineseInput() ? ComposeMode::Cn : ComposeMode::Sym; } else if (compose_mode_ == ComposeMode::Cn) { @@ -5521,6 +6058,37 @@ bool Runtime::handlePhysicalComposeText(char ch) } compose_focus_ = ComposeFocus::Body; + if (compose_mode_ == ComposeMode::Sym) + { + (void)commitPhysicalComposePreedit(true); + const char* chars = phoneSymbolCycleCharsForKey(ch); + const size_t char_count = std::strlen(chars); + if (char_count == 0) + { + return true; + } + + const uint32_t now = nowMs(); + const bool can_cycle = compose_physical_last_key_ == ch && + compose_len_ > 0 && + (now - compose_abc_last_tap_ms_) <= kComposeMultiTapWindowMs; + if (can_cycle) + { + compose_abc_tap_index_ = (compose_abc_tap_index_ + 1U) % char_count; + compose_buffer_[compose_len_ - 1U] = chars[compose_abc_tap_index_]; + } + else + { + compose_abc_tap_index_ = 0; + appendChar(compose_buffer_, sizeof(compose_buffer_), compose_len_, chars[0]); + } + + compose_physical_last_key_ = ch; + compose_abc_last_tap_ms_ = now; + compose_abc_last_group_index_ = -1; + return true; + } + if (compose_mode_ == ComposeMode::Cn) { if (ch == '0') @@ -5760,6 +6328,15 @@ void Runtime::appendComposeChar(char ch) return; } + if (editUsesDirectTextEntry()) + { + appendChar(compose_buffer_, + sizeof(compose_buffer_), + compose_len_, + isAlphaAscii(ch) ? applyComposeAlphaCase(compose_mode_, ch) : ch); + return; + } + if ((isAlphaComposeMode(compose_mode_) || compose_mode_ == ComposeMode::Cn) && isAlphaAscii(ch)) { appendChar(compose_preedit_, sizeof(compose_preedit_), compose_preedit_len_, upperAscii(ch)); @@ -5835,28 +6412,58 @@ void Runtime::activateComposeAction() compose_focus_ = ComposeFocus::Body; break; case ComposeAction::Send: - sendComposeMessage(); + if (edit_target_ == EditTarget::Message) + { + sendComposeMessage(); + } + else + { + finishTextEdit(true); + } break; } } -void Runtime::saveEditedTextToConfig() +bool Runtime::saveEditedTextToConfig() { if (!app()) { - return; + return false; } auto& cfg = app()->getConfig(); + bool mesh_config_changed = false; switch (edit_target_) { case EditTarget::MeshCoreChannelName: copyText(cfg.meshcore_config.meshcore_channel_name, compose_buffer_); + mesh_config_changed = true; + break; + case EditTarget::MeshtasticPrimaryKey: + if (!applyMeshtasticPrimaryKeyText(cfg.meshtastic_config, compose_buffer_)) + { + showTransientPopup("INVALID KEY", "MT PSK"); + return false; + } + mesh_config_changed = true; + break; + case EditTarget::MeshCoreChannelKey: + if (!applyMeshCoreChannelKeyText(cfg.meshcore_config, compose_buffer_)) + { + showTransientPopup("INVALID KEY", "MC KEY"); + return false; + } + mesh_config_changed = true; break; default: break; } commitConfig(); + if (mesh_config_changed) + { + app()->applyMeshConfig(); + } + return true; } void Runtime::formatTime(char* out_time, size_t out_len, char* out_date, size_t date_len) const @@ -6419,14 +7026,38 @@ bool Runtime::editUsesHexCharset() const return false; } +bool Runtime::editUsesKeyCharset() const +{ + return edit_target_ == EditTarget::MeshtasticPrimaryKey || + edit_target_ == EditTarget::MeshCoreChannelKey; +} + +bool Runtime::editUsesDirectTextEntry() const +{ + return edit_target_ == EditTarget::MeshCoreChannelName || + editUsesKeyCharset(); +} + +bool Runtime::editAllowsChineseInput() const +{ + return edit_target_ == EditTarget::Message; +} + +bool Runtime::editAcceptsTextInput() const +{ + return edit_target_ == EditTarget::Message || + edit_target_ == EditTarget::MeshCoreChannelName || + editUsesKeyCharset(); +} + bool Runtime::usesSmartCompose() const { - return edit_target_ == EditTarget::Message && !usesPhysicalTextInput(); + return editAcceptsTextInput() && !usesPhysicalTextInput(); } bool Runtime::usesPhysicalTextInput() const { - return host_.physical_text_input && edit_target_ == EditTarget::Message; + return host_.physical_text_input && editAcceptsTextInput(); } bool Runtime::usesFullVirtualKeyboard() const