initial working version
This commit is contained in:
commit
257ed842f9
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.pio
|
||||||
|
.cache
|
||||||
|
.dummy
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
compile_commands.json
|
||||||
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Jason2866.esp-decoder",
|
||||||
|
"pioarduino.pioarduino-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
17
data/config.txt
Normal file
17
data/config.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#nick=your nick, picked randomly on first run
|
||||||
|
brightness=200
|
||||||
|
|
||||||
|
msg_freq=1800
|
||||||
|
msg_ms=20
|
||||||
|
sys_freq=440
|
||||||
|
sys_ms=10
|
||||||
|
|
||||||
|
touch_threshold=300
|
||||||
|
|
||||||
|
color_bg=ffffff
|
||||||
|
color_header_bg=999999
|
||||||
|
color_msg=dddddd
|
||||||
|
color_nick_other=44ff44
|
||||||
|
color_nick_own=ffdd00
|
||||||
|
color_system=666666
|
||||||
|
color_input_prompt=99ff00
|
||||||
1
data/notes.txt
Normal file
1
data/notes.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Your notes will go here.
|
||||||
95
include/config.h
Normal file
95
include/config.h
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Display (ST7789, SPI)
|
||||||
|
// Source: https://www.lcdwiki.com/2.8inch_ESP32-32E-7789
|
||||||
|
// Driver: LovyanGFX with Panel_ST7789
|
||||||
|
|
||||||
|
#define PIN_TFT_MOSI 13
|
||||||
|
#define PIN_TFT_MISO 12
|
||||||
|
#define PIN_TFT_SCLK 14
|
||||||
|
#define PIN_TFT_CS 15
|
||||||
|
#define PIN_TFT_DC 2
|
||||||
|
#define PIN_TFT_RST -1
|
||||||
|
#define PIN_TFT_BL 21 // Backlight
|
||||||
|
|
||||||
|
#define PANEL_NATIVE_W 240
|
||||||
|
#define PANEL_NATIVE_H 320
|
||||||
|
|
||||||
|
// Dimensions after rotation, for sanity.
|
||||||
|
#define TFT_W 320
|
||||||
|
#define TFT_H 240
|
||||||
|
|
||||||
|
|
||||||
|
// Touchscreen (XPT2046, software SPI)
|
||||||
|
// unused for now, because shared pins with KB i2c.
|
||||||
|
#define PIN_BOOT_BTN 0
|
||||||
|
#define PIN_TOUCH_IRQ 36
|
||||||
|
#define PIN_TOUCH_MOSI 32
|
||||||
|
#define PIN_TOUCH_MISO 39
|
||||||
|
#define PIN_TOUCH_CLK 25
|
||||||
|
#define PIN_TOUCH_CS 33
|
||||||
|
// TODO: should calibrate if used.
|
||||||
|
#define TOUCH_X_MIN 200
|
||||||
|
#define TOUCH_X_MAX 3800
|
||||||
|
#define TOUCH_Y_MIN 200
|
||||||
|
#define TOUCH_Y_MAX 3800
|
||||||
|
|
||||||
|
|
||||||
|
// CardKB (M5Stack v1.1, I2C)
|
||||||
|
// SDA/GPIO21 is taken by the display backlight.
|
||||||
|
#define PIN_KB_SDA 23
|
||||||
|
#define PIN_KB_SCL 19
|
||||||
|
#define CARDKB_ADDR 0x5F
|
||||||
|
#define CARDKB_I2C_HZ 100000UL
|
||||||
|
// CardKB key byte values
|
||||||
|
#define KB_KEY_ENTER 0x0D
|
||||||
|
#define KB_KEY_BACKSPACE 0x08
|
||||||
|
#define KB_KEY_TAB 0x09
|
||||||
|
#define KB_KEY_ESC 0x1B
|
||||||
|
|
||||||
|
// Notes mode / BT dump
|
||||||
|
#define BT_DUMP_COUNTDOWN_S 3
|
||||||
|
#define NOTES_FILE "/notes.txt"
|
||||||
|
|
||||||
|
|
||||||
|
// ESP-NOW Chat
|
||||||
|
#define CHAT_PING_INTERVAL_MS 500
|
||||||
|
#define CHAT_PEER_TIMEOUT_MS 2000
|
||||||
|
#define CHAT_DEDUP_HISTORY 32
|
||||||
|
#define CHAT_MAX_MESSAGES 20
|
||||||
|
#define CHAT_MAX_MSG_LEN 100
|
||||||
|
#define CHAT_MAX_NICK_LEN 16
|
||||||
|
#define CHAT_MAX_PEERS 30
|
||||||
|
#define INPUT_MAX_LEN (CHAT_MAX_MSG_LEN - 1)
|
||||||
|
#define NICK_DEFAULT_PREFIX "user"
|
||||||
|
|
||||||
|
// UI layout constants
|
||||||
|
#define UI_HEADER_H 20 // top status bar height
|
||||||
|
#define UI_INPUT_H 20 // bottom input line height
|
||||||
|
#define UI_MSG_AREA_Y (UI_HEADER_H + 1)
|
||||||
|
#define UI_MSG_AREA_H (TFT_H - UI_HEADER_H - UI_INPUT_H - 2)
|
||||||
|
#define UI_INPUT_Y (TFT_H - UI_INPUT_H)
|
||||||
|
#define UI_LINE_H 18 // Approximate
|
||||||
|
#define UI_VISIBLE_LINES (UI_MSG_AREA_H / UI_LINE_H)
|
||||||
|
|
||||||
|
#define PIN_LED_RED 22
|
||||||
|
#define PIN_LED_GREEN 16
|
||||||
|
#define PIN_LED_BLUE 17
|
||||||
|
|
||||||
|
// SD card reader (unused)
|
||||||
|
// note: some same pins as cardKB!
|
||||||
|
#define PIN_SD_CS 5
|
||||||
|
#define PIN_SD_MOSI 23
|
||||||
|
#define PIN_SD_SCLK 18
|
||||||
|
#define PIN_SD_MISO 19
|
||||||
|
|
||||||
|
|
||||||
|
// Speaker
|
||||||
|
#define PIN_SPEAKER 26
|
||||||
|
#define PIN_SPEAKER_EN 4
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
#define SERIAL_BAUD 115200
|
||||||
|
|
||||||
|
#define PIN_TOUCH7 27
|
||||||
|
#define TOUCH_THRESHOLD 200 /* Greater the value, more the sensitivity */
|
||||||
12
include/lv_conf.h
Normal file
12
include/lv_conf.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#ifndef LV_CONF_H
|
||||||
|
#define LV_CONF_H
|
||||||
|
|
||||||
|
#define LV_USE_LOG 1
|
||||||
|
#define LV_COLOR_DEPTH 16
|
||||||
|
#define LV_MEM_SIZE (24 * 1024U)
|
||||||
|
#define LV_COLOR_16_SWAP 1
|
||||||
|
#define LV_FONT_MONTSERRAT_14 1
|
||||||
|
#define LV_USE_FLEX 1
|
||||||
|
#define LV_USE_LABEL 1
|
||||||
|
|
||||||
|
#endif /* LV_CONF_H */
|
||||||
45
platformio.ini
Normal file
45
platformio.ini
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:cyberdeck-chat]
|
||||||
|
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
board_build.flash_size = 4MB
|
||||||
|
board_build.partitions = huge_app.csv
|
||||||
|
board_build.filesystem = littlefs
|
||||||
|
monitor_speed = 115200
|
||||||
|
upload_speed = 921600
|
||||||
|
;monitor_filters = send_on_enter
|
||||||
|
build_src_filter = +<meshberry/> -<chat-relay/>
|
||||||
|
lib_deps =
|
||||||
|
lovyan03/LovyanGFX @ ^1.2
|
||||||
|
lvgl/lvgl @ ^9.0
|
||||||
|
; t-vk/ESP32 BLE Keyboard@^0.3.2
|
||||||
|
https://github.com/sakul-the-one/sakuls-ESP32-BLE-Keyboard
|
||||||
|
h2zero/NimBLE-Arduino
|
||||||
|
build_flags =
|
||||||
|
-DLV_CONF_INCLUDE_SIMPLE
|
||||||
|
-I include
|
||||||
|
; We only need HID peripheral from NimBLE
|
||||||
|
; saving some memory here.
|
||||||
|
-DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED
|
||||||
|
-DCONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED
|
||||||
|
|
||||||
|
[env:evilchat-relay]
|
||||||
|
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
upload_speed = 921600
|
||||||
|
build_src_filter = -<meshberry/> +<chat-relay/> ; ignores root
|
||||||
|
lib_deps =
|
||||||
|
build_flags =
|
||||||
|
-I include
|
||||||
39
src/meshberry/cardkb.cpp
Normal file
39
src/meshberry/cardkb.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#include "cardkb.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include <Wire.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
static TwoWire kbWire(1); // Use other I2C bus
|
||||||
|
|
||||||
|
void cardkb_init() {
|
||||||
|
esp_log_level_set("esp32-hal-i2c-ng", ESP_LOG_NONE);
|
||||||
|
esp_log_level_set("Wire", ESP_LOG_NONE);
|
||||||
|
pinMode(PIN_KB_SDA, INPUT_PULLUP);
|
||||||
|
pinMode(PIN_KB_SCL, INPUT_PULLUP);
|
||||||
|
kbWire.begin(PIN_KB_SDA, PIN_KB_SCL);
|
||||||
|
kbWire.setClock(CARDKB_I2C_HZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
unsigned long now = millis();
|
||||||
|
if (now - lastRetry < 1000) return 0;
|
||||||
|
lastRetry = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t got = kbWire.requestFrom((uint8_t)CARDKB_ADDR, (uint8_t)1);
|
||||||
|
if (!got) {
|
||||||
|
present = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
present = true;
|
||||||
|
|
||||||
|
char key = kbWire.available() ? (char)kbWire.read() : 0;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
3
src/meshberry/cardkb.h
Normal file
3
src/meshberry/cardkb.h
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#pragma once
|
||||||
|
void cardkb_init();
|
||||||
|
char cardkb_read();
|
||||||
801
src/meshberry/chat.cpp
Normal file
801
src/meshberry/chat.cpp
Normal file
|
|
@ -0,0 +1,801 @@
|
||||||
|
// 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 <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;
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
/* ---------- 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 ChatHistEntry {
|
||||||
|
char nick[CHAT_MAX_NICK_LEN];
|
||||||
|
char text[MAX_MSG_LEN];
|
||||||
|
bool is_own;
|
||||||
|
bool is_system;
|
||||||
|
};
|
||||||
|
static ChatHistEntry 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];
|
||||||
|
strncpy(e.nick, nick, sizeof(e.nick) - 1);
|
||||||
|
e.nick[sizeof(e.nick) - 1] = '\0';
|
||||||
|
strncpy(e.text, text, sizeof(e.text) - 1);
|
||||||
|
e.text[sizeof(e.text) - 1] = '\0';
|
||||||
|
e.is_own = is_own;
|
||||||
|
e.is_system = is_system;
|
||||||
|
chatHistNext = (chatHistNext + 1) % MODE_HISTORY_LINES;
|
||||||
|
if (chatHistCount < MODE_HISTORY_LINES) chatHistCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void chat_show_recent() {
|
||||||
|
int start = (chatHistCount < MODE_HISTORY_LINES) ? 0 : chatHistNext;
|
||||||
|
for (int i = 0; i < chatHistCount; i++) {
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
static void switch_mode(AppMode m) {
|
||||||
|
appMode = m;
|
||||||
|
ui_clear_messages();
|
||||||
|
if (m == AppMode::NOTES) {
|
||||||
|
ui_set_header_mode("NOTE MODE");
|
||||||
|
notes_show_recent();
|
||||||
|
} else {
|
||||||
|
ui_set_header(nodesCount, currentNick);
|
||||||
|
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();
|
||||||
|
|
||||||
|
/* ---------- CALLBACK ESP‑NOW ULTRA‑COURT ---------- */
|
||||||
|
|
||||||
|
void IRAM_ATTR OnDataRecvChat(
|
||||||
|
const esp_now_recv_info_t* recv_info,
|
||||||
|
const uint8_t* incomingData, int len
|
||||||
|
//#else // IDF4 signature, [removed]
|
||||||
|
// const uint8_t* mac,
|
||||||
|
// const uint8_t* incomingData, int len
|
||||||
|
//#endif
|
||||||
|
)
|
||||||
|
{
|
||||||
|
const uint8_t* mac = recv_info ? recv_info->src_addr : nullptr;
|
||||||
|
(void)mac;
|
||||||
|
if (len > (int)sizeof(packetBuffer)) return; // paquet trop gros
|
||||||
|
memcpy((void*)packetBuffer, incomingData, len);
|
||||||
|
packetLen = len;
|
||||||
|
newPacketPending = true; // flag pour la loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Hash pour dé‑duplication -----
|
||||||
|
void simpleHash(const char* s, char* outHash) {
|
||||||
|
uint32_t h = 5381;
|
||||||
|
while (*s) h = ((h << 5) + h) + (uint8_t)(*s++);
|
||||||
|
snprintf(outHash, 9, "%08X", h); // 8 hex, zéro‑terminé
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- CRC16 (CRC‑CCITT) -----
|
||||||
|
uint16_t crc16(const uint8_t* data, size_t len) {
|
||||||
|
uint16_t crc = 0xFFFF;
|
||||||
|
for (size_t i = 0; i < len; ++i) {
|
||||||
|
crc ^= (uint16_t)data[i] << 8;
|
||||||
|
for (int j = 0; j < 8; ++j)
|
||||||
|
crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : (crc << 1);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Historique d'ID reçus -----
|
||||||
|
bool hasSeenId(const char* id) {
|
||||||
|
if ((int)seenMessageIds.size() < MAX_HISTORY) return false;
|
||||||
|
for (int i = 0; i < MAX_HISTORY; ++i)
|
||||||
|
if (strncmp(seenMessageIds[i].data(), id, 8) == 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void rememberId(const char* id) {
|
||||||
|
if ((int)seenMessageIds.size() < MAX_HISTORY) seenMessageIds.resize(MAX_HISTORY);
|
||||||
|
strncpy(seenMessageIds[seenIndex].data(), id, 8);
|
||||||
|
seenMessageIds[seenIndex].data()[8] = '\0';
|
||||||
|
seenIndex = (seenIndex + 1) % MAX_HISTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Buffer circulaire de messages pour l'écran -----
|
||||||
|
//void addMessage(const char* m) {
|
||||||
|
// if (messages.size() < 20) messages.assign(20, Message());
|
||||||
|
// strncpy(messages[messageHead].content, m, MAX_MSG_LEN - 1);
|
||||||
|
// messages[messageHead].content[MAX_MSG_LEN - 1] = '\0';
|
||||||
|
// messageHead = (messageHead + 1) % 20;
|
||||||
|
// if (messageCount < 20) messageCount++;
|
||||||
|
//}
|
||||||
|
|
||||||
|
// rafraîchit l'écran
|
||||||
|
//void drawChatWindow() {
|
||||||
|
// M5.Display.fillScreen(TFT_BLACK);
|
||||||
|
// M5.Display.setTextColor(TFT_GREEN);
|
||||||
|
// M5.Display.setCursor(5, 5);
|
||||||
|
// M5.Display.printf("Connected: %d", nodesCount);
|
||||||
|
//
|
||||||
|
// M5.Display.setTextColor(TFT_WHITE);
|
||||||
|
// M5.Display.setCursor(5, 10);
|
||||||
|
//
|
||||||
|
// const int lines = 13;
|
||||||
|
// int start = (messageCount < lines) ? 0 : (messageHead + 20 - lines) % 20;
|
||||||
|
// for (int i = 0; i < min(lines, messageCount); ++i) {
|
||||||
|
// int idx = (start + i) % 20;
|
||||||
|
// M5.Display.println(messages[idx].content);
|
||||||
|
// }
|
||||||
|
// M5.Display.display();
|
||||||
|
//}
|
||||||
|
|
||||||
|
/* ---------- Traitement d'un paquet en attente ---------- */
|
||||||
|
void handleIncomingPacket(const uint8_t* data, int len) {
|
||||||
|
if (len != (int)(sizeof(MeshMessage) + sizeof(uint16_t))) return;
|
||||||
|
|
||||||
|
uint16_t rxCrc;
|
||||||
|
memcpy(&rxCrc, data + sizeof(MeshMessage), sizeof(uint16_t));
|
||||||
|
if (rxCrc != crc16(data, sizeof(MeshMessage))) return;
|
||||||
|
|
||||||
|
MeshMessage msg;
|
||||||
|
memcpy(&msg, data, sizeof(MeshMessage));
|
||||||
|
|
||||||
|
if (hasSeenId(msg.id)) return;
|
||||||
|
rememberId(msg.id);
|
||||||
|
|
||||||
|
// --- Gestion du PING (mise à jour de présence) ---
|
||||||
|
if (strcmp(msg.body, "PING") == 0) {
|
||||||
|
updatePresence(msg.from);
|
||||||
|
esp_now_send(broadcastAddressEspNow, data, len);
|
||||||
|
return; // pas d'affichage
|
||||||
|
}
|
||||||
|
// --- Filtre messages "System" visant ce client ---
|
||||||
|
if (!strcmp(msg.from, "System") && strstr(msg.body, currentNick))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// addMessage(line) + drawChatWindow() replaced:
|
||||||
|
//char line[MAX_MSG_LEN + 16];
|
||||||
|
//snprintf(line, sizeof(line), "%s: %s", msg.from, msg.body);
|
||||||
|
//addMessage(line);
|
||||||
|
//drawChatWindow();
|
||||||
|
bool is_system = !strcmp(msg.from, "System");
|
||||||
|
chat_history_record(is_system ? "" : msg.from, msg.body, false, is_system);
|
||||||
|
if (appMode == AppMode::CHAT)
|
||||||
|
ui_push_message(is_system ? "" : msg.from, msg.body, false, is_system);
|
||||||
|
is_system ? sound_system() : sound_msg_in();
|
||||||
|
|
||||||
|
esp_now_send(broadcastAddressEspNow, data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- Notes mode / BT dump ----- */
|
||||||
|
|
||||||
|
void chat_set_ble(BleKeyboard* kb) { g_ble = kb; }
|
||||||
|
|
||||||
|
bool chat_is_bt_mode() { return appMode == AppMode::BT; }
|
||||||
|
|
||||||
|
// Append one line to the notes file (created on first write).
|
||||||
|
static void notes_append(const char* line) {
|
||||||
|
File f = LittleFS.open(NOTES_FILE, "a");
|
||||||
|
if (!f) return;
|
||||||
|
f.println(line);
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool btConnectedLast = false;
|
||||||
|
|
||||||
|
static void set_bt_status_message() {
|
||||||
|
if (!g_ble) { ui_push_message("", "BT unavailable.", false, true); return; }
|
||||||
|
if (g_ble->isConnected()) {
|
||||||
|
ui_push_message("", "BT connected. Press FN + ESC to send notes. TAB to exit.", false, true);
|
||||||
|
} else {
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), "Pair Bluetooth keyboard: \"%s %s\"", currentNick, "Keyboard");
|
||||||
|
ui_push_message("", buf, false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch into/out of BT dump mode. /bt command
|
||||||
|
static void toggle_bt_mode() {
|
||||||
|
inputBuffer[0] = '\0';
|
||||||
|
ui_set_input(inputBuffer);
|
||||||
|
if (appMode != AppMode::BT) {
|
||||||
|
modeBeforeBt = appMode;
|
||||||
|
appMode = AppMode::BT;
|
||||||
|
ui_set_header_mode("BT MODE");
|
||||||
|
btConnectedLast = g_ble && g_ble->isConnected();
|
||||||
|
String bleName = String(g_settings.nick) + " Keyboard";
|
||||||
|
g_ble->setName(bleName.c_str()); // make sure to use latest nick
|
||||||
|
if (!btConnectedLast) NimBLEDevice::startAdvertising(); // TODO: not sure if this is still necessary?
|
||||||
|
set_bt_status_message();
|
||||||
|
} else {
|
||||||
|
switch_mode(modeBeforeBt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
BT dump mode: countdown, then replay the notes file as keystrokes.
|
||||||
|
Blocking while it dump.
|
||||||
|
*/
|
||||||
|
static void bt_dump_start() {
|
||||||
|
if (!g_ble || !g_ble->isConnected()) { // dump not started.
|
||||||
|
ui_push_message("", "Not connected yet.", false, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int s = BT_DUMP_COUNTDOWN_S; s > 0; s--) {
|
||||||
|
char buf[24];
|
||||||
|
snprintf(buf, sizeof(buf), "Sending in %d...", s);
|
||||||
|
ui_push_message("", buf, false, true);
|
||||||
|
ui_tick();
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
ui_push_message("", "Sending...", false, true);
|
||||||
|
ui_tick();
|
||||||
|
|
||||||
|
File f = LittleFS.open(NOTES_FILE, "r");
|
||||||
|
if (f) {
|
||||||
|
char line[MAX_MSG_LEN];
|
||||||
|
while (f.available()) {
|
||||||
|
int n = f.readBytesUntil('\n', line, sizeof(line) - 1);
|
||||||
|
if (n > 0 && line[n - 1] == '\r') n--; // remove \r
|
||||||
|
line[n] = '\0';
|
||||||
|
g_ble->setDelay(30); // look what i found!
|
||||||
|
g_ble->print(line); // is too fast, drops keypresses on my machine
|
||||||
|
// for (int i = 0; line[i] != '\0'; i++)
|
||||||
|
// {
|
||||||
|
// g_ble->write(line[i]);
|
||||||
|
// //delay(30); // TODO: maybe make configurable?
|
||||||
|
// }
|
||||||
|
// g_ble->print(line); // is too fast, drops keypresses on my machine
|
||||||
|
g_ble->write(KEY_RETURN);
|
||||||
|
delay(30);
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
ui_push_message("", "Notes sent.", false, true);
|
||||||
|
toggle_bt_mode(); // done, leave BT mode, restore the previous screen
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- Commandes ----- */
|
||||||
|
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);
|
||||||
|
ui_push_message("", l, false, true);
|
||||||
|
|
||||||
|
} else if (!strcmp(in, "/clear")) {
|
||||||
|
ui_clear_messages();
|
||||||
|
|
||||||
|
} else if (!strcmp(in, "/clearnotes")) {
|
||||||
|
LittleFS.remove(NOTES_FILE);
|
||||||
|
ui_push_message("", "Notes cleared.", false, true);
|
||||||
|
|
||||||
|
} else if (!strcmp(in, "/bt")) {
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
*sp = '\0';
|
||||||
|
if (settings_set_color(args, sp + 1))
|
||||||
|
ui_push_message("", "Saved. Restart to apply.", false, true);
|
||||||
|
else
|
||||||
|
ui_push_message("", "Unknown color or bad hex value.", false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (!strncmp(in, "/me ", 4)) {
|
||||||
|
char act[MAX_MSG_LEN];
|
||||||
|
snprintf(act, sizeof(act), "* %s %s", currentNick, in + 4);
|
||||||
|
broadcastChatMessage(currentNick, act);
|
||||||
|
|
||||||
|
} else if (!strcmp(in, "/people")) {
|
||||||
|
|
||||||
|
ui_push_message("", "-> Users online:", false, true);
|
||||||
|
char b[MAX_MSG_LEN];
|
||||||
|
for (int i = 0; i < nodesCount; ++i) {
|
||||||
|
snprintf(b, sizeof(b), " %s", nodesConnected[i].nick);
|
||||||
|
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 : 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?
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ui_push_message("", "Unknown command. /help", false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- 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);
|
||||||
|
delay(200); // let the packet fly
|
||||||
|
|
||||||
|
// Hold the backlight OFF pin through deep sleep so the screen stays dark.
|
||||||
|
digitalWrite(PIN_TFT_BL, LOW);
|
||||||
|
gpio_hold_en((gpio_num_t)PIN_TFT_BL);
|
||||||
|
|
||||||
|
// Wake on BOOT button (GPIO0), active LOW.
|
||||||
|
esp_sleep_enable_ext1_wakeup(1ULL << PIN_BOOT_BTN, ESP_EXT1_WAKEUP_ALL_LOW);
|
||||||
|
// wake from touchpad?
|
||||||
|
touchAttachInterrupt(PIN_TOUCH7, [](){}, g_settings.touch_threshold); // configure GPIO27 as touch7
|
||||||
|
esp_sleep_enable_touchpad_wakeup();
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
char msgArr[MAX_MSG_LEN];
|
||||||
|
snprintf(msgArr, sizeof(msgArr), "%s entered the chat!", currentNick);
|
||||||
|
broadcastChatMessage("System", msgArr);
|
||||||
|
|
||||||
|
Serial.printf("[chat] ready as %s\n", currentNick);
|
||||||
|
}
|
||||||
|
|
||||||
|
void chat_tick(char key) {
|
||||||
|
/* 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();
|
||||||
|
if (nodesCount != lastNodeCount) { // refresh header only on peer count change
|
||||||
|
lastNodeCount = nodesCount;
|
||||||
|
if (appMode == AppMode::CHAT) ui_set_header(nodesCount, currentNick); // only for chat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. clavier */
|
||||||
|
if (key == 0) return;
|
||||||
|
|
||||||
|
/* Tab: toggle notes/chat mode */
|
||||||
|
if (key == KB_KEY_TAB) {
|
||||||
|
if (appMode == AppMode::CHAT) switch_mode(AppMode::NOTES);
|
||||||
|
else if (appMode == AppMode::NOTES) switch_mode(AppMode::CHAT);
|
||||||
|
else if (appMode == AppMode::BT) switch_mode(AppMode::CHAT);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Serial.print("Received key: ");
|
||||||
|
// Serial.print((uint8_t)key);
|
||||||
|
// Serial.print(" -> ");
|
||||||
|
// Serial.println(key);
|
||||||
|
|
||||||
|
/* Backspace */
|
||||||
|
if (key == KB_KEY_BACKSPACE) { // was: if (st.del && *inputBuffer)
|
||||||
|
int n = strlen(inputBuffer);
|
||||||
|
if (n > 0) inputBuffer[n - 1] = '\0';
|
||||||
|
|
||||||
|
/* Entrée : envoi */
|
||||||
|
} 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
|
||||||
|
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);
|
||||||
|
} else if (appMode == AppMode::CHAT) {
|
||||||
|
broadcastChatMessage(currentNick, inputBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputBuffer[0] = '\0';
|
||||||
|
|
||||||
|
/* Ajout des caractères tapés */
|
||||||
|
|
||||||
|
|
||||||
|
} 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();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ligne d'édition */
|
||||||
|
ui_set_input(inputBuffer);
|
||||||
|
|
||||||
|
}
|
||||||
12
src/meshberry/chat.h
Normal file
12
src/meshberry/chat.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
class BleKeyboard;
|
||||||
|
|
||||||
|
// Lifted from EvilChatMesh in Evil-Cardputer-v1-5-2.ino and adapted for
|
||||||
|
// the CYD hardware.
|
||||||
|
|
||||||
|
void chat_init();
|
||||||
|
void chat_tick(char key);
|
||||||
|
void chat_set_ble(BleKeyboard* kb);
|
||||||
|
bool chat_is_bt_mode();
|
||||||
|
static void toggle_bt_mode();
|
||||||
2
src/meshberry/idf_component.yml
Normal file
2
src/meshberry/idf_component.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
dependencies:
|
||||||
|
idf: '>=5.1'
|
||||||
49
src/meshberry/main.cpp
Normal file
49
src/meshberry/main.cpp
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <esp_sleep.h>
|
||||||
|
#include <driver/gpio.h>
|
||||||
|
#include "config.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "ui.h"
|
||||||
|
#include "cardkb.h"
|
||||||
|
#include "sound.h"
|
||||||
|
#include "chat.h"
|
||||||
|
#include <BleKeyboard.h>
|
||||||
|
|
||||||
|
BleKeyboard bleKeyboard;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(SERIAL_BAUD);
|
||||||
|
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
||||||
|
if (cause == ESP_SLEEP_WAKEUP_EXT1 || cause == ESP_SLEEP_WAKEUP_TOUCHPAD) {
|
||||||
|
Serial.println("[wake] resumed from deep sleep");
|
||||||
|
gpio_hold_dis((gpio_num_t)PIN_TFT_BL); // release backlight hold before ui_init
|
||||||
|
}
|
||||||
|
Serial.println("Cyberdeck Chat, starting up");
|
||||||
|
|
||||||
|
settings_load(); // load config.txt from LittleFS
|
||||||
|
ui_init();
|
||||||
|
ui_chat_init();
|
||||||
|
ui_set_header(0, g_settings.nick);
|
||||||
|
|
||||||
|
cardkb_init();
|
||||||
|
sound_init();
|
||||||
|
chat_init();
|
||||||
|
|
||||||
|
// BLE keyboard device name = chat nick + "Keyboard"
|
||||||
|
String bleName = String(g_settings.nick) + " Keyboard";
|
||||||
|
bleKeyboard.setName(bleName.c_str());
|
||||||
|
bleKeyboard.begin();
|
||||||
|
chat_set_ble(&bleKeyboard); // chat gets ble instance
|
||||||
|
|
||||||
|
Serial.println("Ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (chat_is_bt_mode()) {
|
||||||
|
while (Serial.available()) Serial.read();
|
||||||
|
}
|
||||||
|
char key = Serial.available() ? (char)Serial.read() : cardkb_read();
|
||||||
|
chat_tick(key); // TODO: debug why a serial key does not show in the UI?
|
||||||
|
ui_tick();
|
||||||
|
delay(5);
|
||||||
|
}
|
||||||
172
src/meshberry/settings.cpp
Normal file
172
src/meshberry/settings.cpp
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
#include "settings.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
|
settings_t g_settings;
|
||||||
|
|
||||||
|
static const settings_t DEFAULTS = {
|
||||||
|
.nick = NICK_DEFAULT_PREFIX,
|
||||||
|
.brightness = 200,
|
||||||
|
.msg_freq = 1800,
|
||||||
|
.msg_ms = 20,
|
||||||
|
.sys_freq = 440,
|
||||||
|
.sys_ms = 10,
|
||||||
|
.touch_threshold = TOUCH_THRESHOLD,
|
||||||
|
.color_bg = {.blue = 0x11, .green = 0x11, .red = 0x11},
|
||||||
|
.color_header_bg = {.blue = 0x1e, .green = 0x1e, .red = 0x1e},
|
||||||
|
.color_msg = {.blue = 0xdd, .green = 0xdd, .red = 0xdd},
|
||||||
|
.color_nick_other = {.blue = 0x44, .green = 0xff, .red = 0x44},
|
||||||
|
.color_nick_own = {.blue = 0x00, .green = 0xdd, .red = 0xff},
|
||||||
|
.color_system = {.blue = 0x66, .green = 0x66, .red = 0x66},
|
||||||
|
.color_input_prompt = {.blue = 0x33, .green = 0x77, .red = 0x33},
|
||||||
|
};
|
||||||
|
|
||||||
|
static lv_color_t parse_color(const char* hex) {
|
||||||
|
uint32_t v = strtoul(hex, nullptr, 16);
|
||||||
|
return lv_color_make((v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char* trim(char* s) {
|
||||||
|
while (*s == ' ' || *s == '\t') ++s;
|
||||||
|
char* end = s + strlen(s) - 1;
|
||||||
|
while (end > s && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n'))
|
||||||
|
*end-- = '\0';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_hex6(const char* s) {
|
||||||
|
if (strlen(s) != 6) return false;
|
||||||
|
for (int i = 0; i < 6; i++) if (!isxdigit((unsigned char)s[i])) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool apply_kv(const char* key, const char* val) {
|
||||||
|
if (!strcmp(key, "nick")) { strncpy(g_settings.nick, val, CHAT_MAX_NICK_LEN - 1); }
|
||||||
|
else if (!strcmp(key, "brightness")) { g_settings.brightness = (uint8_t)constrain(atoi(val), 0, 255); }
|
||||||
|
else if (!strcmp(key, "msg_freq")) { g_settings.msg_freq = (uint16_t)atoi(val); }
|
||||||
|
else if (!strcmp(key, "msg_ms")) { g_settings.msg_ms = (uint16_t)atoi(val); }
|
||||||
|
else if (!strcmp(key, "sys_freq")) { g_settings.sys_freq = (uint16_t)atoi(val); }
|
||||||
|
else if (!strcmp(key, "sys_ms")) { g_settings.sys_ms = (uint16_t)atoi(val); }
|
||||||
|
else if (!strcmp(key, "touch_threshold")) { g_settings.touch_threshold = (uint16_t)atoi(val); }
|
||||||
|
else if (!strcmp(key, "color_bg")) { if (!is_hex6(val)) return false; g_settings.color_bg = parse_color(val); }
|
||||||
|
else if (!strcmp(key, "color_header_bg")) { if (!is_hex6(val)) return false; g_settings.color_header_bg = parse_color(val); }
|
||||||
|
else if (!strcmp(key, "color_msg")) { if (!is_hex6(val)) return false; g_settings.color_msg = parse_color(val); }
|
||||||
|
else if (!strcmp(key, "color_nick_other")) { if (!is_hex6(val)) return false; g_settings.color_nick_other = parse_color(val); }
|
||||||
|
else if (!strcmp(key, "color_nick_own")) { if (!is_hex6(val)) return false; g_settings.color_nick_own = parse_color(val); }
|
||||||
|
else if (!strcmp(key, "color_system")) { if (!is_hex6(val)) return false; g_settings.color_system = parse_color(val); }
|
||||||
|
else if (!strcmp(key, "color_input_prompt")) { if (!is_hex6(val)) return false; g_settings.color_input_prompt = parse_color(val); }
|
||||||
|
else return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dump_settings() {
|
||||||
|
Serial.println("[settings] ---- effective config ----");
|
||||||
|
Serial.printf ("[settings] nick=%s brightness=%d\n",
|
||||||
|
g_settings.nick, g_settings.brightness);
|
||||||
|
Serial.printf ("[settings] msg_freq=%u msg_ms=%u sys_freq=%u sys_ms=%u touch_threshold=%u\n",
|
||||||
|
g_settings.msg_freq, g_settings.msg_ms, g_settings.sys_freq,
|
||||||
|
g_settings.sys_ms, 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] ----------------------------");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
File in = LittleFS.open("/config.txt", "r");
|
||||||
|
File out = LittleFS.open("/config.tmp", "w");
|
||||||
|
if (!out) {
|
||||||
|
Serial.println("[settings] failed to open /config.tmp for writing");
|
||||||
|
if (in) in.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in) {
|
||||||
|
char line[128];
|
||||||
|
while (in.available()) {
|
||||||
|
int n = in.readBytesUntil('\n', line, sizeof(line) - 1);
|
||||||
|
line[n] = '\0';
|
||||||
|
// Preserve \r\n on Windows-edited files
|
||||||
|
bool had_cr = (n > 0 && line[n-1] == '\r');
|
||||||
|
if (had_cr) line[--n] = '\0';
|
||||||
|
|
||||||
|
if (!found && strncmp(line, key, keylen) == 0 && line[keylen] == '=') {
|
||||||
|
out.printf("%s=%s%s\n", key, val, had_cr ? "\r" : "");
|
||||||
|
found = true;
|
||||||
|
} else {
|
||||||
|
out.printf("%s%s\n", line, had_cr ? "\r" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) out.printf("%s=%s\n", key, val);
|
||||||
|
out.close();
|
||||||
|
|
||||||
|
LittleFS.remove("/config.txt");
|
||||||
|
LittleFS.rename("/config.tmp", "/config.txt");
|
||||||
|
Serial.printf("[settings] %s saved: %s\n", key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
rewrite_config_line("nick", g_settings.nick);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool settings_set_color(const char* key, const char* hex) {
|
||||||
|
if (strncmp(key, "color_", 6) != 0) return false; // scope to color_* only
|
||||||
|
if (!apply_kv(key, hex)) return false; // unknown key or bad hex
|
||||||
|
rewrite_config_line(key, hex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
28
src/meshberry/settings.h
Normal file
28
src/meshberry/settings.h
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#pragma once
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// All values read from /config.txt on LittleFS.
|
||||||
|
struct settings_t {
|
||||||
|
char nick[CHAT_MAX_NICK_LEN];
|
||||||
|
uint8_t brightness;
|
||||||
|
uint16_t msg_freq; // Hz, notification beep for incoming messages
|
||||||
|
uint16_t msg_ms; // duration in ms
|
||||||
|
uint16_t sys_freq; // Hz, beep for join / leave events
|
||||||
|
uint16_t sys_ms; // duration in ms
|
||||||
|
uint16_t touch_threshold; // touch7 (GPIO27) wake-from-sleep sensitivity
|
||||||
|
lv_color_t color_bg;
|
||||||
|
lv_color_t color_header_bg;
|
||||||
|
lv_color_t color_msg;
|
||||||
|
lv_color_t color_nick_other;
|
||||||
|
lv_color_t color_nick_own;
|
||||||
|
lv_color_t color_system;
|
||||||
|
lv_color_t color_input_prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global settings, read-only
|
||||||
|
extern settings_t g_settings;
|
||||||
|
|
||||||
|
void settings_load();
|
||||||
|
void settings_set_nick(const char* nick);
|
||||||
|
bool settings_set_color(const char* key, const char* hex);
|
||||||
12
src/meshberry/sound.cpp
Normal file
12
src/meshberry/sound.cpp
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#include "sound.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
void sound_init() {
|
||||||
|
pinMode(PIN_SPEAKER_EN, OUTPUT);
|
||||||
|
digitalWrite(PIN_SPEAKER_EN, LOW); // LOW = amp enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
void sound_msg_in() { tone(PIN_SPEAKER, g_settings.msg_freq, g_settings.msg_ms); }
|
||||||
|
void sound_system() { tone(PIN_SPEAKER, g_settings.sys_freq, g_settings.sys_ms); }
|
||||||
5
src/meshberry/sound.h
Normal file
5
src/meshberry/sound.h
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
void sound_init();
|
||||||
|
void sound_msg_in(); // short high beep, new chat message
|
||||||
|
void sound_system(); // short low beep , join / leave
|
||||||
290
src/meshberry/ui.cpp
Normal file
290
src/meshberry/ui.cpp
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
#include "ui.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
#include <LovyanGFX.hpp>
|
||||||
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
// All pins are set in config.h
|
||||||
|
class LGFX : public lgfx::LGFX_Device {
|
||||||
|
lgfx::Panel_ST7789 _panel;
|
||||||
|
lgfx::Bus_SPI _bus;
|
||||||
|
lgfx::Light_PWM _light;
|
||||||
|
public:
|
||||||
|
LGFX() {
|
||||||
|
{
|
||||||
|
auto cfg = _bus.config();
|
||||||
|
cfg.spi_host = VSPI_HOST;
|
||||||
|
cfg.spi_mode = 0;
|
||||||
|
cfg.freq_write = 40000000;
|
||||||
|
cfg.pin_sclk = PIN_TFT_SCLK;
|
||||||
|
cfg.pin_mosi = PIN_TFT_MOSI;
|
||||||
|
cfg.pin_miso = PIN_TFT_MISO;
|
||||||
|
cfg.pin_dc = PIN_TFT_DC;
|
||||||
|
_bus.config(cfg);
|
||||||
|
_panel.setBus(&_bus);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto cfg = _panel.config();
|
||||||
|
cfg.pin_cs = PIN_TFT_CS;
|
||||||
|
cfg.pin_rst = PIN_TFT_RST;
|
||||||
|
cfg.panel_width = PANEL_NATIVE_W; // portrait native width
|
||||||
|
cfg.panel_height = PANEL_NATIVE_H; // portrait native height
|
||||||
|
cfg.offset_x = 0;
|
||||||
|
cfg.offset_y = 0;
|
||||||
|
cfg.invert = false;
|
||||||
|
_panel.config(cfg);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto cfg = _light.config();
|
||||||
|
cfg.pin_bl = PIN_TFT_BL;
|
||||||
|
_light.config(cfg);
|
||||||
|
_panel.setLight(&_light);
|
||||||
|
}
|
||||||
|
setPanel(&_panel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static LGFX tft;
|
||||||
|
|
||||||
|
static uint8_t* lvgl_buf = nullptr;
|
||||||
|
static constexpr size_t LVGL_BUF_BYTES = TFT_W * 20 * sizeof(lv_color_t);
|
||||||
|
|
||||||
|
uint16_t bg_sw = TFT_W, bg_sh = TFT_H;
|
||||||
|
|
||||||
|
// LVGL flush
|
||||||
|
static void lvgl_flush(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
|
||||||
|
uint32_t w = area->x2 - area->x1 + 1;
|
||||||
|
uint32_t h = area->y2 - area->y1 + 1;
|
||||||
|
tft.startWrite();
|
||||||
|
tft.setAddrWindow(area->x1, area->y1, w, h);
|
||||||
|
|
||||||
|
tft.writePixels((uint16_t*)px_map, w * h, false);
|
||||||
|
tft.endWrite();
|
||||||
|
lv_display_flush_ready(disp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message display state
|
||||||
|
|
||||||
|
// Flags stored per visible row
|
||||||
|
static constexpr uint8_t MSG_OWN = 0x01;
|
||||||
|
static constexpr uint8_t MSG_SYSTEM = 0x02;
|
||||||
|
|
||||||
|
static char msg_nicks[UI_VISIBLE_LINES][CHAT_MAX_NICK_LEN + 3];
|
||||||
|
static char msg_texts[UI_VISIBLE_LINES][CHAT_MAX_MSG_LEN];
|
||||||
|
static uint8_t msg_flags[UI_VISIBLE_LINES];
|
||||||
|
static int msg_count = 0; // how many rows are filled (0..UI_VISIBLE_LINES)
|
||||||
|
|
||||||
|
static lv_obj_t* header_peers = nullptr;
|
||||||
|
static lv_obj_t* header_nick = nullptr;
|
||||||
|
static lv_obj_t* input_label = nullptr;
|
||||||
|
|
||||||
|
struct MsgRow { lv_obj_t *row, *nick, *text; };
|
||||||
|
static MsgRow msg_rows[UI_VISIBLE_LINES];
|
||||||
|
|
||||||
|
static void refresh_msgs() {
|
||||||
|
int empty = UI_VISIBLE_LINES - msg_count;
|
||||||
|
for (int i = 0; i < UI_VISIBLE_LINES; i++) {
|
||||||
|
if (i < empty) {
|
||||||
|
lv_obj_add_flag(msg_rows[i].row, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lv_obj_clear_flag(msg_rows[i].row, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lv_color_t nick_color, text_color;
|
||||||
|
if (msg_flags[i] & MSG_SYSTEM) {
|
||||||
|
nick_color = text_color = g_settings.color_system;
|
||||||
|
} else if (msg_flags[i] & MSG_OWN) {
|
||||||
|
nick_color = g_settings.color_nick_own;
|
||||||
|
text_color = g_settings.color_msg;
|
||||||
|
} else {
|
||||||
|
nick_color = g_settings.color_nick_other;
|
||||||
|
text_color = g_settings.color_msg;
|
||||||
|
}
|
||||||
|
lv_obj_set_style_text_color(msg_rows[i].nick, nick_color, 0);
|
||||||
|
lv_obj_set_style_text_color(msg_rows[i].text, text_color, 0);
|
||||||
|
lv_label_set_text(msg_rows[i].nick, msg_nicks[i]);
|
||||||
|
lv_label_set_text(msg_rows[i].text, msg_texts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void strip_style(lv_obj_t* obj) {
|
||||||
|
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(obj, 0, 0);
|
||||||
|
lv_obj_set_style_radius(obj, 0, 0);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public
|
||||||
|
//
|
||||||
|
|
||||||
|
void ui_init() {
|
||||||
|
tft.init();
|
||||||
|
tft.setRotation(3); // usb on the right
|
||||||
|
tft.setBrightness(g_settings.brightness);
|
||||||
|
|
||||||
|
lvgl_buf = (uint8_t*)malloc(LVGL_BUF_BYTES);
|
||||||
|
if (!lvgl_buf) {
|
||||||
|
Serial.println("[ui] FATAL: LVGL draw buffer allocation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_init();
|
||||||
|
lv_tick_set_cb(millis);
|
||||||
|
|
||||||
|
lv_display_t* disp = lv_display_create(TFT_W, TFT_H);
|
||||||
|
lv_display_set_buffers(disp, lvgl_buf, nullptr, LVGL_BUF_BYTES, LV_DISPLAY_RENDER_MODE_PARTIAL);
|
||||||
|
lv_display_set_flush_cb(disp, lvgl_flush);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_chat_init() {
|
||||||
|
lv_obj_t* scr = lv_screen_active();
|
||||||
|
|
||||||
|
// Background
|
||||||
|
lv_obj_set_style_bg_color(scr, g_settings.color_bg, 0);
|
||||||
|
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
lv_obj_t* hdr = lv_obj_create(scr);
|
||||||
|
lv_obj_set_size(hdr, TFT_W, UI_HEADER_H);
|
||||||
|
lv_obj_set_pos(hdr, 0, 0);
|
||||||
|
lv_obj_set_style_bg_color(hdr, g_settings.color_header_bg, 0);
|
||||||
|
lv_obj_set_style_bg_opa(hdr, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_border_width(hdr, 1, 0);
|
||||||
|
lv_obj_set_style_border_color(hdr, lv_color_black(), 0);
|
||||||
|
lv_obj_set_style_border_side(hdr, LV_BORDER_SIDE_BOTTOM, 0);
|
||||||
|
lv_obj_set_style_pad_all(hdr, 0, 0);
|
||||||
|
lv_obj_set_style_radius(hdr, 0, 0);
|
||||||
|
lv_obj_clear_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
header_peers = lv_label_create(hdr);
|
||||||
|
lv_obj_set_style_text_color(header_peers, g_settings.color_msg, 0);
|
||||||
|
lv_obj_set_style_bg_opa(header_peers, LV_OPA_TRANSP, 0);
|
||||||
|
lv_label_set_long_mode(header_peers, LV_LABEL_LONG_CLIP);
|
||||||
|
lv_obj_set_width(header_peers, TFT_W / 2);
|
||||||
|
lv_obj_align(header_peers, LV_ALIGN_LEFT_MID, 4, 0);
|
||||||
|
lv_label_set_text(header_peers, "● 0 online");
|
||||||
|
|
||||||
|
header_nick = lv_label_create(hdr);
|
||||||
|
lv_obj_set_style_text_color(header_nick, g_settings.color_nick_own, 0);
|
||||||
|
lv_obj_set_style_bg_opa(header_nick, LV_OPA_TRANSP, 0);
|
||||||
|
lv_label_set_long_mode(header_nick, LV_LABEL_LONG_CLIP);
|
||||||
|
lv_obj_set_width(header_nick, TFT_W / 2 - 4);
|
||||||
|
lv_obj_align(header_nick, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||||
|
lv_obj_set_style_text_align(header_nick, LV_TEXT_ALIGN_RIGHT, 0);
|
||||||
|
lv_label_set_text_fmt(header_nick, "you: %s", g_settings.nick);
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
lv_obj_t* msg_area = lv_obj_create(scr);
|
||||||
|
lv_obj_set_size(msg_area, TFT_W, UI_MSG_AREA_H);
|
||||||
|
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_flex_align(msg_area, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < UI_VISIBLE_LINES; i++) {
|
||||||
|
lv_obj_t* row = lv_obj_create(msg_area);
|
||||||
|
lv_obj_set_width(row, TFT_W);
|
||||||
|
lv_obj_set_height(row, LV_SIZE_CONTENT);
|
||||||
|
strip_style(row);
|
||||||
|
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
|
||||||
|
LV_FLEX_ALIGN_START);
|
||||||
|
lv_obj_set_style_pad_column(row, 0, 0);
|
||||||
|
lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
|
lv_obj_t* nick = lv_label_create(row);
|
||||||
|
lv_obj_set_style_bg_opa(nick, LV_OPA_TRANSP, 0);
|
||||||
|
lv_label_set_long_mode(nick, LV_LABEL_LONG_CLIP);
|
||||||
|
lv_obj_set_height(nick, UI_LINE_H);
|
||||||
|
lv_obj_set_width(nick, LV_SIZE_CONTENT);
|
||||||
|
lv_label_set_text(nick, "");
|
||||||
|
|
||||||
|
lv_obj_t* text = lv_label_create(row);
|
||||||
|
lv_obj_set_style_bg_opa(text, LV_OPA_TRANSP, 0);
|
||||||
|
lv_label_set_long_mode(text, LV_LABEL_LONG_WRAP);
|
||||||
|
lv_obj_set_height(text, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_grow(text, 1);
|
||||||
|
lv_label_set_text(text, "");
|
||||||
|
|
||||||
|
msg_rows[i] = {row, nick, text};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer / input
|
||||||
|
lv_obj_t* inp = lv_obj_create(scr);
|
||||||
|
lv_obj_set_size(inp, TFT_W, UI_INPUT_H);
|
||||||
|
lv_obj_set_pos(inp, 0, UI_INPUT_Y);
|
||||||
|
lv_obj_set_style_bg_color(inp, g_settings.color_header_bg, 0);
|
||||||
|
lv_obj_set_style_bg_opa(inp, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_border_width(inp, 1, 0);
|
||||||
|
lv_obj_set_style_border_color(inp, lv_color_black(), 0);
|
||||||
|
lv_obj_set_style_border_side(inp, LV_BORDER_SIDE_TOP, 0);
|
||||||
|
lv_obj_set_style_pad_all(inp, 0, 0);
|
||||||
|
lv_obj_set_style_radius(inp, 0, 0);
|
||||||
|
lv_obj_clear_flag(inp, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
input_label = lv_label_create(inp);
|
||||||
|
lv_obj_set_style_text_color(input_label, g_settings.color_input_prompt, 0);
|
||||||
|
lv_obj_set_style_bg_opa(input_label, LV_OPA_TRANSP, 0);
|
||||||
|
lv_label_set_long_mode(input_label, LV_LABEL_LONG_CLIP);
|
||||||
|
lv_obj_set_width(input_label, TFT_W - 8);
|
||||||
|
lv_obj_align(input_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||||
|
lv_label_set_text(input_label, "> ");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_push_message(const char* nick, const char* text, bool is_own, bool is_system) {
|
||||||
|
// Shift buffer up by one
|
||||||
|
for (int i = 0; i < UI_VISIBLE_LINES - 1; i++) {
|
||||||
|
memcpy(msg_nicks[i], msg_nicks[i + 1], sizeof(msg_nicks[0]));
|
||||||
|
memcpy(msg_texts[i], msg_texts[i + 1], sizeof(msg_texts[0]));
|
||||||
|
msg_flags[i] = msg_flags[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
int last = UI_VISIBLE_LINES - 1;
|
||||||
|
if (is_system || nick[0] == '\0') {
|
||||||
|
msg_nicks[last][0] = '\0';
|
||||||
|
} else {
|
||||||
|
snprintf(msg_nicks[last], sizeof(msg_nicks[0]), "%s: ", nick);
|
||||||
|
}
|
||||||
|
strncpy(msg_texts[last], text, sizeof(msg_texts[0]) - 1);
|
||||||
|
msg_texts[last][sizeof(msg_texts[0]) - 1] = '\0';
|
||||||
|
msg_flags[last] = (is_own ? MSG_OWN : 0) | (is_system ? MSG_SYSTEM : 0);
|
||||||
|
|
||||||
|
if (msg_count < UI_VISIBLE_LINES) msg_count++;
|
||||||
|
|
||||||
|
refresh_msgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_set_input(const char* text) {
|
||||||
|
if (!input_label) return;
|
||||||
|
lv_label_set_text_fmt(input_label, "> %s", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_set_header(int peer_count, const char* my_nick) {
|
||||||
|
if (!header_peers || !header_nick) return;
|
||||||
|
lv_label_set_text_fmt(header_peers, "* %d online", peer_count);
|
||||||
|
lv_label_set_text_fmt(header_nick, "you: %s", my_nick);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_set_header_mode(const char* text) {
|
||||||
|
if (!header_peers || !header_nick) return;
|
||||||
|
lv_label_set_text(header_peers, "");
|
||||||
|
lv_label_set_text(header_nick, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_clear_messages() {
|
||||||
|
memset(msg_nicks, 0, sizeof(msg_nicks));
|
||||||
|
memset(msg_texts, 0, sizeof(msg_texts));
|
||||||
|
memset(msg_flags, 0, sizeof(msg_flags));
|
||||||
|
msg_count = 0;
|
||||||
|
refresh_msgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ui_tick() {
|
||||||
|
lv_timer_handler();
|
||||||
|
}
|
||||||
23
src/meshberry/ui.h
Normal file
23
src/meshberry/ui.h
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
void ui_init();
|
||||||
|
|
||||||
|
void ui_chat_init();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add one message to the ui.
|
||||||
|
* nick senders nickname ("" for system messages)
|
||||||
|
* text message body (system messages include their own prefix, e.g. "* alice joined")
|
||||||
|
* is_own true: nick rendered in color_nick_own
|
||||||
|
* is_system true: entire line rendered in color_system, nick ignored
|
||||||
|
*/
|
||||||
|
void ui_push_message(const char* nick, const char* text, bool is_own, bool is_system);
|
||||||
|
void ui_set_input(const char* text);
|
||||||
|
void ui_set_header(int peer_count, const char* my_nick);
|
||||||
|
void ui_set_header_mode(const char* text);
|
||||||
|
void ui_clear_messages();
|
||||||
|
|
||||||
|
// Must be called every iteration of loop().
|
||||||
|
void ui_tick();
|
||||||
Loading…
Reference in a new issue