Compare commits

...

10 commits

Author SHA1 Message Date
my 4694422c0c imporve help 2026-09-24 11:23:02 +02:00
my 54cbae5674 cleanup 2026-09-23 15:18:38 +02:00
my bb6b0e4d73 code cleanup. Moved the original mesh code to own file. 2026-09-23 15:18:28 +02:00
my 44f10676b3 fix keyboard startup delay 2026-09-23 13:01:56 +02:00
my 77702c0d05 scroll notes 2026-09-17 09:59:47 +02:00
my d6d2c432f5 added brightness and touch settings commands 2026-09-16 23:04:57 +02:00
my a1777c1d5f add tones to readme. 2026-09-15 16:42:12 +02:00
my 02df479896 simplify tones_ config key 2026-09-15 16:41:27 +02:00
my 815f79bc0b safe random nick on reboot 2026-09-15 15:55:16 +02:00
my e00f9c6293 prevent flooding of 'left' messages 2026-09-15 15:54:57 +02:00
16 changed files with 870 additions and 660 deletions

View file

@ -7,8 +7,7 @@ All the mesh chat functionality was directly ported from https://github.com/7h30
## Hardware
- **Board**: We used a non-standard CYD variant; [1005008564907457] from Aliexpress, mostly for the price and USB-C connector. The peripheral
connectors are placed a little differently from the commonly documented ESP32-2432S028R. The pin configuration in this project is specific to this board and may not work on other CYD revisions without changes to the GPIO assignments.
- **Board**: We used a non-standard CYD variant; [1005008564907457] from Aliexpress also known as E32R28T, mostly for the price and USB-C connector. The peripheral connectors are placed a little differently from the commonly documented ESP32-2432S028R. The pin configuration in this project is specific to this board and may not work on other CYD revisions without changes to the GPIO assignments.
ESP32-32E/ESP32-D0WD-V3, no PSRAM, 320×240 ST7789 display, resistive touch.
- **Keyboard**: M5Stack CardKB v1.1, wired to the board's 1.25 mm
@ -41,10 +40,9 @@ All values have hardcoded defaults, so any key can be left out entirely.
|---|---|
| `nick` | Set nickname. Max 15 characters, no spaces. `/nick <name>` in the app also updates this. |
| `brightness` | Backlight, 0 (off) - 255 (max). Default 200 |
| `snd_msg_freq`, `snd_msg_ms` | Frequency and duration (ms) of the incoming-message beep. |
| `snd_sys_freq`, `snd_sys_ms` | Frequency and duration (ms) of the join/leave beep. |
| `touch_threshold` | Threshold for touch to wake on Touch7 pin. Higher = easier to trigger. Not recommended for now. |
| `color_*` | 6-digit hex RGB (no `#`), same format as CSS, e.g. `ff0000` is red. See the table below for which UI element each one controls. |
| `tones_msg` and `tones_sys` | Set the new message and system message melodies in MML format, see below. |
| Color key | Controls |
|---|---|
@ -59,6 +57,21 @@ All values have hardcoded defaults, so any key can be left out entirely.
Colors can also be changed from the device itself with `/set <key> <hex>`
(e.g. `/set color_bg ff0000`), see below.
## Sounds
You can set the new message [`tones_msg`] and system message [`tones_sys`] sounds (like a new user entering the chat) using a MML music notation string using the `/set` command.
Example:
`/set tones_msg t300 o4`
The defaults are:
- New message: `t300 d8 a+4`, Tempo 300bpm, D for 1/8, A for 1/4
- System message: `t300 a8 d+4`, Tempo 300bpm, A for 1/8, D for 1/4
More information here: https://github.com/maxint-rd/MmlMusic
And to listen to a MML string: https://matarillo.github.io/mmlplayer/
## Chat commands
Typed into the input line, same as any chat message:
@ -71,6 +84,7 @@ Typed into the input line, same as any chat message:
| `/clear` | Clear the chat window |
| `/clearnotes` | Erase all saved notes |
| `/set <color_key> <hex>` | Change a color setting; saved to `config.txt`, visible after sleep/restart |
| `/set <tones_key> <tones>` | Change a tones setting; saved to `config.txt`, audible after sleep/restart |
| `/bt` | Toggle BT send mode (see below) |
| `/sleep` | Put the device to deep sleep (wake via BOOT button or touch7) |
| `/help` | List commands on-device |

View file

@ -1,8 +1,8 @@
#nick=your nick, picked randomly on first run
brightness=200
tones_msg=T300 O4 l8 D l4 A+
tones_sys=T300 O4 l8 A l4 D+
tones_msg=t300 d8 a+4
tones_sys=t300 a8 d+4
touch_threshold=300

View file

@ -46,10 +46,15 @@
#define KB_KEY_BACKSPACE 0x08
#define KB_KEY_TAB 0x09
#define KB_KEY_ESC 0x1B
#define KB_KEY_LEFT 0xB4
#define KB_KEY_UP 0xB5
#define KB_KEY_DOWN 0xB6
#define KB_KEY_RIGHT 0xB7
// Notes mode / BT dump
#define BT_DUMP_COUNTDOWN_S 3
#define NOTES_FILE "/notes.txt"
#define NOTES_HISTORY_LINES 100 // scrollback kept in RAM for notes mode
// ESP-NOW Chat

View file

@ -31,12 +31,10 @@ lib_deps =
build_flags =
-DLV_CONF_INCLUDE_SIMPLE
-I include
; use strchr instead of strchr_P
-Dstrchr_P=strchr
; We only need HID peripheral from NimBLE
; saving some memory here.
; NimBLE takes up too much memory, disable what we dont need.
-DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED
-DCONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED
-include src/meshberry/compat.h
[env:evilchat-relay]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip

2
src/idf_component.yml Normal file
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

@ -5,6 +5,8 @@
#include <Arduino.h>
static TwoWire kbWire(1); // Use other I2C bus
static bool present = false;
static unsigned long lastRetry = 0;
void cardkb_init() {
esp_log_level_set("esp32-hal-i2c-ng", ESP_LOG_NONE);
@ -13,13 +15,11 @@ void cardkb_init() {
pinMode(PIN_KB_SCL, INPUT_PULLUP);
kbWire.begin(PIN_KB_SDA, PIN_KB_SCL);
kbWire.setClock(CARDKB_I2C_HZ);
lastRetry = millis() - 10000;
}
char cardkb_read() {
static bool present = false;
static unsigned long lastRetry = 0;
// test if the keyboard is connected,
// test if the keyboard is connected,
// prevent endless logging of errors
if (!present) {
unsigned long now = millis();
@ -33,7 +33,7 @@ char cardkb_read() {
return 0;
}
present = true;
char key = kbWire.available() ? (char)kbWire.read() : 0;
return key;
return key;
}

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 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,289 +48,60 @@ 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);
}
}
// get the last few lines of the notes file.
static void notes_show_recent() {
File f = LittleFS.open(NOTES_FILE, "r");
if (!f) return;
static char buf[MODE_HISTORY_LINES][MAX_MSG_LEN];
int count = 0, head = 0;
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;
strncpy(buf[head], line, MAX_MSG_LEN - 1);
buf[head][MAX_MSG_LEN - 1] = '\0';
head = (head + 1) % MODE_HISTORY_LINES;
if (count < MODE_HISTORY_LINES) count++;
}
f.close();
int start = (count < MODE_HISTORY_LINES) ? 0 : head;
for (int i = 0; i < count; i++)
ui_push_message("", buf[(start + i) % MODE_HISTORY_LINES], true, false);
}
/*
Switch between chat mode (default) adn notes mode.
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_show_recent();
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 ---------- */
/* ----- Commandes ----- */
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);
bool apply_setting(String key, String value) {
if (key.startsWith("color_")) {
return settings_set_color(key.c_str(), value.c_str());
}
if (key.startsWith("tones_")) {
return settings_set_tones(key.c_str(), value.c_str());
}
if (key == "touch_threshold") {
return settings_set("touch_threshold", value.c_str());
}
if (key == "brightness") {
return settings_set("brightness", value.c_str());
}
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 ----- */
void handleCommand(const char* in) {
if (!strncmp(in, "/nick ", 6)) {
strncpy(currentNick, in + 6, 15); currentNick[15] = '\0';
settings_set_nick(currentNick); // persist to LittleFS
ui_set_header(nodesCount, currentNick); // refresh header bar
char l[MAX_MSG_LEN]; snprintf(l, sizeof(l),
"Nickname set to: %s", currentNick);
char l[MAX_MSG_LEN];
snprintf(l, sizeof(l), "Nickname set to: %s", currentNick);
ui_push_message("", l, false, true);
} else if (!strcmp(in, "/clear")) {
@ -396,23 +115,20 @@ void handleCommand(const char* in) {
toggle_bt_mode();
} else if (!strncmp(in, "/set ", 5)) {
char args[MAX_MSG_LEN];
strncpy(args, in + 5, sizeof(args) - 1); args[sizeof(args) - 1] = '\0';
char* sp = strchr(args, ' ');
if (!sp) {
ui_push_message("", "Usage: /set color_bg ff0000", false, true);
String args = String(in + 5);
args.trim();
int spaceIdx = args.indexOf(' ');
if (spaceIdx == -1) {
ui_push_message("", "Usage: /set <key> <value>", false, true);
} else {
*sp = '\0';
size_t klen = strlen(args);
bool is_tones = klen > 6 && !strcmp(args + klen - 6, "_tones");
bool ok = is_tones ? settings_set_tones(args, sp + 1)
: settings_set_color(args, sp + 1);
if (ok)
ui_push_message("", "Saved. Restart to apply.", false, true);
else if (is_tones)
ui_push_message("", "Unknown tones key.", false, true);
else
ui_push_message("", "Unknown color or bad hex value.", false, true);
String key = args.substring(0, spaceIdx);
String value = args.substring(spaceIdx + 1);
if ( apply_setting (key, value)) {
ui_push_message("", "Saved. For colors: restart to apply.", false, true);
} else {
ui_push_message("", "Unknown key or invalid value.", false, true);
}
}
} else if (!strncmp(in, "/me ", 4)) {
@ -429,23 +145,42 @@ void handleCommand(const char* in) {
ui_push_message("", b, false, true);
}
} else if (!strcmp(in, "/help")) {
//addMessage("/nick <name> : change pseudo");
//addMessage("/me <action> : action roleplay");
//addMessage("/clear : clear the chat");
//addMessage("/people : list connected");
ui_push_message("", "/nick <name> : change pseudo", false, true);
ui_push_message("", "/me <action> : action roleplay", false, true);
ui_push_message("", "/clear : clear the chat", false, true);
ui_push_message("", "/clearnotes : erase saved notes", false, true);
ui_push_message("", "/bt : toggle BT send mode", false, true);
ui_push_message("", "/set color_bg ff0000 : set a color (restart to apply)", false, true);
ui_push_message("", "/people : list connected", false, true);
ui_push_message("", "/sleep or /s : sleep device", false, true);
ui_push_message("", "TAB : notes mode", false, true);
//drawChatWindow();
} else if (strncmp(in, "/help", 5) == 0) {
if (in[5] == '\0' || in[5] == ' ') {
const char* hargs = in + 5;
while (*hargs == ' ') {
hargs++;
}
} else if (!strcmp(in, "/sleep") || !strcmp(in, "/s")) {
Serial.println("help?");
if (*hargs == '\0') {
ui_push_message("", "/nick <name> : change nickname", false, true);
ui_push_message("", "/clear : clear the chat", false, true);
ui_push_message("", "/clearnotes : erase saved notes", false, true);
ui_push_message("", "/bt : toggle BT send mode", false, true);
ui_push_message("", "/set <key> <value> : change a setting", false, true);
ui_push_message("", "/people : list connected", false, true);
ui_push_message("", "/s or /sleep: sleep device", false, true);
ui_push_message("", "TAB : toggle notes/chat mode", false, true);
ui_push_message("", "/help colors, /help tones or /help settings for more help", false, true);
}
else
{
if(!strcmp(hargs, "colors")) {
ui_push_message("", "/set <region> <color>: change colors", false, true);
ui_push_message("", "<region>: color_bg, color_header_bg, color_msg, color_nick_other, color_nick_own, color_system, color_input_prompt", false, true);
ui_push_message("", "<color> is a hexadecimal color, like ff0000 for red. Dont add the #", false, true);
} else if(!strcmp(hargs, "tones")) {
ui_push_message("", "/set <event> <tones>: change tones", false, true);
ui_push_message("", "<event>: tones_msg (incoming messages) and tones_sys (other messages)", false, true);
ui_push_message("", "<tones> is a MML string. Like '/set tones_msg t255 O5 l8 cag': Message tone is Tempo 255, Octave 5, 8th length C, A, G notes.", false, true);
} else if(!strcmp(hargs, "settings")) {
ui_push_message("", "/set brightness <0-255> : screen brightness", false, true);
ui_push_message("", "/set touch_threshold <n> : touch wake sensitivity", false, true);
}
}
}
} else if (!strcmp(in, "/sleep") || !strcmp(in, "/s")) {
go_to_sleep(); // sleep device. maybe add a button?
} else {
@ -453,210 +188,19 @@ 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);
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).
Triggered by /sleep command.
Touch is quirky and not recommended.
*/
static void go_to_sleep() {
char m[MAX_MSG_LEN];
snprintf(m, sizeof(m), "%s left the chat!", currentNick);
broadcastChatMessage("System", m);
delay(200); // let the packet fly
// broadcastChatMessage("System", m); // same here, only show don't broadcasst
ui_push_message("", m, false, true);
// Hold the backlight OFF pin through deep sleep so the screen stays dark.
pinMode(PIN_TFT_BL, OUTPUT);
digitalWrite(PIN_TFT_BL, LOW);
gpio_hold_en((gpio_num_t)PIN_TFT_BL);
@ -672,27 +216,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);
@ -726,14 +250,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;
@ -746,15 +263,18 @@ void chat_tick(char key) {
return;
}
/* Scroll through notes */
if (appMode == AppMode::NOTES &&
((uint8_t)key == KB_KEY_UP || (uint8_t)key == KB_KEY_DOWN)) {
notes_scroll(((uint8_t)key == KB_KEY_UP) ? 1 : -1);
return;
}
if (appMode == AppMode::BT) {
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: ");
@ -771,13 +291,12 @@ void chat_tick(char key) {
} else if (key == KB_KEY_ENTER || key == '\n') { // '\n' for Serial keypresses
// if (appMode == AppMode::BT && inputBuffer[0] != '/') { // let /command pass...
// bt_dump_start();
// } else
// } else
if (*inputBuffer) { // if (st.enter && *inputBuffer) {
if (inputBuffer[0] == '/')
handleCommand(inputBuffer);
else if (appMode == AppMode::NOTES) {
notes_append(inputBuffer);
ui_push_message("", inputBuffer, true, false);
notes_add(inputBuffer);
} else if (appMode == AppMode::CHAT) {
broadcastChatMessage(currentNick, inputBuffer);
}
@ -788,14 +307,14 @@ void chat_tick(char key) {
} else if ((uint8_t)key >= 0x20 && (uint8_t)key < 0x7F) {
int n = strlen(inputBuffer);
if (n < MAX_MSG_LEN - 2) { // -2 : place pour '\0'
inputBuffer[n] = key;
inputBuffer[n+1] = '\0';
}
}
}
// else if (appMode == AppMode::BT && (uint8_t)key == 0x80) { // FN + ESC
// bt_dump_start();
// }

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();

9
src/meshberry/compat.h Normal file
View file

@ -0,0 +1,9 @@
#pragma once
#include <string.h>
// Replace the calls that MMLMusic makes to the
// AVR function strchr_P to strchr
#ifndef strchr_P
#define strchr_P strchr
#endif

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

@ -8,8 +8,8 @@ settings_t g_settings;
static const settings_t DEFAULTS = {
.nick = NICK_DEFAULT_PREFIX,
.brightness = 200,
.tones_msg = "T140 G",
.tones_sys = "T140 A",
.tones_msg = "t300 d8 a+4",
.tones_sys = "t300 a8 d+4",
.touch_threshold = TOUCH_THRESHOLD,
.color_bg = {.blue = 0x11, .green = 0x11, .red = 0x11},
.color_header_bg = {.blue = 0x1e, .green = 0x1e, .red = 0x1e},
@ -57,65 +57,24 @@ static bool apply_kv(const char* key, const char* val) {
}
static void dump_settings() {
Serial.println("[settings]");
Serial.printf ("[settings] nick=%s brightness=%d\n",
g_settings.nick, g_settings.brightness);
Serial.printf ("[settings] tones_msg=%s tones_sys=%s\n",
g_settings.tones_msg.c_str(), g_settings.tones_sys.c_str());
Serial.printf ("[settings] nick=%s\n", g_settings.nick);
Serial.printf ("[settings] brightness=%d\n", g_settings.brightness);
Serial.printf ("[settings] touch_threshold=%u\n", g_settings.touch_threshold);
Serial.printf ("[settings] color_bg=%02x%02x%02x color_header_bg=%02x%02x%02x\n",
g_settings.color_bg.red, g_settings.color_bg.green, g_settings.color_bg.blue,
g_settings.color_header_bg.red, g_settings.color_header_bg.green, g_settings.color_header_bg.blue);
Serial.printf ("[settings] color_msg=%02x%02x%02x color_nick_other=%02x%02x%02x color_nick_own=%02x%02x%02x\n",
g_settings.color_msg.red, g_settings.color_msg.green, g_settings.color_msg.blue,
g_settings.color_nick_other.red, g_settings.color_nick_other.green, g_settings.color_nick_other.blue,
g_settings.color_nick_own.red, g_settings.color_nick_own.green, g_settings.color_nick_own.blue);
Serial.printf ("[settings] color_system=%02x%02x%02x color_input_prompt=%02x%02x%02x\n",
g_settings.color_system.red, g_settings.color_system.green, g_settings.color_system.blue,
g_settings.color_input_prompt.red, g_settings.color_input_prompt.green, g_settings.color_input_prompt.blue);
Serial.println("[settings]");
Serial.printf ("[settings] --- TONES: ---\n");
Serial.printf ("[settings] tones_msg=%s\n", g_settings.tones_msg.c_str());
Serial.printf ("[settings] tones_sys=%s\n", g_settings.tones_sys.c_str());
Serial.printf ("[settings] --- COLORS: ---\n");
Serial.printf ("[settings] color_bg=%02x%02x%02x\n", g_settings.color_header_bg.red, g_settings.color_header_bg.green, g_settings.color_header_bg.blue);
Serial.printf ("[settings] color_header_bg=%02x%02x%02x\n", g_settings.color_header_bg.red, g_settings.color_header_bg.green, g_settings.color_header_bg.blue);
Serial.printf ("[settings] color_msg=%02x%02x%02x\n", g_settings.color_msg.red, g_settings.color_msg.green, g_settings.color_msg.blue);
Serial.printf ("[settings] color_nick_other=%02x%02x%02x\n", g_settings.color_nick_other.red, g_settings.color_nick_other.green, g_settings.color_nick_other.blue);
Serial.printf ("[settings] color_nick_own=%02x%02x%02x\n", g_settings.color_nick_own.red, g_settings.color_nick_own.green, g_settings.color_nick_own.blue);
Serial.printf("[settings] color_system=%02x%02x%02x\n", g_settings.color_system.red, g_settings.color_system.green, g_settings.color_system.blue );
Serial.printf("[settings] color_input_prompt=%02x%02x%02x\n", g_settings.color_input_prompt.red, g_settings.color_input_prompt.green, g_settings.color_input_prompt.blue);
}
// Public
void settings_load() {
g_settings = DEFAULTS;
snprintf(g_settings.nick, sizeof(g_settings.nick),
"%s%04u", NICK_DEFAULT_PREFIX, (unsigned)(esp_random() % 10000));
if (!LittleFS.begin(true)) {
Serial.println("[settings] LittleFS mount failed, using defaults");
dump_settings();
return;
}
File f = LittleFS.open("/config.txt", "r");
if (!f) {
Serial.println("[settings] /config.txt not found, using defaults");
dump_settings();
return;
}
char line[128];
while (f.available()) {
int n = f.readBytesUntil('\n', line, sizeof(line) - 1);
line[n] = '\0';
char* l = trim(line);
if (*l == '\0' || *l == '#') continue; // blank line or comment
char* eq = strchr(l, '=');
if (!eq) continue; // no '=' skip
*eq = '\0';
char* key = trim(l);
char* val = trim(eq + 1);
apply_kv(key, val);
}
f.close();
dump_settings();
}
static void rewrite_config_line(const char* key, const char* val) {
bool found = false;
size_t keylen = strlen(key);
@ -155,6 +114,52 @@ static void rewrite_config_line(const char* key, const char* val) {
Serial.printf("[settings] %s saved: %s\n", key, val);
}
void settings_load() {
g_settings = DEFAULTS;
snprintf(g_settings.nick, sizeof(g_settings.nick),
"%s%04u", NICK_DEFAULT_PREFIX, (unsigned)(esp_random() % 10000));
char nick_check[CHAT_MAX_NICK_LEN];
strcpy(nick_check, g_settings.nick);
if (!LittleFS.begin(true))
{
Serial.println("[settings] LittleFS mount failed, using defaults");
dump_settings();
return;
}
File f = LittleFS.open("/config.txt", "r");
if (!f) {
Serial.println("[settings] /config.txt not found, using defaults");
dump_settings();
return;
}
char line[128];
while (f.available()) {
int n = f.readBytesUntil('\n', line, sizeof(line) - 1);
line[n] = '\0';
char* l = trim(line);
if (*l == '\0' || *l == '#') continue; // blank line or comment
char* eq = strchr(l, '=');
if (!eq) continue;// no '=' skip
*eq = '\0';
char* key = trim(l);
char* val = trim(eq + 1);
apply_kv(key, val);
}
f.close();
if(!strcmp(g_settings.nick, nick_check)) { // safe the random nick
rewrite_config_line("nick", g_settings.nick);
}
dump_settings();
}
void settings_set_nick(const char* nick) {
strncpy(g_settings.nick, nick, CHAT_MAX_NICK_LEN - 1);
g_settings.nick[CHAT_MAX_NICK_LEN - 1] = '\0';
@ -168,12 +173,25 @@ bool settings_set_color(const char* key, const char* hex) {
return true;
}
bool settings_set_tones(const char* key, const char* tones) {
if (strlen(key) <= 6 || strcmp(key + strlen(key) - 6, "_tones") != 0) {
if (strlen(key) <= 6 || strncmp(key, "tones_", 6) != 0) {
return false;
}
if (!apply_kv(key, tones)) return false; // unknown key or bad tones
rewrite_config_line(key, tones);
return true;
}
}
// for brightness and touch_threshold
bool settings_set(const char* key, const char* value) {
if (!apply_kv(key, value)) return false; // unknown key or bad value
rewrite_config_line(key, value);
return true;
}
// bool settings_set_touch_threshold(const char* number) {
// const char *key = "touch_threshold";
// if (!apply_kv(key, number)) return false; // unknown key or bad hex
// rewrite_config_line(key, number);
// return true;
// }

View file

@ -26,3 +26,4 @@ void settings_load();
void settings_set_nick(const char* nick);
bool settings_set_color(const char* key, const char* hex);
bool settings_set_tones(const char* key, const char* tones);
bool settings_set(const char* key, const char *number);

View file

@ -83,6 +83,8 @@ static lv_obj_t* input_label = nullptr;
struct MsgRow { lv_obj_t *row, *nick, *text; };
static MsgRow msg_rows[UI_VISIBLE_LINES];
uint8_t pBrightness = -1;
static void refresh_msgs() {
int empty = UI_VISIBLE_LINES - msg_count;
for (int i = 0; i < UI_VISIBLE_LINES; i++) {
@ -182,6 +184,8 @@ void ui_chat_init() {
lv_obj_set_pos(msg_area, 0, UI_MSG_AREA_Y);
strip_style(msg_area);
lv_obj_set_flex_flow(msg_area, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(msg_area, 0, 0); // no gap between message rows
lv_obj_set_flex_flow(msg_area, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(msg_area, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
@ -284,7 +288,11 @@ void ui_clear_messages() {
refresh_msgs();
}
void ui_tick() {
void ui_tick()
{
lv_timer_handler();
if(pBrightness != g_settings.brightness){
pBrightness = g_settings.brightness;
tft.setBrightness(g_settings.brightness);
}
}