code cleanup. Moved the original mesh code to own file.

This commit is contained in:
my 2026-09-23 15:18:28 +02:00
parent 44f10676b3
commit bb6b0e4d73
9 changed files with 676 additions and 589 deletions

2
src/idf_component.yml Normal file
View file

@ -0,0 +1,2 @@
dependencies:
idf: '>=5.1'

View file

@ -0,0 +1,2 @@
dependencies:
idf: '>=5.1'

View file

@ -0,0 +1,375 @@
#include "EvilChatMesh.h"
#include "chat.h" // currentNick, appMode, chat_add_message()
#include "ui.h"
#include "sound.h"
#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
/*
============================================================================================================================
EvilChatMesh
============================================================================================================================
*/
/* ---------- VARIABLES GLOBALES ---------- */
std::vector<NodePresence> nodesConnected;
int nodesCount = 0;
static std::vector<std::array<char,9>> seenMessageIds;
int seenIndex = 0;
uint8_t broadcastAddressEspNow[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
// Replaced by the LVGL UI
//struct Message {
// char content[MAX_MSG_LEN];
//};
// static std::vector<Message> messages;
int messageCount = 0;
int messageHead = 0;
/* Timings */
unsigned long lastPing = 0;
/* ---------- BUFFER SÉCURISÉ POUR LE CALLBACK ---------- */
volatile bool newPacketPending = false;
volatile uint8_t packetBuffer[sizeof(MeshMessage)+sizeof(uint16_t)];
volatile int packetLen = 0;
// Sets up ESP-NOW, WiFi, and the presence/dedup state. Was inlined in
// chat_init(); pulled out here so seenMessageIds/nodesConnected stay
// private to this file.
void mesh_init() {
nodesConnected.clear(); nodesConnected.resize(MAX_NODES);
seenMessageIds.resize(MAX_HISTORY);
for (auto& e : seenMessageIds) e.fill(0);
WiFi.persistent(false);
WiFi.mode(WIFI_STA);
WiFi.setSleep(false);
if (esp_now_init() != ESP_OK) {
Serial.println("[mesh] ESP-NOW init failed");
ui_push_message("", "ESP-NOW init failed!", false, true);
return;
}
esp_now_register_recv_cb(OnDataRecvChat);
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, broadcastAddressEspNow, 6);
peer.channel = 1; // relay uses channel 1; original was 0
peer.encrypt = false;
if (!esp_now_is_peer_exist(peer.peer_addr))
esp_now_add_peer(&peer);
}
/* ---------- CALLBACK ESP‑NOW ULTRA‑COURT ---------- */
void IRAM_ATTR OnDataRecvChat(
const esp_now_recv_info_t* recv_info,
const uint8_t* incomingData, int len
//#else // IDF4 signature, [removed]
// const uint8_t* mac,
// const uint8_t* incomingData, int len
//#endif
)
{
const uint8_t* mac = recv_info ? recv_info->src_addr : nullptr;
(void)mac;
if (len > (int)sizeof(packetBuffer)) return; // paquet trop gros
memcpy((void*)packetBuffer, incomingData, len);
packetLen = len;
newPacketPending = true; // flag pour la loop()
}
// ----- Hash pour dé‑duplication -----
void simpleHash(const char* s, char* outHash) {
uint32_t h = 5381;
while (*s) h = ((h << 5) + h) + (uint8_t)(*s++);
snprintf(outHash, 9, "%08X", h); // 8 hex, zéro‑terminé
}
// ----- CRC16 (CRC‑CCITT) -----
uint16_t crc16(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; ++j)
crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : (crc << 1);
}
return crc;
}
// ----- Historique d'ID reçus -----
bool hasSeenId(const char* id) {
if ((int)seenMessageIds.size() < MAX_HISTORY) return false;
for (int i = 0; i < MAX_HISTORY; ++i)
if (strncmp(seenMessageIds[i].data(), id, 8) == 0) return true;
return false;
}
void rememberId(const char* id) {
if ((int)seenMessageIds.size() < MAX_HISTORY) seenMessageIds.resize(MAX_HISTORY);
strncpy(seenMessageIds[seenIndex].data(), id, 8);
seenMessageIds[seenIndex].data()[8] = '\0';
seenIndex = (seenIndex + 1) % MAX_HISTORY;
}
// ----- Buffer circulaire de messages pour l'écran -----
//void addMessage(const char* m) {
// if (messages.size() < 20) messages.assign(20, Message());
// strncpy(messages[messageHead].content, m, MAX_MSG_LEN - 1);
// messages[messageHead].content[MAX_MSG_LEN - 1] = '\0';
// messageHead = (messageHead + 1) % 20;
// if (messageCount < 20) messageCount++;
//}
// rafraîchit l'écran
//void drawChatWindow() {
// M5.Display.fillScreen(TFT_BLACK);
// M5.Display.setTextColor(TFT_GREEN);
// M5.Display.setCursor(5, 5);
// M5.Display.printf("Connected: %d", nodesCount);
//
// M5.Display.setTextColor(TFT_WHITE);
// M5.Display.setCursor(5, 10);
//
// const int lines = 13;
// int start = (messageCount < lines) ? 0 : (messageHead + 20 - lines) % 20;
// for (int i = 0; i < min(lines, messageCount); ++i) {
// int idx = (start + i) % 20;
// M5.Display.println(messages[idx].content);
// }
// M5.Display.display();
//}
/* ---------- Traitement d'un paquet en attente ---------- */
void handleIncomingPacket(const uint8_t* data, int len) {
if (len != (int)(sizeof(MeshMessage) + sizeof(uint16_t))) return;
uint16_t rxCrc;
memcpy(&rxCrc, data + sizeof(MeshMessage), sizeof(uint16_t));
if (rxCrc != crc16(data, sizeof(MeshMessage))) return;
MeshMessage msg;
memcpy(&msg, data, sizeof(MeshMessage));
if (hasSeenId(msg.id)) return;
rememberId(msg.id);
// --- Gestion du PING (mise à jour de présence) ---
if (strcmp(msg.body, "PING") == 0) {
updatePresence(msg.from);
esp_now_send(broadcastAddressEspNow, data, len);
return; // pas d'affichage
}
// --- Filtre messages "System" visant ce client ---
if (!strcmp(msg.from, "System") && strstr(msg.body, currentNick))
return;
// addMessage(line) + drawChatWindow() replaced:
//char line[MAX_MSG_LEN + 16];
//snprintf(line, sizeof(line), "%s: %s", msg.from, msg.body);
//addMessage(line);
//drawChatWindow();
bool is_system = !strcmp(msg.from, "System");
chat_add_message(is_system ? "" : msg.from, msg.body, false, is_system);
if (appMode == AppMode::CHAT)
ui_push_message(is_system ? "" : msg.from, msg.body, false, is_system);
is_system ? sound_system() : sound_msg_in();
esp_now_send(broadcastAddressEspNow, data, len);
}
/* ----- Envoi d'un message Chat (hors callback) ----- */
void broadcastChatMessage(const char* from, const char* body) {
MeshMessage msg;
char hash[9], tmp[MAX_MSG_LEN+32];
snprintf(tmp, sizeof(tmp), "%s%s%lu", from, body, millis());
simpleHash(tmp, hash);
strncpy(msg.id, hash, 8); msg.id[8] = '\0';
strncpy(msg.from, from, 15); msg.from[15] = '\0';
strncpy(msg.body, body, MAX_MSG_LEN-1); msg.body[MAX_MSG_LEN-1]='\0';
uint8_t pkt[sizeof(MeshMessage)+2];
memcpy(pkt, &msg, sizeof(MeshMessage));
uint16_t crc = crc16((uint8_t*)&msg, sizeof(MeshMessage));
memcpy(pkt + sizeof(MeshMessage), &crc, 2);
esp_now_send(broadcastAddressEspNow, pkt, sizeof(pkt));
rememberId(msg.id);
bool is_system = !strcmp(from, "System");
bool is_own = !is_system && !strcmp(from, currentNick);
chat_add_message(is_system ? "" : from, body, is_own, is_system);
if (appMode == AppMode::CHAT)
ui_push_message(is_system ? "" : from, body, is_own, is_system);
}
/* ----- Ping périodique ----- */
void broadcastPing() {
MeshMessage msg;
char hash[9], tmp[32];
// hash = from + "PING" + horodatage
snprintf(tmp, sizeof(tmp), "%sPING%lu", currentNick, millis());
simpleHash(tmp, hash);
strncpy(msg.id, hash, 8); msg.id[8] = '\0';
strncpy(msg.from, currentNick, 15); msg.from[15] = '\0';
strncpy(msg.body, "PING", MAX_MSG_LEN - 1);
msg.body[MAX_MSG_LEN - 1] = '\0';
uint8_t pkt[sizeof(MeshMessage) + 2];
memcpy(pkt, &msg, sizeof(MeshMessage));
uint16_t crc = crc16((uint8_t*)&msg, sizeof(MeshMessage));
memcpy(pkt + sizeof(MeshMessage), &crc, 2);
esp_now_send(broadcastAddressEspNow, pkt, sizeof(pkt));
rememberId(msg.id); // pour ignorer notre propre écho
}
/* ----- Gestion de présence ----- */
void updatePresence(const char* nick) {
for (int i = 0; i < nodesCount; ++i)
if (!strcmp(nodesConnected[i].nick, nick)) {
nodesConnected[i].lastSeen = millis(); return;
}
if (nodesCount < MAX_NODES) {
if ((int)nodesConnected.size() < MAX_NODES) nodesConnected.resize(MAX_NODES);
strncpy(nodesConnected[nodesCount].nick, nick, 15);
nodesConnected[nodesCount].nick[15] = '\0';
nodesConnected[nodesCount].lastSeen = millis();
++nodesCount;
}
}
void checkPresenceTimeouts() {
unsigned long now = millis();
for (int i = 0; i < nodesCount; ++i)
if ((now - nodesConnected[i].lastSeen) > TIMEOUT_PRESENCE) {
char m[MAX_MSG_LEN];
snprintf(m, sizeof(m), "%s left the chat!", nodesConnected[i].nick);
// broadcastChatMessage("System", m); // lets not broadcast this, but:
ui_push_message("", m, false, true);
for (int j = i; j < nodesCount-1; ++j)
nodesConnected[j] = nodesConnected[j+1];
--nodesCount; --i;
}
}
// ----- Gestion clavier & saisie utilisateur -----
// handleKeyboard() removed, M5Cardputer-specific; logic moved into chat_tick(char key)
//void handleKeyboard() {
// if (!kpAny()) return;
//
// Keyboard_Class::KeysState st = M5Cardputer.Keyboard.keysState();
//
// /* Ajout des caractères tapés */
// for (char c : st.word) {
// int n = strlen(inputBuffer);
// if (n < MAX_MSG_LEN - 2) { // -2 : place pour '\0'
// inputBuffer[n] = c;
// inputBuffer[n+1] = '\0';
// }
// }
// /* Backspace */
// if (st.del && *inputBuffer)
// inputBuffer[strlen(inputBuffer) - 1] = '\0';
//
// /* Entrée : envoi */
// if (st.enter && *inputBuffer) {
// if (inputBuffer[0] == '/')
// handleCommand(inputBuffer);
// else
// broadcastChatMessage(currentNick, inputBuffer);
//
// inputBuffer[0] = '\0';
// }
//
// /* Ligne d'édition */
// M5.Display.fillRect(0, 120, 240, 16, TFT_NAVY);
// M5.Display.setCursor(5, 120);
// M5.Display.setTextColor(TFT_YELLOW);
// M5.Display.print("> ");
// M5.Display.print(inputBuffer);
//}
// ----- Fonction principale -----
// EvilChatMesh() split into chat_init() + chat_tick(), no blocking while(true)
//void EvilChatMesh() {
// auto cfg = M5.config();
// M5.begin(cfg);
// M5.Display.setTextSize(1);
// M5.Display.setTextColor(TFT_WHITE);
// M5.Display.fillScreen(TFT_BLACK);
//
// WiFi.disconnect(true); delay(100);
// WiFi.mode(WIFI_STA);
//
// if (esp_now_init() != ESP_OK) {
// M5.Display.println("ESP‑NOW init failed"); return;
// }
// esp_now_register_recv_cb(OnDataRecvChat);
//
// esp_now_peer_info_t peer = {};
// memcpy(peer.peer_addr, broadcastAddressEspNow, 6);
// peer.channel = 0; // changed to 1, EvilChatMesh-Relay.ino expects channel 1
// peer.encrypt = false;
// if (!esp_now_is_peer_exist(peer.peer_addr))
// esp_now_add_peer(&peer);
//
// nodesConnected.clear(); nodesConnected.resize(MAX_NODES);
// messages.assign(20, Message());
// drawChatWindow();
//
// char msgArr[MAX_MSG_LEN];
// snprintf(msgArr, sizeof(msgArr), "%s enter the chat!", currentNick);
// broadcastChatMessage("System", msgArr);
//
// /* ---------- Boucle ---------- */
// while (true) {
// M5.update(); cardUpdate();
//
// /* 1. paquet reçu ? */
// if (newPacketPending) {
// noInterrupts();
// uint8_t localBuf[sizeof(MeshMessage)+2];
// int len = packetLen;
// memcpy(localBuf, (const void*)packetBuffer, len);
// newPacketPending = false;
// interrupts();
//
// handleIncomingPacket(localBuf, len);
// }
//
// /* 2. ping & timeouts */
// unsigned long now = millis();
// if (now - lastPing > PING_INTERVAL) {
// lastPing = now; broadcastPing();
// }
// checkPresenceTimeouts();
//
// /* 3. clavier */
// if (M5Cardputer.Keyboard.isChange())
// handleKeyboard();
//
// /* 4. touche ` pour quitter */
// if (kp('`')) {
// char m[MAX_MSG_LEN];
// snprintf(m, sizeof(m), "%s left the chat!", currentNick);
// broadcastChatMessage("System", m);
//
// esp_now_unregister_recv_cb();
// esp_now_deinit();
// waitAndReturnToMenu("Left the chat...");
// return;
// }
// delay(10);
// }
//}

View file

@ -0,0 +1,62 @@
#pragma once
// EvilChatMesh.cpp ported to ESP32 CYD + M5Stack CardKB
//
// Copied from Evil-Cardputer-v1-5-2.ino
// Original author : 7h30th3r0n3, https://github.com/7h30th3r0n3/Evil-M5Project
// I've tried to keep the original code as it was as much as possible,
// but –some- a lot ui/logic snuck in when adding the BT functonality.
// the mesh logic is unchanged.
#include <Arduino.h>
#include <esp_now.h>
#include <vector>
#include <array>
#define MAX_MSG_LEN 100
#define MAX_HISTORY 32
#define MAX_NODES 30
struct NodePresence {
char nick[16];
unsigned long lastSeen;
};
struct MeshMessage {
char id[9];
char from[16];
char body[MAX_MSG_LEN];
};
/* ---------- VARIABLES GLOBALES ---------- */
extern std::vector<NodePresence> nodesConnected;
extern int nodesCount;
extern uint8_t broadcastAddressEspNow[6];
/* Timings */
#define TIMEOUT_PRESENCE 2000
#define PING_INTERVAL 500
extern unsigned long lastPing;
/* ---------- BUFFER SÉCURISÉ POUR LE CALLBACK ---------- */
extern volatile bool newPacketPending;
extern volatile uint8_t packetBuffer[sizeof(MeshMessage) + sizeof(uint16_t)];
extern volatile int packetLen;
// Sets up ESP-NOW, WiFi, and the presence/dedup state. Call once from chat_init().
void mesh_init();
void IRAM_ATTR OnDataRecvChat(const esp_now_recv_info_t* recv_info,
const uint8_t* incomingData, int len);
void simpleHash(const char* s, char* outHash);
uint16_t crc16(const uint8_t* data, size_t len);
bool hasSeenId(const char* id);
void rememberId(const char* id);
void handleIncomingPacket(const uint8_t* data, int len);
void broadcastChatMessage(const char* from, const char* body);
void broadcastPing();
void updatePresence(const char* nick);
void checkPresenceTimeouts();

View file

@ -1,95 +1,43 @@
// chat.cpp, EvilChatMesh ported to ESP32 CYD + M5Stack CardKB
//
// Original source : Evil-Cardputer-v1-5-2.ino, lines 20480-20871
// Heavily based on Evil-Cardputer-v1-5-2.ino
// Original author : 7h30th3r0n3, https://github.com/7h30th3r0n3/Evil-M5Project
//
// I've tried to keep the original code as much as possible as it was
// but –some- a lot ui/logic snuck in when adding the BT functonality.
#include "chat.h"
#include "config.h"
#include "settings.h"
#include "ui.h"
#include "sound.h"
#include "EvilChatMesh.h"
#include "notes.h"
#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_sleep.h> // deep sleep
#include <driver/gpio.h> // gpio_hold_en keeps backlight off during sleep
#include <vector>
#include <array>
#include <LittleFS.h>
#include <BleKeyboard.h>
#include <NimBLEDevice.h>
/*
============================================================================================================================
EvilChatMesh
============================================================================================================================
*/
#define MAX_MSG_LEN 100
#define MAX_HISTORY 32
#define MAX_NODES 30
struct NodePresence {
char nick[16];
unsigned long lastSeen;
};
struct MeshMessage {
char id[9];
char from[16];
char body[MAX_MSG_LEN];
};
// Replaced by the LVGL UI
//struct Message {
// char content[MAX_MSG_LEN];
//};
/* ---------- VARIABLES GLOBALES ---------- */
static std::vector<NodePresence> nodesConnected;
int nodesCount = 0;
static std::vector<std::array<char,9>> seenMessageIds;
int seenIndex = 0;
/* GLOBALS */
char inputBuffer[MAX_MSG_LEN] = "";
uint8_t broadcastAddressEspNow[] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
// static std::vector<Message> messages;
int messageCount = 0;
int messageHead = 0;
// init from g_settings.nick
static char currentNick[16];
char currentNick[16];
/* ---------- Notes mode / BT dump ---------- */
enum class AppMode { CHAT, NOTES, BT };
static AppMode appMode = AppMode::CHAT;
static AppMode modeBeforeBt = AppMode::CHAT;
static BleKeyboard* g_ble = nullptr;
/* Notes mode / BT dump */
AppMode appMode = AppMode::CHAT;
// for redrawing
#define MODE_HISTORY_LINES 12
// buffer for chat messages
struct ChatHistEntry {
struct ChatMessage {
char nick[CHAT_MAX_NICK_LEN];
char text[MAX_MSG_LEN];
bool is_own;
bool is_system;
};
static ChatHistEntry chatHistory[MODE_HISTORY_LINES];
static ChatMessage chatHistory[MODE_HISTORY_LINES];
static int chatHistCount = 0;
static int chatHistNext = 0; // next write slot
static void chat_history_record(const char* nick, const char* text, bool is_own, bool is_system) {
ChatHistEntry& e = chatHistory[chatHistNext];
void chat_add_message(const char* nick, const char* text, bool is_own, bool is_system) {
ChatMessage& e = chatHistory[chatHistNext];
strncpy(e.nick, nick, sizeof(e.nick) - 1);
e.nick[sizeof(e.nick) - 1] = '\0';
strncpy(e.text, text, sizeof(e.text) - 1);
@ -100,303 +48,36 @@ static void chat_history_record(const char* nick, const char* text, bool is_own,
if (chatHistCount < MODE_HISTORY_LINES) chatHistCount++;
}
static void chat_show_recent() {
static void chat_draw() {
int start = (chatHistCount < MODE_HISTORY_LINES) ? 0 : chatHistNext;
for (int i = 0; i < chatHistCount; i++) {
ChatHistEntry& e = chatHistory[(start + i) % MODE_HISTORY_LINES];
ChatMessage& e = chatHistory[(start + i) % MODE_HISTORY_LINES];
ui_push_message(e.is_system ? "" : e.nick, e.text, e.is_own, e.is_system);
}
}
static char notesBuf[NOTES_HISTORY_LINES][MAX_MSG_LEN];
static int currNote = 0;// next pos
static int notesCount = 0;
static int notesScrollPos = 0;
static void notes_to_buffer(const char* line) {
strncpy(notesBuf[currNote], line, MAX_MSG_LEN - 1);
notesBuf[currNote][MAX_MSG_LEN - 1] = '\0';
currNote = (currNote + 1) % NOTES_HISTORY_LINES;
if (notesCount < NOTES_HISTORY_LINES) notesCount++;
}
static void notes_load() {
notesCount = 0; currNote = 0; notesScrollPos = 0;
File f = LittleFS.open(NOTES_FILE, "r");
if (!f) return;
char line[MAX_MSG_LEN];
while (f.available()) {
int n = f.readBytesUntil('\n', line, sizeof(line) - 1);
if (n > 0 && line[n - 1] == '\r')
n--; // remove trailing /r from .println
line[n] = '\0';
if (n == 0) continue;
notes_to_buffer(line);
}
f.close();
}
static void notes_render() {
int visible = UI_VISIBLE_LINES;
int maxScroll = (notesCount > visible) ? (notesCount - visible) : 0;
if (notesScrollPos > maxScroll) notesScrollPos = maxScroll;
if (notesScrollPos < 0) notesScrollPos = 0;
int windowEnd = notesCount - notesScrollPos;
int windowStart = windowEnd - visible;
if (windowStart < 0) windowStart = 0;
int start = (notesCount < NOTES_HISTORY_LINES) ? 0 : currNote;
ui_clear_messages();
for (int i = windowStart; i < windowEnd; i++)
ui_push_message("", notesBuf[(start + i) % NOTES_HISTORY_LINES], true, false);
}
/*
Switch between chat mode (default) adn notes mode.
also redraw the UI somewhat
*/
static void switch_mode(AppMode m) {
void switch_mode(AppMode m) {
appMode = m;
ui_clear_messages();
if (m == AppMode::NOTES) {
ui_set_header_mode("NOTE MODE");
notes_load();
notes_render();
notes_begin();
} else {
ui_set_header(nodesCount, currentNick);
chat_show_recent();
chat_draw();
}
}
/* Timings */
#define TIMEOUT_PRESENCE 2000
#define PING_INTERVAL 500
unsigned long lastPing = 0;
static int lastNodeCount = -1;
/* ---------- BUFFER SÉCURISÉ POUR LE CALLBACK ---------- */
volatile bool newPacketPending = false;
volatile uint8_t packetBuffer[sizeof(MeshMessage)+sizeof(uint16_t)];
volatile int packetLen = 0;
/* ---------- PROTOTYPES (déclarés ici pour la loop) ---------- */
void handleIncomingPacket(const uint8_t* data, int len);
//void drawChatWindow(); // now ui_push_message / ui_set_header
void broadcastPing();
void updatePresence(const char* nick);
void broadcastChatMessage(const char* from, const char* body);
static void go_to_sleep();
/* ---------- CALLBACK ESP‑NOW ULTRA‑COURT ---------- */
void IRAM_ATTR OnDataRecvChat(
const esp_now_recv_info_t* recv_info,
const uint8_t* incomingData, int len
//#else // IDF4 signature, [removed]
// const uint8_t* mac,
// const uint8_t* incomingData, int len
//#endif
)
{
const uint8_t* mac = recv_info ? recv_info->src_addr : nullptr;
(void)mac;
if (len > (int)sizeof(packetBuffer)) return; // paquet trop gros
memcpy((void*)packetBuffer, incomingData, len);
packetLen = len;
newPacketPending = true; // flag pour la loop()
}
// ----- Hash pour dé‑duplication -----
void simpleHash(const char* s, char* outHash) {
uint32_t h = 5381;
while (*s) h = ((h << 5) + h) + (uint8_t)(*s++);
snprintf(outHash, 9, "%08X", h); // 8 hex, zéro‑terminé
}
// ----- CRC16 (CRC‑CCITT) -----
uint16_t crc16(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; ++j)
crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : (crc << 1);
}
return crc;
}
// ----- Historique d'ID reçus -----
bool hasSeenId(const char* id) {
if ((int)seenMessageIds.size() < MAX_HISTORY) return false;
for (int i = 0; i < MAX_HISTORY; ++i)
if (strncmp(seenMessageIds[i].data(), id, 8) == 0) return true;
return false;
}
void rememberId(const char* id) {
if ((int)seenMessageIds.size() < MAX_HISTORY) seenMessageIds.resize(MAX_HISTORY);
strncpy(seenMessageIds[seenIndex].data(), id, 8);
seenMessageIds[seenIndex].data()[8] = '\0';
seenIndex = (seenIndex + 1) % MAX_HISTORY;
}
// ----- Buffer circulaire de messages pour l'écran -----
//void addMessage(const char* m) {
// if (messages.size() < 20) messages.assign(20, Message());
// strncpy(messages[messageHead].content, m, MAX_MSG_LEN - 1);
// messages[messageHead].content[MAX_MSG_LEN - 1] = '\0';
// messageHead = (messageHead + 1) % 20;
// if (messageCount < 20) messageCount++;
//}
// rafraîchit l'écran
//void drawChatWindow() {
// M5.Display.fillScreen(TFT_BLACK);
// M5.Display.setTextColor(TFT_GREEN);
// M5.Display.setCursor(5, 5);
// M5.Display.printf("Connected: %d", nodesCount);
//
// M5.Display.setTextColor(TFT_WHITE);
// M5.Display.setCursor(5, 10);
//
// const int lines = 13;
// int start = (messageCount < lines) ? 0 : (messageHead + 20 - lines) % 20;
// for (int i = 0; i < min(lines, messageCount); ++i) {
// int idx = (start + i) % 20;
// M5.Display.println(messages[idx].content);
// }
// M5.Display.display();
//}
/* ---------- Traitement d'un paquet en attente ---------- */
void handleIncomingPacket(const uint8_t* data, int len) {
if (len != (int)(sizeof(MeshMessage) + sizeof(uint16_t))) return;
uint16_t rxCrc;
memcpy(&rxCrc, data + sizeof(MeshMessage), sizeof(uint16_t));
if (rxCrc != crc16(data, sizeof(MeshMessage))) return;
MeshMessage msg;
memcpy(&msg, data, sizeof(MeshMessage));
if (hasSeenId(msg.id)) return;
rememberId(msg.id);
// --- Gestion du PING (mise à jour de présence) ---
if (strcmp(msg.body, "PING") == 0) {
updatePresence(msg.from);
esp_now_send(broadcastAddressEspNow, data, len);
return; // pas d'affichage
}
// --- Filtre messages "System" visant ce client ---
if (!strcmp(msg.from, "System") && strstr(msg.body, currentNick))
return;
// addMessage(line) + drawChatWindow() replaced:
//char line[MAX_MSG_LEN + 16];
//snprintf(line, sizeof(line), "%s: %s", msg.from, msg.body);
//addMessage(line);
//drawChatWindow();
bool is_system = !strcmp(msg.from, "System");
chat_history_record(is_system ? "" : msg.from, msg.body, false, is_system);
if (appMode == AppMode::CHAT)
ui_push_message(is_system ? "" : msg.from, msg.body, false, is_system);
is_system ? sound_system() : sound_msg_in();
esp_now_send(broadcastAddressEspNow, data, len);
}
/* ----- Notes mode / BT dump ----- */
void chat_set_ble(BleKeyboard* kb) { g_ble = kb; }
bool chat_is_bt_mode() { return appMode == AppMode::BT; }
// Append one line to the notes file (created on first write).
static void notes_append(const char* line) {
File f = LittleFS.open(NOTES_FILE, "a");
if (!f) return;
f.println(line);
f.close();
}
static bool btConnectedLast = false;
static void set_bt_status_message() {
if (!g_ble) { ui_push_message("", "BT unavailable.", false, true); return; }
if (g_ble->isConnected()) {
ui_push_message("", "BT connected. Press FN + ESC to send notes. TAB to exit.", false, true);
} else {
char buf[64];
snprintf(buf, sizeof(buf), "Pair Bluetooth keyboard: \"%s %s\"", currentNick, "Keyboard");
ui_push_message("", buf, false, true);
}
}
// Switch into/out of BT dump mode. /bt command
static void toggle_bt_mode() {
inputBuffer[0] = '\0';
ui_set_input(inputBuffer);
if (appMode != AppMode::BT) {
modeBeforeBt = appMode;
appMode = AppMode::BT;
ui_set_header_mode("BT MODE");
btConnectedLast = g_ble && g_ble->isConnected();
String bleName = String(g_settings.nick) + " Keyboard";
g_ble->setName(bleName.c_str()); // make sure to use latest nick
if (!btConnectedLast) NimBLEDevice::startAdvertising(); // TODO: not sure if this is still necessary?
set_bt_status_message();
} else {
switch_mode(modeBeforeBt);
}
}
/*
BT dump mode: countdown, then replay the notes file as keystrokes.
Blocking while it dump.
*/
static void bt_dump_start() {
if (!g_ble || !g_ble->isConnected()) { // dump not started.
ui_push_message("", "Not connected yet.", false, true);
return;
}
for (int s = BT_DUMP_COUNTDOWN_S; s > 0; s--) {
char buf[24];
snprintf(buf, sizeof(buf), "Sending in %d...", s);
ui_push_message("", buf, false, true);
ui_tick();
delay(1000);
}
ui_push_message("", "Sending...", false, true);
ui_tick();
File f = LittleFS.open(NOTES_FILE, "r");
if (f) {
char line[MAX_MSG_LEN];
while (f.available()) {
int n = f.readBytesUntil('\n', line, sizeof(line) - 1);
if (n > 0 && line[n - 1] == '\r') n--; // remove \r
line[n] = '\0';
g_ble->setDelay(30); // look what i found!
g_ble->print(line); // is too fast, drops keypresses on my machine
// for (int i = 0; line[i] != '\0'; i++)
// {
// g_ble->write(line[i]);
// //delay(30); // TODO: maybe make configurable?
// }
// g_ble->print(line); // is too fast, drops keypresses on my machine
g_ble->write(KEY_RETURN);
delay(30);
}
f.close();
}
ui_push_message("", "Notes sent.", false, true);
toggle_bt_mode(); // done, leave BT mode, restore the previous screen
}
/* ----- Commandes ----- */
bool apply_setting(String key, String value) {
if (key.startsWith("color_")) {
@ -414,7 +95,6 @@ bool apply_setting(String key, String value) {
return false;
}
/* ----- Commandes ----- */
void handleCommand(const char* in) {
if (!strncmp(in, "/nick ", 6)) {
strncpy(currentNick, in + 6, 15); currentNick[15] = '\0';
@ -490,204 +170,8 @@ void handleCommand(const char* in) {
}
/* ----- Envoi d'un message Chat (hors callback) ----- */
void broadcastChatMessage(const char* from, const char* body) {
MeshMessage msg;
char hash[9], tmp[MAX_MSG_LEN+32];
snprintf(tmp, sizeof(tmp), "%s%s%lu", from, body, millis());
simpleHash(tmp, hash);
strncpy(msg.id, hash, 8); msg.id[8] = '\0';
strncpy(msg.from, from, 15); msg.from[15] = '\0';
strncpy(msg.body, body, MAX_MSG_LEN-1); msg.body[MAX_MSG_LEN-1]='\0';
uint8_t pkt[sizeof(MeshMessage)+2];
memcpy(pkt, &msg, sizeof(MeshMessage));
uint16_t crc = crc16((uint8_t*)&msg, sizeof(MeshMessage));
memcpy(pkt + sizeof(MeshMessage), &crc, 2);
esp_now_send(broadcastAddressEspNow, pkt, sizeof(pkt));
rememberId(msg.id);
bool is_system = !strcmp(from, "System");
bool is_own = !is_system && !strcmp(from, currentNick);
chat_history_record(is_system ? "" : from, body, is_own, is_system);
if (appMode == AppMode::CHAT)
ui_push_message(is_system ? "" : from, body, is_own, is_system);
}
/* ----- Ping périodique ----- */
void broadcastPing() {
MeshMessage msg;
char hash[9], tmp[32];
// hash = from + "PING" + horodatage
snprintf(tmp, sizeof(tmp), "%sPING%lu", currentNick, millis());
simpleHash(tmp, hash);
strncpy(msg.id, hash, 8); msg.id[8] = '\0';
strncpy(msg.from, currentNick, 15); msg.from[15] = '\0';
strncpy(msg.body, "PING", MAX_MSG_LEN - 1);
msg.body[MAX_MSG_LEN - 1] = '\0';
uint8_t pkt[sizeof(MeshMessage) + 2];
memcpy(pkt, &msg, sizeof(MeshMessage));
uint16_t crc = crc16((uint8_t*)&msg, sizeof(MeshMessage));
memcpy(pkt + sizeof(MeshMessage), &crc, 2);
esp_now_send(broadcastAddressEspNow, pkt, sizeof(pkt));
rememberId(msg.id); // pour ignorer notre propre écho
}
/* ----- Gestion de présence ----- */
void updatePresence(const char* nick) {
for (int i = 0; i < nodesCount; ++i)
if (!strcmp(nodesConnected[i].nick, nick)) {
nodesConnected[i].lastSeen = millis(); return;
}
if (nodesCount < MAX_NODES) {
if ((int)nodesConnected.size() < MAX_NODES) nodesConnected.resize(MAX_NODES);
strncpy(nodesConnected[nodesCount].nick, nick, 15);
nodesConnected[nodesCount].nick[15] = '\0';
nodesConnected[nodesCount].lastSeen = millis();
++nodesCount;
}
}
void checkPresenceTimeouts() {
unsigned long now = millis();
for (int i = 0; i < nodesCount; ++i)
if ((now - nodesConnected[i].lastSeen) > TIMEOUT_PRESENCE) {
char m[MAX_MSG_LEN];
snprintf(m, sizeof(m), "%s left the chat!", nodesConnected[i].nick);
// broadcastChatMessage("System", m); // lets not broadcast this, but:
ui_push_message("", m, false, true);
for (int j = i; j < nodesCount-1; ++j)
nodesConnected[j] = nodesConnected[j+1];
--nodesCount; --i;
}
}
// ----- Gestion clavier & saisie utilisateur -----
// handleKeyboard() removed, M5Cardputer-specific; logic moved into chat_tick(char key)
//void handleKeyboard() {
// if (!kpAny()) return;
//
// Keyboard_Class::KeysState st = M5Cardputer.Keyboard.keysState();
//
// /* Ajout des caractères tapés */
// for (char c : st.word) {
// int n = strlen(inputBuffer);
// if (n < MAX_MSG_LEN - 2) { // -2 : place pour '\0'
// inputBuffer[n] = c;
// inputBuffer[n+1] = '\0';
// }
// }
// /* Backspace */
// if (st.del && *inputBuffer)
// inputBuffer[strlen(inputBuffer) - 1] = '\0';
//
// /* Entrée : envoi */
// if (st.enter && *inputBuffer) {
// if (inputBuffer[0] == '/')
// handleCommand(inputBuffer);
// else
// broadcastChatMessage(currentNick, inputBuffer);
//
// inputBuffer[0] = '\0';
// }
//
// /* Ligne d'édition */
// M5.Display.fillRect(0, 120, 240, 16, TFT_NAVY);
// M5.Display.setCursor(5, 120);
// M5.Display.setTextColor(TFT_YELLOW);
// M5.Display.print("> ");
// M5.Display.print(inputBuffer);
//}
// ----- Fonction principale -----
// EvilChatMesh() split into chat_init() + chat_tick(), no blocking while(true)
//void EvilChatMesh() {
// auto cfg = M5.config();
// M5.begin(cfg);
// M5.Display.setTextSize(1);
// M5.Display.setTextColor(TFT_WHITE);
// M5.Display.fillScreen(TFT_BLACK);
//
// WiFi.disconnect(true); delay(100);
// WiFi.mode(WIFI_STA);
//
// if (esp_now_init() != ESP_OK) {
// M5.Display.println("ESP‑NOW init failed"); return;
// }
// esp_now_register_recv_cb(OnDataRecvChat);
//
// esp_now_peer_info_t peer = {};
// memcpy(peer.peer_addr, broadcastAddressEspNow, 6);
// peer.channel = 0; // changed to 1, EvilChatMesh-Relay.ino expects channel 1
// peer.encrypt = false;
// if (!esp_now_is_peer_exist(peer.peer_addr))
// esp_now_add_peer(&peer);
//
// nodesConnected.clear(); nodesConnected.resize(MAX_NODES);
// messages.assign(20, Message());
// drawChatWindow();
//
// char msgArr[MAX_MSG_LEN];
// snprintf(msgArr, sizeof(msgArr), "%s enter the chat!", currentNick);
// broadcastChatMessage("System", msgArr);
//
// /* ---------- Boucle ---------- */
// while (true) {
// M5.update(); cardUpdate();
//
// /* 1. paquet reçu ? */
// if (newPacketPending) {
// noInterrupts();
// uint8_t localBuf[sizeof(MeshMessage)+2];
// int len = packetLen;
// memcpy(localBuf, (const void*)packetBuffer, len);
// newPacketPending = false;
// interrupts();
//
// handleIncomingPacket(localBuf, len);
// }
//
// /* 2. ping & timeouts */
// unsigned long now = millis();
// if (now - lastPing > PING_INTERVAL) {
// lastPing = now; broadcastPing();
// }
// checkPresenceTimeouts();
//
// /* 3. clavier */
// if (M5Cardputer.Keyboard.isChange())
// handleKeyboard();
//
// /* 4. touche ` pour quitter */
// if (kp('`')) {
// char m[MAX_MSG_LEN];
// snprintf(m, sizeof(m), "%s left the chat!", currentNick);
// broadcastChatMessage("System", m);
//
// esp_now_unregister_recv_cb();
// esp_now_deinit();
// waitAndReturnToMenu("Left the chat...");
// return;
// }
// delay(10);
// }
//}
/*
Triggered by /sleep command.
Broadcasts a leave message, turns off the backlight.
two wake sources: touch IRQ (GPIO36) and BOOT button (GPIO0).
Touch is quirky and not recommended.
*/
static void go_to_sleep() {
@ -696,8 +180,6 @@ static void go_to_sleep() {
// broadcastChatMessage("System", m); // same here, only show don't broadcasst
ui_push_message("", m, false, true);
delay(200); // let the packet fly
// Hold the backlight OFF pin through deep sleep so the screen stays dark.
pinMode(PIN_TFT_BL, OUTPUT);
digitalWrite(PIN_TFT_BL, LOW);
@ -715,27 +197,7 @@ void chat_init() {
strncpy(currentNick, g_settings.nick, sizeof(currentNick) - 1);
currentNick[sizeof(currentNick) - 1] = '\0';
nodesConnected.clear(); nodesConnected.resize(MAX_NODES);
seenMessageIds.resize(MAX_HISTORY);
for (auto& e : seenMessageIds) e.fill(0);
WiFi.persistent(false);
WiFi.mode(WIFI_STA);
WiFi.setSleep(false);
if (esp_now_init() != ESP_OK) {
Serial.println("[chat] ESP-NOW init failed");
ui_push_message("", "ESP-NOW init failed!", false, true);
return;
}
esp_now_register_recv_cb(OnDataRecvChat);
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, broadcastAddressEspNow, 6);
peer.channel = 1; // relay uses channel 1; original was 0
peer.encrypt = false;
if (!esp_now_is_peer_exist(peer.peer_addr))
esp_now_add_peer(&peer);
mesh_init();
char msgArr[MAX_MSG_LEN];
snprintf(msgArr, sizeof(msgArr), "%s entered the chat!", currentNick);
@ -769,14 +231,7 @@ void chat_tick(char key) {
}
// update bt connection status (here instead of waiting for keypress)
if (appMode == AppMode::BT) {
bool nowConnected = g_ble && g_ble->isConnected();
if (nowConnected != btConnectedLast) {
btConnectedLast = nowConnected;
if (!nowConnected) NimBLEDevice::startAdvertising();
set_bt_status_message();
}
}
if (appMode == AppMode::BT) bt_tick();
/* 3. clavier */
if (key == 0) return;
@ -792,8 +247,7 @@ void chat_tick(char key) {
/* Scroll through notes */
if (appMode == AppMode::NOTES &&
((uint8_t)key == KB_KEY_UP || (uint8_t)key == KB_KEY_DOWN)) {
notesScrollPos += ((uint8_t)key == KB_KEY_UP) ? 1 : -1;
notes_render();
notes_scroll(((uint8_t)key == KB_KEY_UP) ? 1 : -1);
return;
}
@ -801,11 +255,7 @@ void chat_tick(char key) {
if((uint8_t)key == 0x80) { // FN + ESC
bt_dump_start();
} else {
if(KB_KEY_ENTER == key) {
g_ble->write(KEY_RETURN);
} else {
g_ble->write(key);
}
bt_send_key(key);
}
} else {
// Serial.print("Received key: ");
@ -827,10 +277,7 @@ void chat_tick(char key) {
if (inputBuffer[0] == '/')
handleCommand(inputBuffer);
else if (appMode == AppMode::NOTES) {
notes_append(inputBuffer);
notes_to_buffer(inputBuffer);
notesScrollPos = 0; // snap back to the latest note
notes_render();
notes_add(inputBuffer);
} else if (appMode == AppMode::CHAT) {
broadcastChatMessage(currentNick, inputBuffer);
}

View file

@ -1,12 +1,16 @@
#pragma once
#include "EvilChatMesh.h"
class BleKeyboard;
// Lifted from EvilChatMesh in Evil-Cardputer-v1-5-2.ino and adapted for
// the CYD hardware.
enum class AppMode { CHAT, NOTES, BT };
extern AppMode appMode;
extern char currentNick[16];
extern char inputBuffer[MAX_MSG_LEN];
void switch_mode(AppMode m);
void chat_add_message(const char* nick, const char* text, bool is_own, bool is_system);
void chat_init();
void chat_tick(char key);
void chat_set_ble(BleKeyboard* kb);
bool chat_is_bt_mode();
static void toggle_bt_mode();

183
src/meshberry/notes.cpp Normal file
View file

@ -0,0 +1,183 @@
// notes.cpp - Notes mode / BT dump.
#include "notes.h"
#include "chat.h"
#include "EvilChatMesh.h"
#include "config.h"
#include "settings.h"
#include "ui.h"
#include <Arduino.h>
#include <LittleFS.h>
#include <BleKeyboard.h>
#include <NimBLEDevice.h>
static char notesBuf[NOTES_HISTORY_LINES][MAX_MSG_LEN];
static int currNote = 0;// next pos
static int notesCount = 0;
static int notesScrollPos = 0;
static void notes_to_buffer(const char* line) {
strncpy(notesBuf[currNote], line, MAX_MSG_LEN - 1);
notesBuf[currNote][MAX_MSG_LEN - 1] = '\0';
currNote = (currNote + 1) % NOTES_HISTORY_LINES;
if (notesCount < NOTES_HISTORY_LINES) notesCount++;
}
static void notes_load() {
notesCount = 0; currNote = 0; notesScrollPos = 0;
File f = LittleFS.open(NOTES_FILE, "r");
if (!f) return;
char line[MAX_MSG_LEN];
while (f.available()) {
int n = f.readBytesUntil('\n', line, sizeof(line) - 1);
if (n > 0 && line[n - 1] == '\r')
n--; // remove trailing /r from .println
line[n] = '\0';
if (n == 0) continue;
notes_to_buffer(line);
}
f.close();
}
static void notes_draw() {
int visible = UI_VISIBLE_LINES;
int maxScroll = (notesCount > visible) ? (notesCount - visible) : 0;
if (notesScrollPos > maxScroll) notesScrollPos = maxScroll;
if (notesScrollPos < 0) notesScrollPos = 0;
int windowEnd = notesCount - notesScrollPos;
int windowStart = windowEnd - visible;
if (windowStart < 0) windowStart = 0;
int start = (notesCount < NOTES_HISTORY_LINES) ? 0 : currNote;
ui_clear_messages();
for (int i = windowStart; i < windowEnd; i++)
ui_push_message("", notesBuf[(start + i) % NOTES_HISTORY_LINES], true, false);
}
void notes_begin() {
notes_load();
notes_draw();
}
void notes_scroll(int dir) {
notesScrollPos += dir;
notes_draw();
}
/* ----- Notes mode / BT dump ----- */
static BleKeyboard* g_ble = nullptr;
static AppMode modeBeforeBt = AppMode::CHAT;
void chat_set_ble(BleKeyboard* kb) { g_ble = kb; }
bool chat_is_bt_mode() { return appMode == AppMode::BT; }
// Append one line to the notes file (created on first write).
static void notes_append(const char* line) {
File f = LittleFS.open(NOTES_FILE, "a");
if (!f) return;
f.println(line);
f.close();
}
void notes_add(const char* line) {
notes_append(line);
notes_to_buffer(line);
notesScrollPos = 0; // snap back to the latest note
notes_draw();
}
static bool btConnectedLast = false;
static void set_bt_status_message() {
if (!g_ble) { ui_push_message("", "BT unavailable.", false, true); return; }
if (g_ble->isConnected()) {
ui_push_message("", "BT connected. Press FN + ESC to send notes. TAB to exit.", false, true);
} else {
char buf[64];
snprintf(buf, sizeof(buf), "Pair Bluetooth keyboard: \"%s %s\"", currentNick, "Keyboard");
ui_push_message("", buf, false, true);
}
}
// Switch into/out of BT dump mode. /bt command
void toggle_bt_mode() {
inputBuffer[0] = '\0';
ui_set_input(inputBuffer);
if (appMode != AppMode::BT) {
modeBeforeBt = appMode; // remember previous state.
appMode = AppMode::BT;
ui_set_header_mode("BT MODE");
btConnectedLast = g_ble && g_ble->isConnected();
String bleName = String(g_settings.nick) + " Keyboard";
g_ble->setName(bleName.c_str()); // make sure to use latest nick
if (!btConnectedLast) NimBLEDevice::startAdvertising(); // TODO: not sure if this is still necessary?
set_bt_status_message();
} else {
switch_mode(modeBeforeBt);
}
}
/*
BT dump mode: countdown, then replay the notes file as keystrokes.
Blocking while it dumps.
*/
void bt_dump_start() {
if (!g_ble || !g_ble->isConnected()) { // dump not started.
ui_push_message("", "Not connected yet.", false, true);
return;
}
for (int s = BT_DUMP_COUNTDOWN_S; s > 0; s--) {
char buf[24];
snprintf(buf, sizeof(buf), "Sending in %d...", s);
ui_push_message("", buf, false, true);
ui_tick();
delay(1000);
}
ui_push_message("", "Sending...", false, true);
ui_tick();
File f = LittleFS.open(NOTES_FILE, "r");
if (f) {
char line[MAX_MSG_LEN];
while (f.available()) {
int n = f.readBytesUntil('\n', line, sizeof(line) - 1);
if (n > 0 && line[n - 1] == '\r') n--; // remove \r
line[n] = '\0';
g_ble->setDelay(30); // look what i found!
g_ble->print(line); // is too fast, drops keypresses on my machine
// for (int i = 0; line[i] != '\0'; i++)
// {
// g_ble->write(line[i]);
// //delay(30); // TODO: maybe make configurable?
// }
// g_ble->print(line); // is too fast, drops keypresses on my machine
g_ble->write(KEY_RETURN);
delay(30);
}
f.close();
}
ui_push_message("", "Notes sent.", false, true);
toggle_bt_mode(); // done, leave BT mode, restore the previous screen
}
// poll BT connection status
void bt_tick() {
bool nowConnected = g_ble && g_ble->isConnected();
if (nowConnected != btConnectedLast) {
btConnectedLast = nowConnected;
if (!nowConnected) NimBLEDevice::startAdvertising();
set_bt_status_message();
}
}
void bt_send_key(char key) {
if (!g_ble) return;
if (KB_KEY_ENTER == key) g_ble->write(KEY_RETURN);
else g_ble->write(key);
}

12
src/meshberry/notes.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
// Notes mode / BT dump.
void notes_begin();
void notes_add(const char* line);
void notes_scroll(int dir);
void toggle_bt_mode();
void bt_dump_start();
void bt_tick();
void bt_send_key(char key);

View file

@ -144,7 +144,7 @@ void settings_load() {
if (*l == '\0' || *l == '#') continue; // blank line or comment
char* eq = strchr(l, '=');
if (!eq) continue; // no '=' skip
if (!eq) continue;// no '=' skip
*eq = '\0';
char* key = trim(l);
char* val = trim(eq + 1);