Compare commits
No commits in common. "4694422c0c992f2583f43634809cbdb86d96092a" and "8b03bc5e8439b1391d468b25188213741b9cdc1c" have entirely different histories.
4694422c0c
...
8b03bc5e84
22
README.md
22
README.md
|
|
@ -7,7 +7,8 @@ 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 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.
|
||||
- **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.
|
||||
|
||||
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
|
||||
|
|
@ -40,9 +41,10 @@ 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 |
|
||||
|---|---|
|
||||
|
|
@ -57,21 +59,6 @@ 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:
|
||||
|
|
@ -84,7 +71,6 @@ 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 |
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
#nick=your nick, picked randomly on first run
|
||||
brightness=200
|
||||
|
||||
tones_msg=t300 d8 a+4
|
||||
tones_sys=t300 a8 d+4
|
||||
tones_msg=T300 O4 l8 D l4 A+
|
||||
tones_sys=T300 O4 l8 A l4 D+
|
||||
|
||||
touch_threshold=300
|
||||
|
||||
|
|
|
|||
|
|
@ -46,15 +46,10 @@
|
|||
#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
|
||||
|
|
|
|||
|
|
@ -31,10 +31,12 @@ lib_deps =
|
|||
build_flags =
|
||||
-DLV_CONF_INCLUDE_SIMPLE
|
||||
-I include
|
||||
; NimBLE takes up too much memory, disable what we dont need.
|
||||
; use strchr instead of strchr_P
|
||||
-Dstrchr_P=strchr
|
||||
; We only need HID peripheral from NimBLE
|
||||
; saving some memory here.
|
||||
-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
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
dependencies:
|
||||
idf: '>=5.1'
|
||||
|
|
@ -1,375 +0,0 @@
|
|||
#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);
|
||||
// }
|
||||
//}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#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();
|
||||
|
|
@ -5,8 +5,6 @@
|
|||
#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);
|
||||
|
|
@ -15,10 +13,12 @@ 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,
|
||||
// prevent endless logging of errors
|
||||
if (!present) {
|
||||
|
|
|
|||
|
|
@ -1,43 +1,95 @@
|
|||
// Heavily based on Evil-Cardputer-v1-5-2.ino
|
||||
// chat.cpp, EvilChatMesh ported to ESP32 CYD + M5Stack CardKB
|
||||
//
|
||||
// Original source : Evil-Cardputer-v1-5-2.ino, lines 20480-20871
|
||||
// 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] = "";
|
||||
|
||||
// init from g_settings.nick
|
||||
char currentNick[16];
|
||||
uint8_t broadcastAddressEspNow[] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
|
||||
|
||||
/* Notes mode / BT dump */
|
||||
AppMode appMode = AppMode::CHAT;
|
||||
// static std::vector<Message> messages;
|
||||
int messageCount = 0;
|
||||
int messageHead = 0;
|
||||
|
||||
// init from g_settings.nick
|
||||
static 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;
|
||||
|
||||
// for redrawing
|
||||
#define MODE_HISTORY_LINES 12
|
||||
|
||||
// buffer for chat messages
|
||||
struct ChatMessage {
|
||||
struct ChatHistEntry {
|
||||
char nick[CHAT_MAX_NICK_LEN];
|
||||
char text[MAX_MSG_LEN];
|
||||
bool is_own;
|
||||
bool is_system;
|
||||
};
|
||||
static ChatMessage chatHistory[MODE_HISTORY_LINES];
|
||||
static ChatHistEntry chatHistory[MODE_HISTORY_LINES];
|
||||
static int chatHistCount = 0;
|
||||
static int chatHistNext = 0; // next write slot
|
||||
|
||||
void chat_add_message(const char* nick, const char* text, bool is_own, bool is_system) {
|
||||
ChatMessage& e = chatHistory[chatHistNext];
|
||||
static void chat_history_record(const char* nick, const char* text, bool is_own, bool is_system) {
|
||||
ChatHistEntry& 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);
|
||||
|
|
@ -48,60 +100,289 @@ void chat_add_message(const char* nick, const char* text, bool is_own, bool is_s
|
|||
if (chatHistCount < MODE_HISTORY_LINES) chatHistCount++;
|
||||
}
|
||||
|
||||
static void chat_draw() {
|
||||
static void chat_show_recent() {
|
||||
int start = (chatHistCount < MODE_HISTORY_LINES) ? 0 : chatHistNext;
|
||||
for (int i = 0; i < chatHistCount; i++) {
|
||||
ChatMessage& e = chatHistory[(start + i) % MODE_HISTORY_LINES];
|
||||
ChatHistEntry& 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.
|
||||
also redraw the UI somewhat
|
||||
*/
|
||||
void switch_mode(AppMode m) {
|
||||
static void switch_mode(AppMode m) {
|
||||
appMode = m;
|
||||
ui_clear_messages();
|
||||
if (m == AppMode::NOTES) {
|
||||
ui_set_header_mode("NOTE MODE");
|
||||
notes_begin();
|
||||
notes_show_recent();
|
||||
} else {
|
||||
ui_set_header(nodesCount, currentNick);
|
||||
chat_draw();
|
||||
chat_show_recent();
|
||||
}
|
||||
}
|
||||
|
||||
/* 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();
|
||||
|
||||
/* ----- Commandes ----- */
|
||||
/* ---------- CALLBACK ESP‑NOW ULTRA‑COURT ---------- */
|
||||
|
||||
bool apply_setting(String key, String value) {
|
||||
if (key.startsWith("color_")) {
|
||||
return settings_set_color(key.c_str(), value.c_str());
|
||||
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()
|
||||
}
|
||||
if (key.startsWith("tones_")) {
|
||||
return settings_set_tones(key.c_str(), value.c_str());
|
||||
|
||||
// ----- 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é
|
||||
}
|
||||
if (key == "touch_threshold") {
|
||||
return settings_set("touch_threshold", value.c_str());
|
||||
|
||||
// ----- 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);
|
||||
}
|
||||
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")) {
|
||||
|
|
@ -115,20 +396,23 @@ void handleCommand(const char* in) {
|
|||
toggle_bt_mode();
|
||||
|
||||
} else if (!strncmp(in, "/set ", 5)) {
|
||||
String args = String(in + 5);
|
||||
args.trim();
|
||||
|
||||
int spaceIdx = args.indexOf(' ');
|
||||
if (spaceIdx == -1) {
|
||||
ui_push_message("", "Usage: /set <key> <value>", false, true);
|
||||
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);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
*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);
|
||||
}
|
||||
|
||||
} else if (!strncmp(in, "/me ", 4)) {
|
||||
|
|
@ -145,41 +429,22 @@ void handleCommand(const char* in) {
|
|||
ui_push_message("", b, false, true);
|
||||
}
|
||||
|
||||
} else if (strncmp(in, "/help", 5) == 0) {
|
||||
if (in[5] == '\0' || in[5] == ' ') {
|
||||
const char* hargs = in + 5;
|
||||
while (*hargs == ' ') {
|
||||
hargs++;
|
||||
}
|
||||
|
||||
Serial.println("help?");
|
||||
if (*hargs == '\0') {
|
||||
ui_push_message("", "/nick <name> : change nickname", 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 <key> <value> : change a setting", 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("", "/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
ui_push_message("", "/sleep or /s : sleep device", false, true);
|
||||
ui_push_message("", "TAB : notes mode", false, true);
|
||||
//drawChatWindow();
|
||||
|
||||
} else if (!strcmp(in, "/sleep") || !strcmp(in, "/s")) {
|
||||
go_to_sleep(); // sleep device. maybe add a button?
|
||||
|
||||
|
|
@ -188,19 +453,210 @@ 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).
|
||||
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); // same here, only show don't broadcasst
|
||||
ui_push_message("", m, false, true);
|
||||
broadcastChatMessage("System", m);
|
||||
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);
|
||||
gpio_hold_en((gpio_num_t)PIN_TFT_BL);
|
||||
|
||||
|
|
@ -216,7 +672,27 @@ void chat_init() {
|
|||
strncpy(currentNick, g_settings.nick, sizeof(currentNick) - 1);
|
||||
currentNick[sizeof(currentNick) - 1] = '\0';
|
||||
|
||||
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("[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);
|
||||
|
||||
char msgArr[MAX_MSG_LEN];
|
||||
snprintf(msgArr, sizeof(msgArr), "%s entered the chat!", currentNick);
|
||||
|
|
@ -250,7 +726,14 @@ void chat_tick(char key) {
|
|||
}
|
||||
|
||||
// update bt connection status (here instead of waiting for keypress)
|
||||
if (appMode == AppMode::BT) bt_tick();
|
||||
if (appMode == AppMode::BT) {
|
||||
bool nowConnected = g_ble && g_ble->isConnected();
|
||||
if (nowConnected != btConnectedLast) {
|
||||
btConnectedLast = nowConnected;
|
||||
if (!nowConnected) NimBLEDevice::startAdvertising();
|
||||
set_bt_status_message();
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. clavier */
|
||||
if (key == 0) return;
|
||||
|
|
@ -263,18 +746,15 @@ 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 {
|
||||
bt_send_key(key);
|
||||
if(KB_KEY_ENTER == key) {
|
||||
g_ble->write(KEY_RETURN);
|
||||
} else {
|
||||
g_ble->write(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Serial.print("Received key: ");
|
||||
|
|
@ -296,7 +776,8 @@ void chat_tick(char key) {
|
|||
if (inputBuffer[0] == '/')
|
||||
handleCommand(inputBuffer);
|
||||
else if (appMode == AppMode::NOTES) {
|
||||
notes_add(inputBuffer);
|
||||
notes_append(inputBuffer);
|
||||
ui_push_message("", inputBuffer, true, false);
|
||||
} else if (appMode == AppMode::CHAT) {
|
||||
broadcastChatMessage(currentNick, inputBuffer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
#pragma once
|
||||
#include "EvilChatMesh.h"
|
||||
|
||||
class BleKeyboard;
|
||||
|
||||
enum class AppMode { CHAT, NOTES, BT };
|
||||
extern AppMode appMode;
|
||||
extern char currentNick[16];
|
||||
extern char inputBuffer[MAX_MSG_LEN];
|
||||
// Lifted from EvilChatMesh in Evil-Cardputer-v1-5-2.ino and adapted for
|
||||
// the CYD hardware.
|
||||
|
||||
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();
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#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
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
// 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);
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#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);
|
||||
|
|
@ -8,8 +8,8 @@ settings_t g_settings;
|
|||
static const settings_t DEFAULTS = {
|
||||
.nick = NICK_DEFAULT_PREFIX,
|
||||
.brightness = 200,
|
||||
.tones_msg = "t300 d8 a+4",
|
||||
.tones_sys = "t300 a8 d+4",
|
||||
.tones_msg = "T140 G",
|
||||
.tones_sys = "T140 A",
|
||||
.touch_threshold = TOUCH_THRESHOLD,
|
||||
.color_bg = {.blue = 0x11, .green = 0x11, .red = 0x11},
|
||||
.color_header_bg = {.blue = 0x1e, .green = 0x1e, .red = 0x1e},
|
||||
|
|
@ -57,24 +57,65 @@ static bool apply_kv(const char* key, const char* val) {
|
|||
}
|
||||
|
||||
static void dump_settings() {
|
||||
Serial.printf ("[settings] nick=%s\n", g_settings.nick);
|
||||
Serial.printf ("[settings] brightness=%d\n", g_settings.brightness);
|
||||
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] touch_threshold=%u\n", g_settings.touch_threshold);
|
||||
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);
|
||||
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]");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
@ -114,52 +155,6 @@ 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';
|
||||
|
|
@ -173,25 +168,12 @@ 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 || strncmp(key, "tones_", 6) != 0) {
|
||||
if (strlen(key) <= 6 || strcmp(key + strlen(key) - 6, "_tones") != 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;
|
||||
// }
|
||||
|
|
@ -26,4 +26,3 @@ 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);
|
||||
|
|
|
|||
|
|
@ -83,8 +83,6 @@ 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++) {
|
||||
|
|
@ -184,8 +182,6 @@ 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);
|
||||
|
||||
|
||||
|
|
@ -288,11 +284,7 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue