Chat app update, EspNow v2 & GPS Info (#460)
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user