Chat app update, EspNow v2 & GPS Info (#460)
This commit is contained in:
@@ -4,141 +4,179 @@
|
||||
|
||||
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
||||
|
||||
#include <Tactility/app/chat/ChatAppPrivate.h>
|
||||
#include <Tactility/app/chat/ChatProtocol.h>
|
||||
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/Assets.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/service/espnow/EspNow.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
|
||||
#include "Tactility/lvgl/LvglSync.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <esp_wifi.h>
|
||||
#include <lvgl.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::chat {
|
||||
|
||||
static const auto LOGGER = Logger("ChatApp");
|
||||
constexpr uint8_t BROADCAST_ADDRESS[ESP_NOW_ETH_ALEN] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
static constexpr uint8_t BROADCAST_ADDRESS[ESP_NOW_ETH_ALEN] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
|
||||
class ChatApp : public App {
|
||||
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);
|
||||
}
|
||||
|
||||
lv_obj_t* msg_list = nullptr;
|
||||
lv_obj_t* input_field = nullptr;
|
||||
service::espnow::ReceiverSubscription receiveSubscription;
|
||||
|
||||
void addMessage(const char* message) {
|
||||
lv_obj_t* msg_label = lv_label_create(msg_list);
|
||||
lv_label_set_text(msg_label, message);
|
||||
lv_obj_set_width(msg_label, lv_pct(100));
|
||||
lv_label_set_long_mode(msg_label, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_align(msg_label, LV_TEXT_ALIGN_LEFT, 0);
|
||||
lv_obj_set_style_pad_all(msg_label, 2, 0);
|
||||
lv_obj_scroll_to_y(msg_list, lv_obj_get_scroll_y(msg_list) + 20, LV_ANIM_ON);
|
||||
void ChatApp::disableEspNow() {
|
||||
if (service::espnow::isEnabled()) {
|
||||
service::espnow::disable();
|
||||
}
|
||||
}
|
||||
|
||||
static void onSendClicked(lv_event_t* e) {
|
||||
auto* self = static_cast<ChatApp*>(lv_event_get_user_data(e));
|
||||
auto* msg = lv_textarea_get_text(self->input_field);
|
||||
const auto msg_len = strlen(msg);
|
||||
|
||||
if (self->msg_list && msg && msg_len) {
|
||||
self->addMessage(msg);
|
||||
|
||||
if (!service::espnow::send(BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(msg), msg_len)) {
|
||||
LOGGER.error("Failed to send message");
|
||||
}
|
||||
|
||||
lv_textarea_set_text(self->input_field, "");
|
||||
}
|
||||
void ChatApp::onCreate(AppContext& appContext) {
|
||||
isFirstLaunch = !settingsFileExists();
|
||||
settings = loadSettings();
|
||||
state.setLocalNickname(settings.nickname);
|
||||
if (!settings.chatChannel.empty()) {
|
||||
state.setCurrentChannel(settings.chatChannel);
|
||||
}
|
||||
enableEspNow();
|
||||
|
||||
void onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) {
|
||||
// Append \0 to make it a string
|
||||
auto buffer = static_cast<char*>(malloc(length + 1));
|
||||
memcpy(buffer, data, length);
|
||||
buffer[length] = 0x00;
|
||||
const std::string message_prefixed = std::string("Received: ") + buffer;
|
||||
|
||||
lvgl::getSyncLock()->lock();
|
||||
addMessage(message_prefixed.c_str());
|
||||
lvgl::getSyncLock()->unlock();
|
||||
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onCreate(AppContext& appContext) override {
|
||||
// TODO: Move this to a configuration screen/app
|
||||
static const uint8_t key[ESP_NOW_KEY_LEN] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
auto config = service::espnow::EspNowConfig(
|
||||
const_cast<uint8_t*>(key),
|
||||
service::espnow::Mode::Station,
|
||||
1,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
service::espnow::enable(config);
|
||||
|
||||
receiveSubscription = service::espnow::subscribeReceiver([this](const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) {
|
||||
receiveSubscription = service::espnow::subscribeReceiver(
|
||||
[this](const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) {
|
||||
onReceive(receiveInfo, data, length);
|
||||
});
|
||||
}
|
||||
|
||||
void onDestroy(AppContext& appContext) override {
|
||||
service::espnow::unsubscribeReceiver(receiveSubscription);
|
||||
|
||||
if (service::espnow::isEnabled()) {
|
||||
service::espnow::disable();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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<size_t>(length), parsed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
void onShow(AppContext& context, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
StoredMessage msg;
|
||||
msg.displayText = parsed.senderName + ": " + parsed.message;
|
||||
msg.target = parsed.target;
|
||||
msg.isOwn = false;
|
||||
|
||||
lvgl::toolbar_create(parent, context);
|
||||
state.addMessage(msg);
|
||||
|
||||
// Message list
|
||||
msg_list = lv_list_create(parent);
|
||||
lv_obj_set_flex_grow(msg_list, 1);
|
||||
lv_obj_set_width(msg_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(msg_list, 1);
|
||||
lv_obj_set_style_bg_color(msg_list, lv_color_hex(0x262626), 0);
|
||||
lv_obj_set_style_border_width(msg_list, 1, 0);
|
||||
lv_obj_set_style_pad_ver(msg_list, 0, 0);
|
||||
lv_obj_set_style_pad_hor(msg_list, 4, 0);
|
||||
{
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
lock.lock();
|
||||
view.displayMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Input panel
|
||||
auto* bottom_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_flex_flow(bottom_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_size(bottom_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(bottom_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_column(bottom_wrapper, 4, 0);
|
||||
lv_obj_set_style_border_opa(bottom_wrapper, 0, LV_STATE_DEFAULT);
|
||||
void ChatApp::sendMessage(const std::string& text) {
|
||||
if (text.empty()) return;
|
||||
|
||||
// Input field
|
||||
input_field = lv_textarea_create(bottom_wrapper);
|
||||
lv_obj_set_flex_grow(input_field, 1);
|
||||
lv_textarea_set_placeholder_text(input_field, "Type a message...");
|
||||
lv_textarea_set_one_line(input_field, true);
|
||||
std::string nickname = state.getLocalNickname();
|
||||
std::string channel = state.getCurrentChannel();
|
||||
|
||||
// Send button
|
||||
auto* send_btn = lv_button_create(bottom_wrapper);
|
||||
lv_obj_set_style_margin_all(send_btn, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_margin_top(send_btn, 2, LV_STATE_DEFAULT); // Hack to fix alignment
|
||||
lv_obj_add_event_cb(send_btn, onSendClicked, LV_EVENT_CLICKED, this);
|
||||
|
||||
auto* btn_label = lv_label_create(send_btn);
|
||||
lv_label_set_text(btn_label, "Send");
|
||||
lv_obj_center(btn_label);
|
||||
std::vector<uint8_t> wireMsg;
|
||||
if (!serializeTextMessage(settings.senderId, BROADCAST_ID, nickname, channel, text, wireMsg)) {
|
||||
LOGGER.error("Failed to serialize message");
|
||||
return;
|
||||
}
|
||||
|
||||
~ChatApp() override = default;
|
||||
};
|
||||
if (!service::espnow::send(BROADCAST_ADDRESS, wireMsg.data(), wireMsg.size())) {
|
||||
LOGGER.error("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<uint8_t>(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 {
|
||||
LOGGER.warn("Invalid hex characters in encryption key");
|
||||
}
|
||||
} else if (keyHex.empty()) {
|
||||
if (settings.hasEncryptionKey) {
|
||||
settings.encryptionKey.fill(0);
|
||||
settings.hasEncryptionKey = false;
|
||||
needRestart = true;
|
||||
}
|
||||
} else {
|
||||
LOGGER.warn("Key must be exactly {} hex characters, got {}", ESP_NOW_KEY_LEN * 2, 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",
|
||||
@@ -147,6 +185,6 @@ extern const AppManifest manifest = {
|
||||
.createApp = create<ChatApp>
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace tt::app::chat
|
||||
|
||||
#endif // CONFIG_SOC_WIFI_SUPPORTED && !CONFIG_SLAVE_SOC_WIFI_SUPPORTED
|
||||
|
||||
Reference in New Issue
Block a user