Chat app update, EspNow v2 & GPS Info (#460)

This commit is contained in:
Shadowtrance
2026-01-27 02:32:57 +10:00
committed by GitHub
parent dfe2c865d1
commit 10381b10cd
18 changed files with 1547 additions and 124 deletions
@@ -0,0 +1,46 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
#include "ChatState.h"
#include "ChatView.h"
#include "ChatSettings.h"
#include <Tactility/app/App.h>
#include <Tactility/service/espnow/EspNow.h>
namespace tt::app::chat {
class ChatApp final : public App {
ChatState state;
ChatView view = ChatView(this, &state);
service::espnow::ReceiverSubscription receiveSubscription = -1;
ChatSettingsData settings;
bool isFirstLaunch = false;
void onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length);
void enableEspNow();
void disableEspNow();
public:
void onCreate(AppContext& appContext) override;
void onDestroy(AppContext& appContext) override;
void onShow(AppContext& context, lv_obj_t* parent) override;
void sendMessage(const std::string& text);
void applySettings(const std::string& nickname, const std::string& keyHex);
void switchChannel(const std::string& chatChannel);
const ChatSettingsData& getSettings() const { return settings; }
~ChatApp() override = default;
};
} // namespace tt::app::chat
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED
@@ -0,0 +1,97 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace tt::app::chat {
// Protocol identification
constexpr uint32_t CHAT_MAGIC_V2 = 0x54435432; // "TCT2"
constexpr uint16_t PROTOCOL_VERSION = 2;
// Broadcast/channel target ID
constexpr uint32_t BROADCAST_ID = 0;
// Payload types
enum class PayloadType : uint8_t {
TextMessage = 1,
// Future: Position = 2, Telemetry = 3, etc.
};
// Wire format header (16 bytes)
struct __attribute__((packed)) MessageHeader {
uint32_t magic; // CHAT_MAGIC_V2
uint16_t protocol_version; // PROTOCOL_VERSION
uint32_t from; // Sender ID (random, stored in settings)
uint32_t to; // Recipient ID (0 = broadcast/channel)
uint8_t payload_type; // PayloadType enum
uint8_t payload_size; // Size of payload following header
};
static_assert(sizeof(MessageHeader) == 16, "MessageHeader must be 16 bytes");
// Size limits
constexpr size_t HEADER_SIZE = sizeof(MessageHeader);
constexpr size_t MAX_PAYLOAD_V1 = 250 - HEADER_SIZE; // 234 bytes for ESP-NOW v1
constexpr size_t MAX_PAYLOAD_V2 = 1470 - HEADER_SIZE; // 1454 bytes for ESP-NOW v2
// Nickname constraints
constexpr size_t MIN_NICKNAME_LEN = 2; // Single-letter names not allowed
constexpr size_t MAX_NICKNAME_LEN = 23; // Max nickname length (excluding null)
// Target/channel constraints (0 = broadcast allowed)
constexpr size_t MIN_TARGET_LEN = 0; // Empty = broadcast
constexpr size_t MAX_TARGET_LEN = 23; // Max target/channel length (excluding null)
// Message constraints
constexpr size_t MIN_MESSAGE_LEN = 1; // At least 1 char (e.g. "?")
// Max message length: 255 (uint8_t payload_size) - nickname - null - target - null
// Using max lengths: 255 - 23 - 1 - 23 - 1 = 207, rounded down for safety
constexpr size_t MAX_MESSAGE_LEN = 200;
// Parsed message for application use
struct ParsedMessage {
uint32_t senderId;
uint32_t targetId;
std::string senderName;
std::string target;
std::string message;
};
/** Serialize a text message into wire format.
* @param senderId Sender's unique ID
* @param targetId Recipient ID (0 for broadcast/channel)
* @param senderName Sender's display name
* @param target Channel name (e.g. "#general") or empty for broadcast
* @param message The message text
* @param out Output buffer (will be resized to fit)
* @return true on success, false if inputs exceed limits
*/
bool serializeTextMessage(uint32_t senderId, uint32_t targetId,
const std::string& senderName, const std::string& target,
const std::string& message, std::vector<uint8_t>& out);
/** Deserialize a received buffer into a ParsedMessage.
* @param data Raw received data
* @param length Length of received data
* @param out Parsed message output
* @return true if valid (correct magic, version, and format)
*/
bool deserializeMessage(const uint8_t* data, size_t length, ParsedMessage& out);
/** Get maximum message length for current ESP-NOW version.
* Accounts for header + nickname + target overhead. */
size_t getMaxMessageLength(size_t nicknameLen, size_t targetLen);
} // namespace tt::app::chat
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED
@@ -0,0 +1,34 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
#include <array>
#include <cstdint>
#include <string>
#include <esp_now.h>
namespace tt::app::chat {
constexpr auto* CHAT_SETTINGS_FILE = "/data/settings/chat.properties";
struct ChatSettingsData {
uint32_t senderId = 0; // Unique device ID (randomly generated on first launch)
std::string nickname = "Device";
std::array<uint8_t, ESP_NOW_KEY_LEN> encryptionKey = {};
bool hasEncryptionKey = false;
std::string chatChannel = "#general";
};
ChatSettingsData loadSettings();
bool saveSettings(const ChatSettingsData& settings);
ChatSettingsData getDefaultSettings();
bool settingsFileExists();
} // namespace tt::app::chat
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED
@@ -0,0 +1,59 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
#include <Tactility/RecursiveMutex.h>
#include <cstddef>
#include <deque>
#include <string>
#include <vector>
namespace tt::app::chat {
constexpr size_t MAX_MESSAGES = 100;
struct StoredMessage {
std::string displayText;
std::string target; // for channel filtering
bool isOwn;
};
/** Thread safety: All public methods are mutex-protected.
* LVGL sync lock must be held separately when updating UI. */
class ChatState {
mutable RecursiveMutex mutex;
std::deque<StoredMessage> messages;
std::string currentChannel = "#general";
std::string localNickname = "Device";
public:
ChatState() = default;
~ChatState() = default;
ChatState(const ChatState&) = delete;
ChatState& operator=(const ChatState&) = delete;
ChatState(ChatState&&) = delete;
ChatState& operator=(ChatState&&) = delete;
void setLocalNickname(const std::string& nickname);
std::string getLocalNickname() const;
void setCurrentChannel(const std::string& channel);
std::string getCurrentChannel() const;
void addMessage(const StoredMessage& msg);
/** Returns messages matching the current channel (or broadcast). */
std::vector<StoredMessage> getFilteredMessages() const;
};
} // namespace tt::app::chat
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED
@@ -0,0 +1,79 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
#include "ChatState.h"
#include "ChatSettings.h"
#include <Tactility/app/AppContext.h>
#include <esp_now.h>
#include <lvgl.h>
namespace tt::app::chat {
class ChatApp;
class ChatView {
ChatApp* app;
ChatState* state;
lv_obj_t* toolbar = nullptr;
lv_obj_t* msgList = nullptr;
lv_obj_t* inputWrapper = nullptr;
lv_obj_t* inputField = nullptr;
// Settings panel widgets
lv_obj_t* settingsPanel = nullptr;
lv_obj_t* nicknameInput = nullptr;
lv_obj_t* keyInput = nullptr;
// Channel selector panel widgets
lv_obj_t* channelPanel = nullptr;
lv_obj_t* channelInput = nullptr;
void createInputBar(lv_obj_t* parent);
void createSettingsPanel(lv_obj_t* parent);
void createChannelPanel(lv_obj_t* parent);
void updateToolbarTitle();
static void addMessageToList(lv_obj_t* msgList, const StoredMessage& msg);
static void onSendClicked(lv_event_t* e);
static void onSettingsClicked(lv_event_t* e);
static void onSettingsSave(lv_event_t* e);
static void onSettingsCancel(lv_event_t* e);
static void onChannelClicked(lv_event_t* e);
static void onChannelSave(lv_event_t* e);
static void onChannelCancel(lv_event_t* e);
public:
ChatView(ChatApp* app, ChatState* state) : app(app), state(state) {}
~ChatView() = default;
ChatView(const ChatView&) = delete;
ChatView& operator=(const ChatView&) = delete;
ChatView(ChatView&&) = delete;
ChatView& operator=(ChatView&&) = delete;
void init(AppContext& appContext, lv_obj_t* parent);
void displayMessage(const StoredMessage& msg);
void refreshMessageList();
void showSettings(const ChatSettingsData& current);
void hideSettings();
void showChannelSelector();
void hideChannelSelector();
};
} // namespace tt::app::chat
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED
@@ -31,6 +31,7 @@ class EspNowService final : public Service {
std::vector<ReceiverSubscriptionData> subscriptions;
ReceiverSubscription lastSubscriptionId = 0;
bool enabled = false;
uint32_t espnowVersion = 0;
// Dispatcher calls this and forwards to non-static function
void enableFromDispatcher(const EspNowConfig& config);
@@ -65,6 +66,8 @@ public:
void unsubscribeReceiver(ReceiverSubscription subscription);
uint32_t getVersion() const;
// region Internal API
};