#ifdef ESP_PLATFORM #include #endif #if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED) #include #include #include #include #include #include #include #include #include #include namespace tt::app::chat { constexpr auto* TAG = "ChatApp"; static constexpr uint8_t BROADCAST_ADDRESS[ESP_NOW_ETH_ALEN] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; void ChatApp::enableEspNow() { static uint8_t defaultKey[ESP_NOW_KEY_LEN] = {}; auto config = service::espnow::EspNowConfig( settings.hasEncryptionKey ? settings.encryptionKey.data() : defaultKey, service::espnow::Mode::Station, 1, // Channel 1 default; actual channel determined by WiFi if connected false, settings.hasEncryptionKey ); service::espnow::enable(config); } void ChatApp::disableEspNow() { if (service::espnow::isEnabled()) { service::espnow::disable(); } } void ChatApp::onCreate(AppContext& appContext) { isFirstLaunch = !settingsFileExists(); settings = loadSettings(); state.setLocalNickname(settings.nickname); if (!settings.chatChannel.empty()) { state.setCurrentChannel(settings.chatChannel); } enableEspNow(); receiveSubscription = service::espnow::subscribeReceiver( [this](const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) { onReceive(receiveInfo, data, length); } ); } void ChatApp::onDestroy(AppContext& appContext) { service::espnow::unsubscribeReceiver(receiveSubscription); disableEspNow(); } void ChatApp::onShow(AppContext& context, lv_obj_t* parent) { view.init(context, parent); if (isFirstLaunch) { view.showSettings(settings); } } void ChatApp::onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) { if (length <= 0) return; ParsedMessage parsed; if (!deserializeMessage(data, static_cast(length), parsed)) { return; } StoredMessage msg; msg.displayText = parsed.senderName + ": " + parsed.message; msg.target = parsed.target; msg.isOwn = false; state.addMessage(msg); { auto lock = lvgl::getSyncLock()->asScopedLock(); lock.lock(); view.displayMessage(msg); } } void ChatApp::sendMessage(const std::string& text) { if (text.empty()) return; std::string nickname = state.getLocalNickname(); std::string channel = state.getCurrentChannel(); std::vector wireMsg; if (!serializeTextMessage(settings.senderId, BROADCAST_ID, nickname, channel, text, wireMsg)) { LOG_E(TAG, "Failed to serialize message"); return; } if (!service::espnow::send(BROADCAST_ADDRESS, wireMsg.data(), wireMsg.size())) { LOG_E(TAG, "Failed to send message"); return; } StoredMessage msg; msg.displayText = nickname + ": " + text; msg.target = channel; msg.isOwn = true; state.addMessage(msg); { auto lock = lvgl::getSyncLock()->asScopedLock(); lock.lock(); view.displayMessage(msg); } } void ChatApp::applySettings(const std::string& nickname, const std::string& keyHex) { bool needRestart = false; // Trim nickname to protocol limit settings.nickname = nickname.substr(0, MAX_NICKNAME_LEN); // Parse hex key if (keyHex.size() == ESP_NOW_KEY_LEN * 2) { bool validHex = std::all_of(keyHex.begin(), keyHex.end(), [](unsigned char c) { return std::isxdigit(c); }); if (validHex) { uint8_t newKey[ESP_NOW_KEY_LEN]; for (int i = 0; i < ESP_NOW_KEY_LEN; i++) { char hex[3] = { keyHex[i * 2], keyHex[i * 2 + 1], 0 }; newKey[i] = static_cast(strtoul(hex, nullptr, 16)); } // Restart if key changed OR if encryption is being enabled bool wasEnabled = settings.hasEncryptionKey; if (!wasEnabled || !std::equal(newKey, newKey + ESP_NOW_KEY_LEN, settings.encryptionKey.begin())) { std::copy(newKey, newKey + ESP_NOW_KEY_LEN, settings.encryptionKey.begin()); needRestart = true; } settings.hasEncryptionKey = true; } else { LOG_W(TAG, "Invalid hex characters in encryption key"); } } else if (keyHex.empty()) { if (settings.hasEncryptionKey) { settings.encryptionKey.fill(0); settings.hasEncryptionKey = false; needRestart = true; } } else { LOG_W(TAG, "Key must be exactly %d hex characters, got %d", (int)(ESP_NOW_KEY_LEN * 2), (int)keyHex.size()); } state.setLocalNickname(settings.nickname); saveSettings(settings); if (needRestart) { disableEspNow(); enableEspNow(); } } void ChatApp::switchChannel(const std::string& chatChannel) { const auto trimmedChannel = chatChannel.substr(0, MAX_TARGET_LEN); state.setCurrentChannel(trimmedChannel); settings.chatChannel = trimmedChannel; saveSettings(settings); { auto lock = lvgl::getSyncLock()->asScopedLock(); lock.lock(); view.refreshMessageList(); } } extern const AppManifest manifest = { .appId = "Chat", .appName = "Chat", .appIcon = LVGL_ICON_SHARED_FORUM, .createApp = create }; } // namespace tt::app::chat #endif // CONFIG_SOC_WIFI_SUPPORTED || CONFIG_SLAVE_SOC_WIFI_SUPPORTED