-
-
Notifications
You must be signed in to change notification settings - Fork 78
Chat app update, EspNow v2 & GPS Info #460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
KenVanHoeylandt
merged 10 commits into
TactilityProject:main
from
Shadowtrance:chat-update-gps
Jan 26, 2026
+1,542
−119
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
de9fb01
Chat app update, EspNow v2 & GPS Info
Shadowtrance bdf60e2
Fixes, Adjustments
Shadowtrance 22d1d51
Update GpsService.cpp
Shadowtrance 4a2fe73
and some more...
Shadowtrance 2666b73
and another
Shadowtrance dc2d50f
and some more!
Shadowtrance 95f0c51
Update ChatProtocol.h
Shadowtrance c9d457d
protocol v2
Shadowtrance ca8cf0b
updoot
Shadowtrance 2da1f61
Update chat.md
Shadowtrance File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| # Chat App | ||
|
|
||
| ESP-NOW-based chat application with channel-based messaging. Devices with the same encryption key can communicate in real-time without requiring a WiFi access point or internet connection. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Channel-based messaging**: Join named channels (e.g. `#general`, `#random`) to organize conversations | ||
| - **Broadcast support**: Messages with empty target are visible in all channels | ||
| - **Configurable nickname**: Identify yourself with a custom name (max 23 characters) | ||
| - **Unique sender ID**: Each device gets a random 32-bit ID on first launch for future DM support | ||
| - **Encryption key**: Optional shared key for private group communication | ||
| - **Persistent settings**: Sender ID, nickname, key, and current chat channel are saved across reboots | ||
|
|
||
| ## Requirements | ||
|
|
||
| - ESP32 with WiFi support (not available on ESP32-P4) | ||
| - ESP-NOW service enabled | ||
|
|
||
| ## UI Layout | ||
|
|
||
| ```text | ||
| +------------------------------------------+ | ||
| | [Back] Chat: #general [List] [Gear] | | ||
| +------------------------------------------+ | ||
| | alice: hello everyone | | ||
| | bob: hey alice! | | ||
| | You: hi there | | ||
| | (scrollable message list) | | ||
| +------------------------------------------+ | ||
| | [____input textarea____] [Send] | | ||
| +------------------------------------------+ | ||
| ``` | ||
|
|
||
| - **Toolbar title**: Shows `Chat: <channel>` with the current channel name | ||
| - **List icon**: Opens channel selector to switch channels | ||
| - **Gear icon**: Opens settings panel (nickname, encryption key) | ||
| - **Message list**: Shows messages matching the current channel or broadcast messages | ||
| - **Input bar**: Type and send messages to the current channel | ||
|
|
||
| ## Channel Selector | ||
|
|
||
| Tap the list icon to change channels. Enter a channel name (e.g. `#general`, `#team1`) and press OK. The message list refreshes to show only messages matching the new channel. | ||
|
|
||
| Messages are sent with the current channel as the target. Only devices viewing the same channel will display the message. Broadcast messages (empty target) appear in all channels. | ||
|
|
||
| ## First Launch | ||
|
|
||
| On first launch (when no settings file exists), the settings panel opens automatically so users can configure their nickname before chatting. A unique sender ID is also generated using the hardware RNG. | ||
|
|
||
| ## Settings | ||
|
|
||
| Tap the gear icon to configure: | ||
|
|
||
| | Setting | Description | Default | | ||
| |---------|-------------|---------| | ||
| | Nickname | Your display name (max 23 chars) | `Device` | | ||
| | Key | Encryption key as 32 hex characters (16 bytes) | All zeros (empty field) | | ||
|
|
||
| Settings are stored in `/data/settings/chat.properties`. The encryption key is stored encrypted using AES-256-CBC. The sender ID is stored as a decimal number. | ||
|
|
||
| When the key field is left empty, the default all-zeros key is used. All devices using the default key can communicate without configuration. | ||
|
|
||
| Changing the encryption key causes ESP-NOW to restart with the new configuration. | ||
|
|
||
| ## Wire Protocol v2 | ||
|
|
||
| Compact variable-length packets broadcast over ESP-NOW: | ||
|
|
||
| ### Header (16 bytes) | ||
|
|
||
| ```text | ||
| Offset Size Field | ||
| ------ ---- ----- | ||
| 0 4 magic (0x54435432 "TCT2") | ||
| 4 2 protocol_version (2) | ||
| 6 4 from (sender ID, random uint32) | ||
| 10 4 to (recipient ID, 0 = broadcast/channel) | ||
| 14 1 payload_type (1 = TextMessage) | ||
| 15 1 payload_size (length of payload) | ||
| ``` | ||
|
|
||
| ### Text Message Payload (variable) | ||
|
|
||
| ```text | ||
| [nickname\0][target\0][message bytes] | ||
| ``` | ||
|
|
||
| - `nickname`: Null-terminated sender display name (2-23 chars + null; single-letter names rejected) | ||
| - `target`: Null-terminated channel or empty for broadcast (0-23 chars + null) | ||
| - Empty string (`\0`): broadcast to all channels | ||
| - Channel name (e.g. `#general`): visible only when viewing that channel | ||
| - `message`: Remaining bytes, NOT null-terminated, minimum 1 byte (length = `payload_size - strlen(nickname) - 1 - strlen(target) - 1`) | ||
|
|
||
| **Minimum packet size for TextMessage:** 16 (header) + 2 (min nickname) + 1 (null) + 0 (empty target) + 1 (null) + 1 (min message) = **21 bytes** | ||
|
|
||
| **Example calculation:** If nickname is "Alice" (5 chars) and target is "#general" (8 chars): | ||
| - Overhead: 5 + 1 + 8 + 1 = 15 bytes | ||
| - Max message: 255 - 15 = 240 bytes | ||
|
|
||
| ### Example | ||
|
|
||
| "Alice" sends "Hi!" to #general: | ||
| - Header: 16 bytes | ||
| - Payload: `Alice\0#general\0Hi!` = 18 bytes | ||
| - **Total: 34 bytes** | ||
|
|
||
| ### Size Limits | ||
|
|
||
| | Constraint | Min | Max | | ||
| |------------|-----|-----| | ||
| | Header size | 16 bytes | 16 bytes | | ||
| | Payload (uint8_t) | 5 bytes | 255 bytes | | ||
| | Nickname | 2 characters | 23 characters | | ||
| | Channel/target | 0 (broadcast) | 23 characters | | ||
| | Message (wire) | 1 byte | up to 251 bytes (varies by overhead) | | ||
| | Message (UI) | 1 character | 200 characters | | ||
| | Total packet (TextMessage) | 21 bytes | 271 bytes | | ||
|
|
||
| ### Payload Types | ||
|
|
||
| | Type | Value | Description | | ||
| |------|-------|-------------| | ||
| | TextMessage | 1 | Chat message with nickname, target, and text | | ||
| | (reserved) | 2+ | Future: Position, Telemetry, etc. | | ||
|
|
||
| ### Target Field Semantics | ||
|
|
||
| | `to` Value | `target` Field | Meaning | | ||
| |------------|----------------|---------| | ||
| | 0 | `""` (empty) | Broadcast - visible in all channels | | ||
| | 0 | `#channel` | Channel message - visible only when viewing that channel | | ||
| | non-zero | `nickname` | Direct message (future - requires address discovery protocol) | | ||
|
|
||
| Messages with incorrect magic/version or invalid payload are silently discarded. | ||
|
|
||
| > **Note:** Direct messaging (non-zero `to`) will require an address discovery mechanism, such as periodic broadcasts announcing nickname→sender_id mappings, before devices can address each other directly. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ```text | ||
| ChatApp - App lifecycle, ESP-NOW send/receive, settings management | ||
| ChatState - Message storage (deque, max 100), channel filtering, mutex-protected | ||
| ChatView - LVGL UI: toolbar, message list, input bar, settings/channel panels | ||
| ChatProtocol - MessageHeader struct, serialize/deserialize, PayloadType enum | ||
| ChatSettings - Properties file load/save with encrypted key storage, sender ID generation | ||
| ``` | ||
|
|
||
| All files are guarded with `#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)` to exclude from P4 builds. | ||
|
|
||
| ## Message Flow | ||
|
|
||
| ### Sending | ||
|
|
||
| 1. User types message and taps Send | ||
| 2. `serializeTextMessage()` builds compact packet with sender ID, nickname, channel, message | ||
| 3. Broadcast via ESP-NOW to nearby devices | ||
| 4. Own message stored and displayed locally | ||
|
|
||
| ### Receiving | ||
|
|
||
| 1. ESP-NOW callback fires with raw data | ||
| 2. Validate packet: | ||
| - Minimum size: 21 bytes (16 header + 2 min nickname + 1 null + 0 min target + 1 null + 1 min message) | ||
| - Magic bytes: must be `0x54435432` ("TCT2") | ||
| - Protocol version: must be 2 | ||
| - Payload size: `header.payload_size` must equal `received_length - 16` | ||
| 3. Parse null-terminated nickname and target from payload | ||
| 4. Validate minimum lengths: nickname >= 2 chars, message >= 1 byte | ||
| 5. Extract message from remaining bytes (length derived from payload_size) | ||
| 6. Store in message deque with sender ID | ||
| 7. Display if target matches current channel or is broadcast (empty) | ||
|
|
||
| ## Limitations | ||
|
|
||
| - Maximum 100 stored messages (oldest discarded when full) | ||
| - Nickname: 23 characters max | ||
| - Channel name: 23 characters max | ||
| - Message text: 200 characters max (UI limit; actual wire limit varies by nickname/target length) | ||
| - No message persistence across app restarts (messages are in-memory only) | ||
| - All communication is broadcast; channel filtering is client-side only | ||
| - Sender ID collisions: 32-bit random IDs have ~50% collision probability at ~77,000 active devices (birthday paradox); no collision detection/resolution implemented | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| The chat protocol relies on ESP-NOW's built-in encryption (when configured) but has additional security limitations: | ||
|
|
||
| - **No message authentication**: No MAC/HMAC to verify message integrity or sender authenticity beyond the sender ID | ||
| - **No replay protection**: No sequence numbers or timestamps; messages can be replayed | ||
| - **Sender ID spoofing**: Any device knowing the encryption key can forge messages with arbitrary sender IDs | ||
| - **No forward secrecy**: Compromise of the shared key exposes all past and future messages | ||
|
|
||
| These tradeoffs are acceptable for casual local communication but should be understood before using for sensitive applications. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.