Skip to content

Commit de9fb01

Browse files
committed
Chat app update, EspNow v2 & GPS Info
1 parent 96eccbd commit de9fb01

File tree

19 files changed

+1379
-113
lines changed

19 files changed

+1379
-113
lines changed

Documentation/chat.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Chat App
2+
3+
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.
4+
5+
## Features
6+
7+
- **Channel-based messaging**: Join named channels (e.g. `#general`, `#random`) to organize conversations
8+
- **Broadcast support**: Messages with empty target are visible in all channels
9+
- **Configurable nickname**: Identify yourself with a custom name (max 23 characters)
10+
- **Encryption key**: Optional shared key for private group communication
11+
- **Persistent settings**: Nickname, key, and current chat channel are saved across reboots
12+
13+
## Requirements
14+
15+
- ESP32 with WiFi support (not available on ESP32-P4)
16+
- ESP-NOW service enabled
17+
18+
## UI Layout
19+
20+
```
21+
+------------------------------------------+
22+
| [Back] Chat: #general [List] [Gear] |
23+
+------------------------------------------+
24+
| alice: hello everyone |
25+
| bob: hey alice! |
26+
| You: hi there |
27+
| (scrollable message list) |
28+
+------------------------------------------+
29+
| [____input textarea____] [Send] |
30+
+------------------------------------------+
31+
```
32+
33+
- **Toolbar title**: Shows `Chat: <channel>` with the current channel name
34+
- **List icon**: Opens channel selector to switch channels
35+
- **Gear icon**: Opens settings panel (nickname, encryption key)
36+
- **Message list**: Shows messages matching the current channel or broadcast messages
37+
- **Input bar**: Type and send messages to the current channel
38+
39+
## Channel Selector
40+
41+
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.
42+
43+
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.
44+
45+
## First Launch
46+
47+
On first launch (when no settings file exists), the settings panel opens automatically so users can configure their nickname before chatting.
48+
49+
## Settings
50+
51+
Tap the gear icon to configure:
52+
53+
| Setting | Description | Default |
54+
|---------|-------------|---------|
55+
| Nickname | Your display name (max 23 chars) | `Device` |
56+
| Key | Encryption key as 32 hex characters (16 bytes) | All zeros (empty field) |
57+
58+
Settings are stored in `/data/settings/chat.properties`. The encryption key is stored encrypted using AES-256-CBC.
59+
60+
When the key field is left empty, the default all-zeros key is used. All devices using the default key can communicate without configuration.
61+
62+
Changing the encryption key causes ESP-NOW to restart with the new configuration.
63+
64+
## Wire Protocol
65+
66+
Variable-length packed struct broadcast over ESP-NOW (ESP-NOW v2.0):
67+
68+
```
69+
Offset Size Field
70+
------ ---- -----
71+
0 4 header (magic: 0x31544354 "TCT1")
72+
4 1 protocol_version (0x01)
73+
5 24 sender_name (null-terminated, zero-padded)
74+
29 24 target (null-terminated, zero-padded)
75+
53 1-1417 message (null-terminated, variable length)
76+
```
77+
78+
- **Minimum packet**: 54 bytes (header + 1 byte message)
79+
- **Maximum packet**: 1470 bytes (ESP-NOW v2.0 limit)
80+
- **v1.0 compatibility**: Messages < 250 bytes work with ESP-NOW v1.0 devices
81+
82+
Messages with incorrect magic/version or invalid length are silently discarded.
83+
84+
### Target Field Semantics
85+
86+
| Target Value | Meaning |
87+
|-------------|---------|
88+
| `""` (empty) | Broadcast - visible in all channels |
89+
| `#channel` | Channel message - visible only when viewing that channel |
90+
| `username` | Direct message |
91+
92+
## Architecture
93+
94+
```
95+
ChatApp - App lifecycle, ESP-NOW send/receive, settings management
96+
ChatState - Message storage (deque, max 100), channel filtering, mutex-protected
97+
ChatView - LVGL UI: toolbar, message list, input bar, settings/channel panels
98+
ChatProtocol - Variable-length Message struct, serialize/deserialize (v2.0 support)
99+
ChatSettings - Properties file load/save with encrypted key storage
100+
```
101+
102+
All files are guarded with `#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)` to exclude from P4 builds.
103+
104+
## Message Flow
105+
106+
### Sending
107+
108+
1. User types message and taps Send
109+
2. Message serialized into `Message` struct with current nickname and channel as target
110+
3. Broadcast via ESP-NOW to nearby devices
111+
4. Own message stored and displayed locally
112+
113+
### Receiving
114+
115+
1. ESP-NOW callback fires with raw data
116+
2. Validate: size within valid range (54-1470 bytes), magic and version must match
117+
3. Copy into aligned local struct (avoids unaligned access on embedded platforms)
118+
4. Extract sender name, target, and message as strings
119+
5. Store in message deque
120+
6. Display if target matches current channel or is broadcast (empty)
121+
122+
## Limitations
123+
124+
- Maximum 100 stored messages (oldest discarded when full)
125+
- Nickname: 23 characters max
126+
- Channel name: 23 characters max
127+
- Message text: 1416 characters max (ESP-NOW v2.0)
128+
- No message persistence across app restarts (messages are in-memory only)
129+
- All communication is broadcast; channel filtering is client-side only
130+
- Messages > 250 bytes only received by devices running ESP-NOW v2.0

Documentation/espnow-v2.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# ESP-NOW v2.0 Support & Chat App Enhancements
2+
3+
## Summary
4+
5+
- Add ESP-NOW v2.0 support with version detection and larger payload capability
6+
- Update Chat app to use variable-length messages (up to 1416 characters)
7+
- Maintain backwards compatibility with ESP-NOW v1.0 devices
8+
9+
## ESP-NOW Service Changes
10+
11+
### New API
12+
13+
```cpp
14+
// Get the ESP-NOW protocol version (1 or 2)
15+
uint32_t espnow::getVersion();
16+
17+
// Get max payload size for current version (250 or 1470 bytes)
18+
size_t espnow::getMaxDataLength();
19+
20+
// Constants
21+
constexpr size_t MAX_DATA_LEN_V1 = 250;
22+
constexpr size_t MAX_DATA_LEN_V2 = 1470;
23+
```
24+
25+
### Version Detection
26+
27+
ESP-NOW version is queried on initialization and logged:
28+
```
29+
I (15620) ESPNOW: espnow [version: 2.0] init
30+
I [EspNowService] ESP-NOW version: 2.0
31+
```
32+
33+
### Files Modified
34+
35+
| File | Change |
36+
|------|--------|
37+
| `Tactility/Include/Tactility/service/espnow/EspNow.h` | Added constants and `getVersion()`, `getMaxDataLength()` |
38+
| `Tactility/Source/service/espnow/EspNow.cpp` | Implemented new functions |
39+
| `Tactility/Private/Tactility/service/espnow/EspNowService.h` | Added `espnowVersion` member |
40+
| `Tactility/Source/service/espnow/EspNowService.cpp` | Query version after init |
41+
42+
## Chat App Changes
43+
44+
### Larger Messages
45+
46+
- Message size increased from **127 to 1416 characters**
47+
- Variable-length packet transmission (only sends actual message length)
48+
- Backwards compatible: messages < 197 chars still work with v1.0 devices
49+
50+
### Wire Protocol
51+
52+
```
53+
Offset Size Field
54+
------ ---- -----
55+
0 4 header (magic: 0x31544354)
56+
4 1 protocol_version (0x01)
57+
5 24 sender_name
58+
29 24 target
59+
53 1-1417 message (variable length)
60+
```
61+
62+
- **Min packet**: 54 bytes
63+
- **Max packet**: 1470 bytes (v2.0 limit)
64+
65+
### Files Modified
66+
67+
| File | Change |
68+
|------|--------|
69+
| `Tactility/Private/Tactility/app/chat/ChatProtocol.h` | `MESSAGE_SIZE` = 1417, added header constants |
70+
| `Tactility/Source/app/chat/ChatProtocol.cpp` | Variable-length serialize/deserialize |
71+
| `Tactility/Source/app/chat/ChatApp.cpp` | Send actual packet size |
72+
| `Tactility/Source/app/chat/ChatView.cpp` | Input field max length = 1416 |
73+
| `Documentation/chat.md` | Updated protocol documentation |
74+
75+
## Compatibility
76+
77+
| Scenario | Result |
78+
|----------|--------|
79+
| v2.0 device sends short message (< 250 bytes) | v1.0 and v2.0 devices receive |
80+
| v2.0 device sends long message (> 250 bytes) | Only v2.0 devices receive |
81+
| v1.0 device sends message | v1.0 and v2.0 devices receive |
82+
83+
## Requirements
84+
85+
- ESP-IDF v5.4 or later (for ESP-NOW v2.0 support)
86+
87+
## Test Plan
88+
89+
- [ ] Verify "ESP-NOW version: 2.0" appears in logs on startup
90+
- [ ] Send short message between two v2.0 devices
91+
- [ ] Send long message (> 200 chars) between two v2.0 devices
92+
- [ ] Verify `espnow::getMaxDataLength()` returns 1470

Tactility/Include/Tactility/service/espnow/EspNow.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ namespace tt::service::espnow {
1616
typedef int ReceiverSubscription;
1717
constexpr ReceiverSubscription NO_SUBSCRIPTION = -1;
1818

19+
// ESP-NOW version payload limits
20+
constexpr size_t MAX_DATA_LEN_V1 = 250; // ESP-NOW v1.0 max payload
21+
constexpr size_t MAX_DATA_LEN_V2 = 1470; // ESP-NOW v2.0 max payload (requires ESP-IDF v5.4+)
22+
1923
enum class Mode {
2024
Station,
2125
AccessPoint
@@ -54,6 +58,12 @@ ReceiverSubscription subscribeReceiver(std::function<void(const esp_now_recv_inf
5458

5559
void unsubscribeReceiver(ReceiverSubscription subscription);
5660

61+
/** Get the ESP-NOW protocol version (1 for v1.0, 2 for v2.0). Returns 0 if service not running. */
62+
uint32_t getVersion();
63+
64+
/** Get the maximum data length for current ESP-NOW version (250 for v1.0, 1470 for v2.0). */
65+
size_t getMaxDataLength();
66+
5767
}
5868

5969
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED

Tactility/Include/Tactility/service/gps/GpsService.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class GpsService final : public Service {
2121
minmea_sentence_rmc rmcRecord;
2222
TickType_t rmcTime = 0;
2323

24+
minmea_sentence_gga ggaRecord;
25+
TickType_t ggaTime = 0;
26+
2427
RecursiveMutex mutex;
2528
Mutex stateMutex;
2629
std::vector<GpsDeviceRecord> deviceRecords;
@@ -58,6 +61,7 @@ class GpsService final : public Service {
5861

5962
bool hasCoordinates() const;
6063
bool getCoordinates(minmea_sentence_rmc& rmc) const;
64+
bool getGga(minmea_sentence_gga& gga) const;
6165

6266
/** @return GPS service pubsub that broadcasts State* objects */
6367
std::shared_ptr<PubSub<State>> getStatePubsub() const { return statePubSub; }
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#pragma once
2+
3+
#ifdef ESP_PLATFORM
4+
#include <sdkconfig.h>
5+
#endif
6+
7+
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
8+
9+
#include "ChatState.h"
10+
#include "ChatView.h"
11+
#include "ChatSettings.h"
12+
13+
#include <Tactility/app/App.h>
14+
#include <Tactility/service/espnow/EspNow.h>
15+
16+
namespace tt::app::chat {
17+
18+
class ChatApp final : public App {
19+
20+
ChatState state;
21+
ChatView view = ChatView(this, &state);
22+
service::espnow::ReceiverSubscription receiveSubscription = -1;
23+
ChatSettingsData settings;
24+
bool isFirstLaunch = false;
25+
26+
void onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length);
27+
void enableEspNow();
28+
void disableEspNow();
29+
30+
public:
31+
void onCreate(AppContext& appContext) override;
32+
void onDestroy(AppContext& appContext) override;
33+
void onShow(AppContext& context, lv_obj_t* parent) override;
34+
35+
void sendMessage(const std::string& text);
36+
void applySettings(const std::string& nickname, const std::string& keyHex);
37+
void switchChannel(const std::string& chatChannel);
38+
39+
const ChatSettingsData& getSettings() const { return settings; }
40+
41+
~ChatApp() override = default;
42+
};
43+
44+
} // namespace tt::app::chat
45+
46+
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#pragma once
2+
3+
#ifdef ESP_PLATFORM
4+
#include <sdkconfig.h>
5+
#endif
6+
7+
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
8+
9+
#include <cstddef>
10+
#include <cstdint>
11+
#include <string>
12+
13+
namespace tt::app::chat {
14+
15+
constexpr uint32_t CHAT_MAGIC_HEADER = 0x31544354; // "TCT1"
16+
constexpr uint8_t PROTOCOL_VERSION = 0x01;
17+
constexpr size_t SENDER_NAME_SIZE = 24;
18+
constexpr size_t TARGET_SIZE = 24;
19+
constexpr size_t MESSAGE_SIZE = 1417; // Max for ESP-NOW v2.0 (1470 - 53 header bytes)
20+
21+
// Header size = offset to message field (header + version + sender_name + target)
22+
constexpr size_t MESSAGE_HEADER_SIZE = 4 + 1 + SENDER_NAME_SIZE + TARGET_SIZE; // 53 bytes
23+
constexpr size_t MIN_PACKET_SIZE = MESSAGE_HEADER_SIZE + 1; // At least 1 byte of message
24+
25+
struct __attribute__((packed)) Message {
26+
uint32_t header;
27+
uint8_t protocol_version;
28+
char sender_name[SENDER_NAME_SIZE];
29+
char target[TARGET_SIZE]; // empty=broadcast, "#channel" or "username"
30+
char message[MESSAGE_SIZE];
31+
};
32+
33+
static_assert(sizeof(Message) == 1470, "Message struct must be exactly ESP-NOW v2.0 max payload");
34+
static_assert(MESSAGE_HEADER_SIZE == offsetof(Message, message), "Header size calculation mismatch");
35+
36+
struct ParsedMessage {
37+
std::string senderName;
38+
std::string target;
39+
std::string message;
40+
};
41+
42+
/** Serialize fields into the wire format.
43+
* Returns the actual packet size to send (variable length), or 0 on failure.
44+
* Short messages (< 250 bytes total) are compatible with ESP-NOW v1.0 devices. */
45+
size_t serializeMessage(const std::string& senderName, const std::string& target,
46+
const std::string& message, Message& out);
47+
48+
/** Deserialize a received buffer into a ParsedMessage.
49+
* Returns true if valid (correct magic, version, and minimum length). */
50+
bool deserializeMessage(const uint8_t* data, int length, ParsedMessage& out);
51+
52+
} // namespace tt::app::chat
53+
54+
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED

0 commit comments

Comments
 (0)