Compare commits
56 Commits
v0.1.0-bet
...
main
Author | SHA1 | Date | |
---|---|---|---|
0f2598b600 | |||
03a84e8301 | |||
dbca0988eb | |||
1b00c01641 | |||
a15fae2fd9 | |||
a530492937 | |||
8950faf44a | |||
a5e208755e | |||
995529af02 | |||
b0b1ee297d | |||
9d19af9c95 | |||
4aa62e9982 | |||
847963c2ab | |||
e697bf6790 | |||
6dc045e66f | |||
e77b192dee | |||
68b86a1d6d | |||
4b4be4c9db | |||
fb5784188e | |||
da18703357 | |||
bd15b524fb | |||
eaf5cdfc8d | |||
b00d779471 | |||
5845a84250 | |||
0cfe90ff99 | |||
d4911ee2d3 | |||
86b4488d92 | |||
e991577a77 | |||
b2506fb589 | |||
66845d0dcf | |||
12a437ff69 | |||
885c0f57c1 | |||
b3f87b2fc4 | |||
efdd8bcb34 | |||
|
5312071d1c | ||
|
432ccdca90 | ||
|
252e97f139 | ||
|
711e8dee5b | ||
|
8ff6e5811e | ||
77a9144837 | |||
4afe369d27 | |||
462ed146c9 | |||
833d15db65 | |||
a590dfccc8 | |||
6bb2532038 | |||
527eb75d67 | |||
eec732eb29 | |||
0d59ab3b7e | |||
8e3f702c98 | |||
af93986495 | |||
f4dc64fd69 | |||
2445482bfd | |||
e620ad7a76 | |||
b212090e91 | |||
0289343061 | |||
bf090d5d88 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
github: acidvegas
|
||||
patreon: acid_drop
|
||||
buy_me_a_coffee: acidvegas
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +1,2 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
.vscode
|
||||
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 322 KiB |
BIN
.screens/lvgl.png
Normal file
BIN
.screens/lvgl.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
Before Width: | Height: | Size: 377 KiB After Width: | Height: | Size: 377 KiB |
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@ -1,10 +0,0 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
56
.vscode/settings.json
vendored
56
.vscode/settings.json
vendored
@ -1,56 +0,0 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"*.tcc": "cpp",
|
||||
"cctype": "cpp",
|
||||
"chrono": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"vector": "cpp",
|
||||
"exception": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"functional": "cpp",
|
||||
"iterator": "cpp",
|
||||
"map": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"string": "cpp",
|
||||
"string_view": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"fstream": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"limits": "cpp",
|
||||
"new": "cpp",
|
||||
"ostream": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"thread": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"typeinfo": "cpp"
|
||||
}
|
||||
}
|
73
README.md
73
README.md
@ -2,20 +2,16 @@
|
||||
<img src="./.screens/aciddrop2.png" />
|
||||
</p>
|
||||
|
||||
|
||||
# Work in progress
|
||||
This is a custom firmware being developed for the [LilyGo T-Deck](https://www.lilygo.cc/products/t-deck), currently it is experimental & buggy.
|
||||
This is a custom firmware being developed for the [LilyGo T-Deck](https://www.lilygo.cc/products/t-deck), currently it is experimental & buggy while we are in beta status.
|
||||
|
||||
I am terrible at C++ and still am trying to wrap my head around the language's logic, so most of this code has been shit out and written like a cowboy.
|
||||
If you are familiar with or use [Internet Relay Chat](https://en.wikipedia.org/wiki/IRC), we have a team of developers working on this project in **#comms** on **[irc.supernets.org](irc://irc.supernets.org)**, join us if you have ideas, bugs, or want to get your hands dirty & develope this project with us.
|
||||
|
||||
The project started out with as IRC bridge for [Meshtastic](https://meshtastic.org/) over MQTT, just so I could say "we have IRC over radio", which later led to me modifying the official Meshtastic [firmware](https://github.com/meshtastic/firmware) to add extended features. I am deep in the rabbit hole of embedded development and am starting from scratch making this an entirely custom firmware.
|
||||
Consider sponsoring our project, all the money goes towards motivation to develope on this, we also like buying T-Decks for people who want to learn about this stuff!
|
||||
|
||||
This is being developed in my free time as a fun project. It is no where near being useful.
|
||||
![](./.screens/preview.png)
|
||||
|
||||
A compiled "release" will be done once I finish somoe fo the basic features, but feel free t compile this on your own to test it out!
|
||||
|
||||
# Previews
|
||||
![](./.screens/preview1.png) ![](./.screens/preview2.png)
|
||||
![](./.screens/lvgl.png)
|
||||
|
||||
# Flashing the Firmware
|
||||
###### Using VS Code
|
||||
@ -25,23 +21,26 @@ A compiled "release" will be done once I finish somoe fo the basic features, but
|
||||
4. Hold down the trackball on the device, turn it on, and plug it in to the computer.
|
||||
5. Press **F1** and select `PlatformIO: Build`
|
||||
6. Press **F1** and select `PlatformIO: Upload`
|
||||
7. Press the RST *(reset)* button ont he device.
|
||||
7. Press the RST *(reset)* button on the device.
|
||||
|
||||
###### Using ESP Tool
|
||||
1. Take the `firmware.bin` file from the release page and download it. *(no releases yet, we still building)*
|
||||
1. Take the `firmware.bin` file from the release page and download it.
|
||||
2. Install [esptool](https://pypi.org/project/esptool/): `pip install esptool`
|
||||
3. Hold down the trackball on the device, turn it on, and plug it in to the computer.
|
||||
4. Confirm the serial device in your `/dev` directory *(Your device will likely be `/dev/ttyAMC0` or `/dev/ttyUSB0`)*
|
||||
5. Flash the device: `esptool.py --chip esp32-s3 --port /dev/ttyUSB0 --baud 115200 write_flash -z 0x1000 firmware.bin`
|
||||
6. Press the RST *(reset)* button ont he device.
|
||||
6. Press the RST *(reset)* button on the device.
|
||||
|
||||
# Connecting to WiFi
|
||||
The device will scan for WiFi networks on boot. Once the list is displayed, you can scroll up and down the list with the "u" key for UP and the "d" key for down.
|
||||
# Command & Control
|
||||
###### Menu controls
|
||||
On boot, if you press the `w` key, it will wipe all of the stored preferences.
|
||||
|
||||
# Commands
|
||||
The device will scan for WiFi networks on boot. Once the list is displayed, you can scroll up and down the list with the `u` key for UP and the `d` key for down.
|
||||
|
||||
###### IRC commands
|
||||
| Command | Description |
|
||||
| --------------- | --------------------------- |
|
||||
| `/debug` | Show hardware information |
|
||||
| `/info` | Show hardware information |
|
||||
| `/me <message>` | Send an ACTION message |
|
||||
| `/nick <new>` | Change your NICK on IRC |
|
||||
| `/raw <data>` | Send RAW data to the server |
|
||||
@ -54,13 +53,20 @@ The device will scan for WiFi networks on boot. Once the list is displayed, you
|
||||
# Roapmap
|
||||
###### Device functionality
|
||||
- [X] Screen timeout on inactivity *(default 30 seconds)*
|
||||
- [ ] Keyboard backlight timeout on 10 seconds oof inactivity.
|
||||
- [ ] Keyboard backlight timeout with screen timeout
|
||||
- [ ] Trackball support
|
||||
- [ ] Speaker support
|
||||
- [X] Speaker support
|
||||
- [X] Bootup sounds
|
||||
- [X] IRC mention sounds
|
||||
- [ ] GPS support
|
||||
- [ ] Lora support
|
||||
- [ ] BLE support
|
||||
- [ ] SD card support
|
||||
|
||||
###### Features
|
||||
- [X] LVGL used for enhanced UI
|
||||
- [X] Wifi scanning & selection menu
|
||||
- [ ] Saved wifi profiles
|
||||
- [x] Saved wifi profiles
|
||||
- [ ] Wifi Hotspot
|
||||
- [ ] Notifcations Window *(All notifications will go here, from IRC, Gotify, Meshtastic, or anything)*
|
||||
- [X] Status bar *(Time, Date, Notification, Wifi, and Battery)*
|
||||
@ -70,33 +76,38 @@ The device will scan for WiFi networks on boot. Once the list is displayed, you
|
||||
- [X] Serial debug logs
|
||||
|
||||
###### Applications
|
||||
- [ ] Rubber Ducky
|
||||
- [X] IRC Client
|
||||
- [X] `/raw` command for IRC client to send raw data to the server
|
||||
- [ ] Add scrolling backlog for IRC to see the last 200 messages
|
||||
- [ ] Multi-buffer support *(`/join` & `/part` support with switching between buffers with `/0`, `/1`, `/2`, etc)* *(`/close` also for PM buffers or kicked from channels)*
|
||||
- [ ] Status window for network to show RAW lines from the IRC server *(buffer 0)*
|
||||
- [ ] Hilight monitor buffer
|
||||
- [X] Hilight support *(so we can see when people mention our NICK)*
|
||||
- [X] 99 color support
|
||||
- [ ] `/pm` support *(it should open a buffer for pms)*
|
||||
- [ ] NickServ support
|
||||
- [ ] ChatGPT
|
||||
- [ ] SSH Client
|
||||
- [ ] Wardriving
|
||||
- [ ] Evil Portal AP
|
||||
- [ ] Local Network Probe *(Scans for devices on the wifi network you are connected to, add port scanning)*
|
||||
- [ ] Gotify
|
||||
- [ ] Meshtastic
|
||||
- [ ] Gotify *(in progress)*
|
||||
- [ ] Meshtastic *(in progress)*
|
||||
- [ ] Spotify/Music player *(can we play audio throuigh Bluetoth headphones or the on-board speaker?)*
|
||||
- [ ] Syslog *(All serial logs will be displayed here for on-device debugging)*
|
||||
|
||||
# Known issues
|
||||
- Messages that exceed the screen width and wrap to the next line will throw off thje logic of calculating the max lines able to be displayed on the screen. Messages eventually go off screen.
|
||||
- Input messages exceeding the screen width go off screen, and need to wrap and create a new input line, pushing up the backlog of messages.
|
||||
- Some colors are not working when its only a background color or only contains a space
|
||||
- Sometimes it will not connect to IRC even from reconnections, and you have to reboot and try again.
|
||||
- Screen timeout can occur during an IRC reconnection and blck keyboard input
|
||||
# Ideas
|
||||
- Replace the `ESP32-S3FN16R8` with a `ESP32-S3-WROOM-1U` which has an iPex connector for an external WiFi antenna.
|
||||
|
||||
# Contributors
|
||||
Join us in **#comms** on **[irc.supernets.org](irc://irc.supernets.org)** if you want to get your hands dirty.
|
||||
|
||||
# More screens..
|
||||
# Previews
|
||||
###### 99 Color support
|
||||
![](./.screens/99colors.png)
|
||||
|
||||
###### Full ASCII support for PUMPERS
|
||||
![](./.screens/ascii.png)
|
||||
|
||||
###### Support for /HUEG
|
||||
![](./.screens/hueg.png)
|
||||
|
||||
___
|
||||
|
BIN
firmware/acid-drop_v0.1.0_beta.bin
Normal file
BIN
firmware/acid-drop_v0.1.0_beta.bin
Normal file
Binary file not shown.
@ -1,3 +1,24 @@
|
||||
// Added by acidvegas to hide warnings during build
|
||||
#ifdef TFT_MISO
|
||||
#undef TFT_MISO
|
||||
#endif
|
||||
#ifdef TFT_MOSI
|
||||
#undef TFT_MOSI
|
||||
#endif
|
||||
#ifdef TFT_CS
|
||||
#undef TFT_CS
|
||||
#endif
|
||||
#ifdef TFT_DC
|
||||
#undef TFT_DC
|
||||
#endif
|
||||
#ifdef TFT_RST
|
||||
#undef TFT_RST
|
||||
#endif
|
||||
#ifdef TFT_BL
|
||||
#undef TFT_BL
|
||||
#endif
|
||||
|
||||
|
||||
// ST7789 240 x 280 display with no chip select line
|
||||
#define USER_SETUP_ID 210
|
||||
|
||||
|
@ -22,7 +22,9 @@ board_build.partitions = default_16MB.csv
|
||||
build_flags =
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DDISABLE_ALL_LIBRARY_WARNINGS
|
||||
lib_deps =
|
||||
;mikalhart/TinyGPSPlus@^1.0.2
|
||||
marian-craciunescu/ESP32Ping@^1.7.0
|
||||
;sandeepmistry/LoRa
|
||||
ArduinoWebsockets
|
||||
Wireguard-ESP32
|
||||
earlephilhower/ESP8266Audio
|
||||
lib_extra_dirs = lib
|
721
src/Display.cpp
Normal file
721
src/Display.cpp
Normal file
@ -0,0 +1,721 @@
|
||||
#include "bootScreen.h"
|
||||
#include "Display.h"
|
||||
#include "IRC.h"
|
||||
#include "pins.h"
|
||||
#include "Speaker.h"
|
||||
#include "Storage.h"
|
||||
#include "Utilities.h"
|
||||
|
||||
|
||||
bool infoScreen = false;
|
||||
bool configScreen = false;
|
||||
bool screenOn = true;
|
||||
const char* channel = "#comms";
|
||||
unsigned long infoScreenStartTime = 0;
|
||||
unsigned long configScreenStartTime = 0;
|
||||
unsigned long lastStatusUpdateTime = 0;
|
||||
unsigned long lastActivityTime = 0;
|
||||
String inputBuffer = "";
|
||||
|
||||
std::vector<String> lines;
|
||||
std::vector<bool> mentions;
|
||||
std::map<String, uint32_t> nickColors;
|
||||
|
||||
TFT_eSPI tft = TFT_eSPI();
|
||||
|
||||
|
||||
void addLine(String senderNick, String message, String type, bool mention, uint16_t errorColor, uint16_t reasonColor) {
|
||||
if (type != "error" && nickColors.find(senderNick) == nickColors.end())
|
||||
nickColors[senderNick] = generateRandomColor();
|
||||
|
||||
String formattedMessage;
|
||||
|
||||
if (type == "join") {
|
||||
formattedMessage = "JOIN " + senderNick + " has joined " + String(channel);
|
||||
} else if (type == "part") {
|
||||
formattedMessage = "PART " + senderNick + " has EMO-QUIT " + String(channel);
|
||||
} else if (type == "quit") {
|
||||
formattedMessage = "QUIT " + senderNick;
|
||||
} else if (type == "nick") {
|
||||
int arrowPos = message.indexOf(" -> ");
|
||||
String oldNick = senderNick;
|
||||
String newNick = message.substring(arrowPos + 4);
|
||||
if (nickColors.find(newNick) == nickColors.end()) {
|
||||
nickColors[newNick] = generateRandomColor();
|
||||
}
|
||||
formattedMessage = "NICK " + oldNick + " -> " + newNick;
|
||||
} else if (type == "kick") {
|
||||
formattedMessage = "KICK " + senderNick + message;
|
||||
} else if (type == "mode") {
|
||||
formattedMessage = "MODE " + message;
|
||||
tft.setTextColor(TFT_BLUE);
|
||||
} else if (type == "action") {
|
||||
formattedMessage = "* " + senderNick + " " + message;
|
||||
} else if (type == "error") {
|
||||
formattedMessage = "ERROR " + message;
|
||||
senderNick = "ERROR";
|
||||
} else {
|
||||
formattedMessage = senderNick + ": " + message;
|
||||
}
|
||||
|
||||
int linesRequired = calculateLinesRequired(formattedMessage);
|
||||
|
||||
while (lines.size() + linesRequired > MAX_LINES) {
|
||||
lines.erase(lines.begin());
|
||||
mentions.erase(mentions.begin());
|
||||
}
|
||||
|
||||
if (type == "error") {
|
||||
lines.push_back("ERROR " + message);
|
||||
mentions.push_back(false);
|
||||
} else {
|
||||
lines.push_back(formattedMessage);
|
||||
mentions.push_back(mention);
|
||||
}
|
||||
|
||||
displayLines();
|
||||
}
|
||||
|
||||
|
||||
int calculateLinesRequired(String message) {
|
||||
int linesRequired = 1;
|
||||
int lineWidth = 0;
|
||||
|
||||
for (unsigned int i = 0; i < message.length(); i++) {
|
||||
char c = message[i];
|
||||
if (c == '\x03') {
|
||||
if (i + 1 < message.length() && isdigit(message[i + 1])) {
|
||||
i++;
|
||||
if (i + 1 < message.length() && isdigit(message[i + 1]))
|
||||
i++;
|
||||
}
|
||||
if (i + 1 < message.length() && message[i + 1] == ',' && isdigit(message[i + 2])) {
|
||||
i += 2;
|
||||
if (i + 1 < message.length() && isdigit(message[i + 1]))
|
||||
i++;
|
||||
}
|
||||
} else if (c != '\x02' && c != '\x0F' && c != '\x1F') {
|
||||
lineWidth += tft.textWidth(String(c));
|
||||
if (lineWidth > SCREEN_WIDTH) {
|
||||
linesRequired++;
|
||||
lineWidth = tft.textWidth(String(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return linesRequired;
|
||||
}
|
||||
|
||||
|
||||
void displayCenteredText(String text) {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.drawString(text, SCREEN_WIDTH / 2, (SCREEN_HEIGHT + STATUS_BAR_HEIGHT) / 2);
|
||||
}
|
||||
|
||||
|
||||
void displayInputLine() {
|
||||
tft.fillRect(0, SCREEN_HEIGHT - INPUT_LINE_HEIGHT, SCREEN_WIDTH, INPUT_LINE_HEIGHT, TFT_BLACK);
|
||||
tft.setCursor(0, SCREEN_HEIGHT - INPUT_LINE_HEIGHT);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
tft.setTextSize(1);
|
||||
|
||||
String displayInput = inputBuffer;
|
||||
int displayWidth = tft.textWidth(displayInput);
|
||||
int inputWidth = SCREEN_WIDTH - tft.textWidth("> ");
|
||||
|
||||
while (displayWidth > inputWidth) {
|
||||
displayInput = displayInput.substring(1);
|
||||
displayWidth = tft.textWidth(displayInput);
|
||||
}
|
||||
|
||||
if (inputBuffer.length() >= 510)
|
||||
tft.setTextColor(TFT_RED);
|
||||
|
||||
tft.print("> " + displayInput);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
}
|
||||
|
||||
|
||||
void displayLines() {
|
||||
tft.fillRect(0, STATUS_BAR_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT - STATUS_BAR_HEIGHT - INPUT_LINE_HEIGHT, TFT_BLACK);
|
||||
|
||||
int cursorY = STATUS_BAR_HEIGHT;
|
||||
int totalLinesHeight = 0;
|
||||
std::vector<int> lineHeights;
|
||||
|
||||
for (const String& line : lines) {
|
||||
int lineHeight = calculateLinesRequired(line) * CHAR_HEIGHT;
|
||||
lineHeights.push_back(lineHeight);
|
||||
totalLinesHeight += lineHeight;
|
||||
}
|
||||
|
||||
while (totalLinesHeight > SCREEN_HEIGHT - STATUS_BAR_HEIGHT - INPUT_LINE_HEIGHT) {
|
||||
totalLinesHeight -= lineHeights.front();
|
||||
lines.erase(lines.begin());
|
||||
mentions.erase(mentions.begin());
|
||||
lineHeights.erase(lineHeights.begin());
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
const String& line = lines[i];
|
||||
bool mention = mentions[i];
|
||||
|
||||
tft.setCursor(0, cursorY);
|
||||
|
||||
if (line.startsWith("JOIN ")) {
|
||||
tft.setTextColor(TFT_GREEN);
|
||||
tft.print("JOIN ");
|
||||
int startIndex = 5;
|
||||
int endIndex = line.indexOf(" has joined ");
|
||||
String senderNick = line.substring(startIndex, endIndex);
|
||||
tft.setTextColor(nickColors[senderNick]);
|
||||
tft.print(senderNick);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
tft.print(" has joined ");
|
||||
tft.setTextColor(TFT_CYAN);
|
||||
tft.print(channel);
|
||||
cursorY += CHAR_HEIGHT;
|
||||
} else if (line.startsWith("PART ")) {
|
||||
tft.setTextColor(TFT_RED);
|
||||
tft.print("PART ");
|
||||
int startIndex = 5;
|
||||
int endIndex = line.indexOf(" has EMO-QUIT ");
|
||||
String senderNick = line.substring(startIndex, endIndex);
|
||||
tft.setTextColor(nickColors[senderNick]);
|
||||
tft.print(senderNick);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
tft.print(" has EMO-QUIT ");
|
||||
tft.setTextColor(TFT_CYAN);
|
||||
tft.print(channel);
|
||||
cursorY += CHAR_HEIGHT;
|
||||
} else if (line.startsWith("QUIT ")) {
|
||||
tft.setTextColor(TFT_RED);
|
||||
tft.print("QUIT ");
|
||||
String senderNick = line.substring(5);
|
||||
tft.setTextColor(nickColors[senderNick]);
|
||||
tft.print(senderNick);
|
||||
cursorY += CHAR_HEIGHT;
|
||||
} else if (line.startsWith("NICK ")) {
|
||||
tft.setTextColor(TFT_BLUE);
|
||||
tft.print("NICK ");
|
||||
int startIndex = 5;
|
||||
int endIndex = line.indexOf(" -> ");
|
||||
String oldNick = line.substring(startIndex, endIndex);
|
||||
String newNick = line.substring(endIndex + 4);
|
||||
tft.setTextColor(nickColors[oldNick]);
|
||||
tft.print(oldNick);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
tft.print(" -> ");
|
||||
tft.setTextColor(nickColors[newNick]);
|
||||
tft.print(newNick);
|
||||
cursorY += CHAR_HEIGHT;
|
||||
} else if (line.startsWith("KICK ")) {
|
||||
tft.setTextColor(TFT_RED);
|
||||
tft.print("KICK ");
|
||||
int startIndex = 5;
|
||||
int endIndex = line.indexOf(" by ");
|
||||
String kickedNick = line.substring(startIndex, endIndex);
|
||||
String kicker = line.substring(endIndex + 4);
|
||||
tft.setTextColor(nickColors[kickedNick]);
|
||||
tft.print(kickedNick);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
tft.print(" by ");
|
||||
tft.setTextColor(nickColors[kicker]);
|
||||
tft.print(kicker);
|
||||
cursorY += CHAR_HEIGHT;
|
||||
} else if (line.startsWith("MODE ")) {
|
||||
tft.setTextColor(TFT_BLUE);
|
||||
tft.print("MODE ");
|
||||
String modeChange = line.substring(5);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
tft.print(modeChange);
|
||||
cursorY += CHAR_HEIGHT;
|
||||
} else if (line.startsWith("ERROR ")) {
|
||||
tft.setTextColor(TFT_RED);
|
||||
tft.print("ERROR ");
|
||||
String errorReason = line.substring(6);
|
||||
tft.setTextColor(TFT_DARKGREY);
|
||||
tft.print(errorReason);
|
||||
cursorY += CHAR_HEIGHT;
|
||||
} else if (line.startsWith("* ")) {
|
||||
tft.setTextColor(TFT_MAGENTA);
|
||||
tft.print("* ");
|
||||
int startIndex = 2;
|
||||
int endIndex = line.indexOf(' ', startIndex);
|
||||
String senderNick = line.substring(startIndex, endIndex);
|
||||
String actionMessage = line.substring(endIndex + 1);
|
||||
tft.setTextColor(nickColors[senderNick]);
|
||||
tft.print(senderNick);
|
||||
tft.setTextColor(TFT_MAGENTA);
|
||||
tft.print(" " + actionMessage);
|
||||
cursorY += CHAR_HEIGHT;
|
||||
} else {
|
||||
int colonIndex = line.indexOf(':');
|
||||
String senderNick = line.substring(0, colonIndex);
|
||||
String message = line.substring(colonIndex + 1);
|
||||
|
||||
tft.setTextColor(nickColors[senderNick]);
|
||||
tft.print(senderNick);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
cursorY = renderFormattedMessage(":" + message, cursorY, CHAR_HEIGHT, mention);
|
||||
}
|
||||
}
|
||||
|
||||
displayInputLine();
|
||||
}
|
||||
|
||||
|
||||
void displayXBM() {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
|
||||
int x = (SCREEN_WIDTH - logo_width) / 2;
|
||||
int y = (SCREEN_HEIGHT - logo_height) / 2;
|
||||
|
||||
tft.drawXBitmap(x, y, logo_bits, logo_width, logo_height, TFT_GREEN);
|
||||
|
||||
unsigned long startTime = millis();
|
||||
bool wipeInitiated = false;
|
||||
|
||||
while (millis() - startTime < 3000) {
|
||||
if (getKeyboardInput() == 'w') {
|
||||
wipeNVS();
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
displayCenteredText("NVS WIPED");
|
||||
delay(2000);
|
||||
wipeInitiated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uint32_t generateRandomColor() {
|
||||
return tft.color565(random(0, 255), random(0, 255), random(0, 255));
|
||||
}
|
||||
|
||||
|
||||
uint16_t getColorFromCode(int colorCode) {
|
||||
switch (colorCode) {
|
||||
case 0: return TFT_WHITE;
|
||||
case 1: return TFT_BLACK;
|
||||
case 2: return tft.color565(0, 0, 128); // Dark Blue (Navy)
|
||||
case 3: return TFT_GREEN;
|
||||
case 4: return TFT_RED;
|
||||
case 5: return tft.color565(128, 0, 0); // Brown (Maroon)
|
||||
case 6: return tft.color565(128, 0, 128); // Purple
|
||||
case 7: return tft.color565(255, 165, 0); // Orange
|
||||
case 8: return TFT_YELLOW;
|
||||
case 9: return tft.color565(144, 238, 144); // Light Green
|
||||
case 10: return tft.color565(0, 255, 255); // Cyan (Light Blue)
|
||||
case 11: return tft.color565(224, 255, 255); // Light Cyan (Aqua)
|
||||
case 12: return TFT_BLUE;
|
||||
case 13: return tft.color565(255, 192, 203); // Pink (Light Purple)
|
||||
case 14: return tft.color565(128, 128, 128); // Grey
|
||||
case 15: return tft.color565(211, 211, 211); // Light Grey
|
||||
case 16: return 0x4000;
|
||||
case 17: return 0x4100;
|
||||
case 18: return 0x4220;
|
||||
case 19: return 0x3220;
|
||||
case 20: return 0x0220;
|
||||
case 21: return 0x0225;
|
||||
case 22: return 0x0228;
|
||||
case 23: return 0x0128;
|
||||
case 24: return 0x0008;
|
||||
case 25: return 0x2808;
|
||||
case 26: return 0x4008;
|
||||
case 27: return 0x4005;
|
||||
case 28: return 0x7000;
|
||||
case 29: return 0x71C0;
|
||||
case 30: return 0x73A0;
|
||||
case 31: return 0x53A0;
|
||||
case 32: return 0x03A0;
|
||||
case 33: return 0x03A9;
|
||||
case 34: return 0x03AE;
|
||||
case 35: return 0x020E;
|
||||
case 36: return 0x000E;
|
||||
case 37: return 0x480E;
|
||||
case 38: return 0x700E;
|
||||
case 39: return 0x7008;
|
||||
case 40: return 0xB000;
|
||||
case 41: return 0xB300;
|
||||
case 42: return 0xB5A0;
|
||||
case 43: return 0x7DA0;
|
||||
case 44: return 0x05A0;
|
||||
case 45: return 0x05AE;
|
||||
case 46: return 0x05B6;
|
||||
case 47: return 0x0316;
|
||||
case 48: return 0x0016;
|
||||
case 49: return 0x7016;
|
||||
case 50: return 0xB016;
|
||||
case 51: return 0xB00D;
|
||||
case 52: return 0xF800;
|
||||
case 53: return 0xFC60;
|
||||
case 54: return 0xFFE0;
|
||||
case 55: return 0xB7E0;
|
||||
case 56: return 0x07E0;
|
||||
case 57: return 0x07F4;
|
||||
case 58: return 0x07FF;
|
||||
case 59: return 0x047F;
|
||||
case 60: return 0x001F;
|
||||
case 61: return 0xA01F;
|
||||
case 62: return 0xF81F;
|
||||
case 63: return 0xF813;
|
||||
case 64: return 0xFACB;
|
||||
case 65: return 0xFDAB;
|
||||
case 66: return 0xFFEE;
|
||||
case 67: return 0xCFEC;
|
||||
case 68: return 0x6FED;
|
||||
case 69: return 0x67F9;
|
||||
case 70: return 0x6FFF;
|
||||
case 71: return 0x5DBF;
|
||||
case 72: return 0x5ADF;
|
||||
case 73: return 0xC2DF;
|
||||
case 74: return 0xFB3F;
|
||||
case 75: return 0xFAD7;
|
||||
case 76: return 0xFCF3;
|
||||
case 77: return 0xFE93;
|
||||
case 78: return 0xFFF3;
|
||||
case 79: return 0xE7F3;
|
||||
case 80: return 0x9FF3;
|
||||
case 81: return 0x9FFB;
|
||||
case 82: return 0x9FFF;
|
||||
case 83: return 0x9E9F;
|
||||
case 84: return 0x9CFF;
|
||||
case 85: return 0xDCFF;
|
||||
case 86: return 0xFCFF;
|
||||
case 87: return 0xFCBA;
|
||||
case 88: return 0x0000;
|
||||
case 89: return 0x1082;
|
||||
case 90: return 0x2945;
|
||||
case 91: return 0x31A6;
|
||||
case 92: return 0x4A69;
|
||||
case 93: return 0x632C;
|
||||
case 94: return 0x8410;
|
||||
case 95: return 0x9CF3;
|
||||
case 96: return 0xBDF7;
|
||||
case 97: return 0xE71C;
|
||||
case 98: return 0xFFFF;
|
||||
default: return TFT_WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uint16_t getColorFromPercentage(int percentage) {
|
||||
if (percentage > 75) return TFT_GREEN;
|
||||
else if (percentage > 50) return TFT_YELLOW;
|
||||
else if (percentage > 25) return TFT_ORANGE;
|
||||
else return TFT_RED;
|
||||
}
|
||||
|
||||
|
||||
void handleKeyboardInput(char key) {
|
||||
lastActivityTime = millis();
|
||||
|
||||
if (!screenOn) {
|
||||
turnOnScreen();
|
||||
screenOn = true;
|
||||
return;
|
||||
}
|
||||
|
||||
static bool altPressed = false;
|
||||
|
||||
if (key == '\n' || key == '\r') {
|
||||
if (inputBuffer.startsWith("/nick ")) {
|
||||
String newNick = inputBuffer.substring(6);
|
||||
sendIRC("NICK " + newNick);
|
||||
inputBuffer = "";
|
||||
} else if (inputBuffer.startsWith("/config")) {
|
||||
configScreen = true;
|
||||
configScreenStartTime = millis();
|
||||
inputBuffer = "";
|
||||
} else if (inputBuffer.startsWith("/info")) {
|
||||
infoScreen = true;
|
||||
infoScreenStartTime = millis();
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
printDeviceInfo();
|
||||
inputBuffer = "";
|
||||
} else if (inputBuffer.startsWith("/raw ")) {
|
||||
String rawCommand = inputBuffer.substring(5);
|
||||
sendIRC(rawCommand);
|
||||
} else if (inputBuffer.startsWith("/me ")) {
|
||||
String actionMessage = inputBuffer.substring(4);
|
||||
sendIRC("PRIVMSG " + String(channel) + " :\001ACTION " + actionMessage + "\001");
|
||||
addLine(irc_nickname, actionMessage, "action");
|
||||
inputBuffer = "";
|
||||
} else {
|
||||
sendIRC("PRIVMSG " + String(channel) + " :" + inputBuffer);
|
||||
addLine(irc_nickname, inputBuffer, "message");
|
||||
}
|
||||
inputBuffer = "";
|
||||
displayInputLine();
|
||||
} else if (key == '\b') {
|
||||
if (inputBuffer.length() > 0) {
|
||||
inputBuffer.remove(inputBuffer.length() - 1);
|
||||
displayInputLine();
|
||||
}
|
||||
} else {
|
||||
inputBuffer += key;
|
||||
displayInputLine();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void parseAndDisplay(String line) {
|
||||
int firstSpace = line.indexOf(' ');
|
||||
int secondSpace = line.indexOf(' ', firstSpace + 1);
|
||||
|
||||
if (firstSpace != -1 && secondSpace != -1) {
|
||||
String command = line.substring(firstSpace + 1, secondSpace);
|
||||
|
||||
if (command == "PRIVMSG") {
|
||||
int thirdSpace = line.indexOf(' ', secondSpace + 1);
|
||||
String target = line.substring(secondSpace + 1, thirdSpace);
|
||||
if (target == String(channel)) {
|
||||
int colonPos = line.indexOf(':', thirdSpace);
|
||||
String message = line.substring(colonPos + 1);
|
||||
String senderNick = line.substring(1, line.indexOf('!'));
|
||||
bool mention = message.indexOf(irc_nickname) != -1;
|
||||
|
||||
if (mention)
|
||||
playNotificationSound();
|
||||
|
||||
if (message.startsWith(String("\x01") + "ACTION ") && message.endsWith("\x01")) {
|
||||
String actionMessage = message.substring(8, message.length() - 1);
|
||||
addLine(senderNick, actionMessage, "action");
|
||||
} else {
|
||||
addLine(senderNick, message, "message", mention);
|
||||
}
|
||||
}
|
||||
} else if (command == "JOIN" && line.indexOf(channel) != -1) {
|
||||
String senderNick = line.substring(1, line.indexOf('!'));
|
||||
addLine(senderNick, " has joined " + String(channel), "join");
|
||||
} else if (command == "PART" && line.indexOf(channel) != -1) {
|
||||
String senderNick = line.substring(1, line.indexOf('!'));
|
||||
addLine(senderNick, " has EMO-QUIT " + String(channel), "part");
|
||||
} else if (command == "QUIT") {
|
||||
String senderNick = line.substring(1, line.indexOf('!'));
|
||||
addLine(senderNick, "", "quit");
|
||||
} else if (command == "NICK") {
|
||||
String prefix = line.startsWith(":") ? line.substring(1, firstSpace) : "";
|
||||
String newNick = line.substring(line.lastIndexOf(':') + 1);
|
||||
|
||||
if (prefix.indexOf('!') == -1) {
|
||||
addLine(irc_nickname, " -> " + newNick, "nick");
|
||||
irc_nickname = newNick;
|
||||
} else {
|
||||
String oldNick = prefix.substring(0, prefix.indexOf('!'));
|
||||
addLine(oldNick, " -> " + newNick, "nick");
|
||||
if (oldNick == irc_nickname) {
|
||||
irc_nickname = newNick;
|
||||
}
|
||||
}
|
||||
} else if (command == "KICK") {
|
||||
int thirdSpace = line.indexOf(' ', secondSpace + 1);
|
||||
int fourthSpace = line.indexOf(' ', thirdSpace + 1);
|
||||
String kicker = line.substring(1, line.indexOf('!'));
|
||||
String kicked = line.substring(thirdSpace + 1, fourthSpace);
|
||||
addLine(kicked, " by " + kicker, "kick");
|
||||
} else if (command == "MODE") {
|
||||
String modeChange = line.substring(secondSpace + 1);
|
||||
addLine("", modeChange, "mode");
|
||||
} else if (command == "432") {
|
||||
addLine("ERROR", "ERR_ERRONEUSNICKNAME", "error", TFT_RED, TFT_DARKGREY);
|
||||
} else if (command == "433") {
|
||||
addLine("ERROR", "ERR_NICKNAMEINUSE", "error", TFT_RED, TFT_DARKGREY);
|
||||
irc_nickname = "ACID_" + String(random(1000, 9999));
|
||||
sendIRC("NICK " + irc_nickname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int renderFormattedMessage(String message, int cursorY, int lineHeight, bool highlightNick) {
|
||||
uint16_t fgColor = TFT_WHITE;
|
||||
uint16_t bgColor = TFT_BLACK;
|
||||
bool bold = false;
|
||||
bool underline = false;
|
||||
bool nickHighlighted = false;
|
||||
|
||||
int nickPos = -1;
|
||||
if (highlightNick) {
|
||||
nickPos = message.indexOf(irc_nickname);
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < message.length(); i++) {
|
||||
char c = message[i];
|
||||
if (c == '\x02') {
|
||||
bold = !bold;
|
||||
} else if (c == '\x1F') {
|
||||
underline = !underline;
|
||||
} else if (c == '\x03') {
|
||||
fgColor = TFT_WHITE;
|
||||
bgColor = TFT_BLACK;
|
||||
|
||||
if (i + 1 < message.length() && (isdigit(message[i + 1]) || message[i + 1] == ',')) {
|
||||
int colorCode = -1;
|
||||
if (isdigit(message[i + 1])) {
|
||||
colorCode = message[++i] - '0';
|
||||
if (i + 1 < message.length() && isdigit(message[i + 1]))
|
||||
colorCode = colorCode * 10 + (message[++i] - '0');
|
||||
}
|
||||
|
||||
if (colorCode != -1)
|
||||
fgColor = getColorFromCode(colorCode);
|
||||
|
||||
if (i + 1 < message.length() && message[i + 1] == ',') {
|
||||
i++;
|
||||
int bgColorCode = -1;
|
||||
if (isdigit(message[i + 1])) {
|
||||
bgColorCode = message[++i] - '0';
|
||||
if (i + 1 < message.length() && isdigit(message[i + 1]))
|
||||
bgColorCode = bgColorCode * 10 + (message[++i] - '0');
|
||||
}
|
||||
|
||||
if (bgColorCode != -1)
|
||||
bgColor = getColorFromCode(bgColorCode);
|
||||
|
||||
}
|
||||
|
||||
tft.setTextColor(fgColor, bgColor);
|
||||
}
|
||||
} else if (c == '\x0F') {
|
||||
fgColor = TFT_WHITE;
|
||||
bgColor = TFT_BLACK;
|
||||
bold = false;
|
||||
underline = false;
|
||||
tft.setTextColor(fgColor, bgColor);
|
||||
tft.setTextFont(1);
|
||||
} else {
|
||||
if (highlightNick && !nickHighlighted && nickPos != -1 && i == nickPos) {
|
||||
tft.setTextColor(TFT_YELLOW, bgColor);
|
||||
for (char nc : irc_nickname) {
|
||||
tft.print(nc);
|
||||
i++;
|
||||
}
|
||||
i--;
|
||||
tft.setTextColor(TFT_WHITE, bgColor);
|
||||
nickHighlighted = true;
|
||||
} else {
|
||||
if (tft.getCursorX() + tft.textWidth(String(c)) > SCREEN_WIDTH) {
|
||||
cursorY += lineHeight;
|
||||
tft.setCursor(0, cursorY);
|
||||
}
|
||||
if (c == ' ') {
|
||||
int spaceWidth = tft.textWidth(" ");
|
||||
tft.fillRect(tft.getCursorX(), tft.getCursorY(), spaceWidth, lineHeight, bgColor);
|
||||
tft.setCursor(tft.getCursorX() + spaceWidth, tft.getCursorY());
|
||||
} else {
|
||||
tft.fillRect(tft.getCursorX(), tft.getCursorY(), tft.textWidth(String(c)), lineHeight, bgColor);
|
||||
tft.setTextColor(fgColor, bgColor);
|
||||
tft.print(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.endsWith(" ")) {
|
||||
int trailingSpaces = 0;
|
||||
for (int i = message.length() - 1; i >= 0 && message[i] == ' '; i--) {
|
||||
trailingSpaces++;
|
||||
}
|
||||
for (int i = 0; i < trailingSpaces; i++) {
|
||||
int spaceWidth = tft.textWidth(" ");
|
||||
tft.fillRect(tft.getCursorX(), tft.getCursorY(), spaceWidth, lineHeight, bgColor);
|
||||
tft.setCursor(tft.getCursorX() + spaceWidth, tft.getCursorY());
|
||||
}
|
||||
}
|
||||
|
||||
cursorY += lineHeight;
|
||||
return cursorY;
|
||||
}
|
||||
|
||||
|
||||
void setupScreen() {
|
||||
pinMode(TFT_BL, OUTPUT);
|
||||
digitalWrite(TFT_BL, HIGH);
|
||||
setBrightness(8);
|
||||
tft.begin();
|
||||
tft.setRotation(1);
|
||||
tft.invertDisplay(1);
|
||||
Serial.println("TFT initialized");
|
||||
}
|
||||
|
||||
|
||||
void turnOffScreen() {
|
||||
Serial.println("Screen turned off");
|
||||
tft.writecommand(TFT_DISPOFF);
|
||||
tft.writecommand(TFT_SLPIN);
|
||||
digitalWrite(TFT_BL, LOW);
|
||||
screenOn = false;
|
||||
}
|
||||
|
||||
|
||||
void turnOnScreen() {
|
||||
Serial.println("Screen turned on");
|
||||
digitalWrite(TFT_BL, HIGH);
|
||||
tft.writecommand(TFT_SLPOUT);
|
||||
tft.writecommand(TFT_DISPON);
|
||||
screenOn = true;
|
||||
}
|
||||
|
||||
|
||||
void updateStatusBar() {
|
||||
Serial.println("Updating status bar...");
|
||||
uint16_t darkerGrey = tft.color565(25, 25, 25);
|
||||
tft.fillRect(0, 0, SCREEN_WIDTH, STATUS_BAR_HEIGHT, darkerGrey);
|
||||
|
||||
struct tm timeinfo;
|
||||
char timeStr[9];
|
||||
if (!getLocalTime(&timeinfo)) {
|
||||
sprintf(timeStr, "12:00 AM");
|
||||
} else {
|
||||
int hour = timeinfo.tm_hour;
|
||||
char ampm[] = "AM";
|
||||
if (hour == 0) {
|
||||
hour = 12;
|
||||
} else if (hour >= 12) {
|
||||
if (hour > 12)
|
||||
hour -= 12;
|
||||
strcpy(ampm, "PM");
|
||||
}
|
||||
sprintf(timeStr, "%02d:%02d %s", hour, timeinfo.tm_min, ampm);
|
||||
}
|
||||
tft.setTextDatum(ML_DATUM);
|
||||
tft.setTextColor(TFT_WHITE, darkerGrey);
|
||||
tft.drawString(timeStr, 0, STATUS_BAR_HEIGHT / 2);
|
||||
|
||||
char wifiStr[15];
|
||||
int wifiSignal = 0;
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
sprintf(wifiStr, "WiFi: N/A");
|
||||
tft.setTextColor(TFT_PINK, darkerGrey);
|
||||
tft.setTextDatum(MR_DATUM);
|
||||
tft.drawString(wifiStr, SCREEN_WIDTH - 100, STATUS_BAR_HEIGHT / 2);
|
||||
} else {
|
||||
int32_t rssi = WiFi.RSSI();
|
||||
if (rssi > -50) wifiSignal = 100;
|
||||
else if (rssi > -60) wifiSignal = 80;
|
||||
else if (rssi > -70) wifiSignal = 60;
|
||||
else if (rssi > -80) wifiSignal = 40;
|
||||
else if (rssi > -90) wifiSignal = 20;
|
||||
else wifiSignal = 0;
|
||||
|
||||
sprintf(wifiStr, "WiFi: %d%%", wifiSignal);
|
||||
tft.setTextDatum(MR_DATUM);
|
||||
tft.setTextColor(TFT_PINK, darkerGrey);
|
||||
tft.drawString("WiFi:", SCREEN_WIDTH - 120, STATUS_BAR_HEIGHT / 2);
|
||||
tft.setTextColor(getColorFromPercentage(wifiSignal), darkerGrey);
|
||||
tft.drawString(wifiStr + 6, SCREEN_WIDTH - 100, STATUS_BAR_HEIGHT / 2);
|
||||
}
|
||||
|
||||
int batteryLevel = BL.getBatteryChargeLevel();
|
||||
char batteryStr[15];
|
||||
sprintf(batteryStr, "Batt: %d%%", batteryLevel);
|
||||
tft.setTextDatum(MR_DATUM);
|
||||
tft.setTextColor(TFT_CYAN, darkerGrey);
|
||||
tft.drawString("Batt:", SCREEN_WIDTH - 40, STATUS_BAR_HEIGHT / 2);
|
||||
tft.setTextColor(getColorFromPercentage(batteryLevel), darkerGrey);
|
||||
tft.drawString(batteryStr + 5, SCREEN_WIDTH - 5, STATUS_BAR_HEIGHT / 2);
|
||||
}
|
47
src/Display.h
Normal file
47
src/Display.h
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <time.h>
|
||||
|
||||
#include <TFT_eSPI.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#define CHAR_HEIGHT 10
|
||||
#define LINE_SPACING 0
|
||||
#define STATUS_BAR_HEIGHT 10
|
||||
#define INPUT_LINE_HEIGHT (CHAR_HEIGHT + LINE_SPACING)
|
||||
#define MAX_LINES ((SCREEN_HEIGHT - INPUT_LINE_HEIGHT - STATUS_BAR_HEIGHT) / (CHAR_HEIGHT + LINE_SPACING))
|
||||
|
||||
extern bool infoScreen;
|
||||
extern bool configScreen;
|
||||
extern bool screenOn;
|
||||
extern const char* channel;
|
||||
extern unsigned long infoScreenStartTime;
|
||||
extern unsigned long configScreenStartTime;
|
||||
extern unsigned long lastStatusUpdateTime;
|
||||
extern unsigned long lastActivityTime;
|
||||
extern String inputBuffer;
|
||||
|
||||
extern std::vector<String> lines;
|
||||
extern std::vector<bool> mentions;
|
||||
extern std::map<String, uint32_t> nickColors;
|
||||
|
||||
extern TFT_eSPI tft;
|
||||
|
||||
void addLine(String senderNick, String message, String type, bool mention = false, uint16_t errorColor = TFT_WHITE, uint16_t reasonColor = TFT_WHITE);
|
||||
int calculateLinesRequired(String message);
|
||||
void displayCenteredText(String text);
|
||||
void displayInputLine();
|
||||
void displayLines();
|
||||
void displayXBM();
|
||||
uint32_t generateRandomColor();
|
||||
uint16_t getColorFromCode(int colorCode);
|
||||
uint16_t getColorFromPercentage(int percentage);
|
||||
void handleKeyboardInput(char key);
|
||||
void parseAndDisplay(String line);
|
||||
int renderFormattedMessage(String message, int cursorY, int lineHeight, bool highlightNick = false);
|
||||
void setupScreen();
|
||||
void turnOffScreen();
|
||||
void turnOnScreen();
|
||||
void updateStatusBar();
|
47
src/Gotify.cpp
Normal file
47
src/Gotify.cpp
Normal file
@ -0,0 +1,47 @@
|
||||
#include "Gotify.h"
|
||||
|
||||
|
||||
WebsocketsClient gotifyClient;
|
||||
|
||||
const char* gotify_server = "ws://your.gotify.server.com/stream"; // Use ws:// or wss:// based on your server configuration
|
||||
const char* gotify_token = "your_gotify_app_token";
|
||||
|
||||
unsigned long lastAttemptTime = 0;
|
||||
const unsigned long reconnectInterval = 60000; // 1 minute
|
||||
|
||||
|
||||
void onMessageCallback(WebsocketsMessage message) {
|
||||
Serial.println("Gotify Notification:");
|
||||
Serial.println(message.data());
|
||||
playNotificationSound();
|
||||
}
|
||||
|
||||
|
||||
void connectToGotify() {
|
||||
String url = String(gotify_server) + "?token=" + gotify_token;
|
||||
gotifyClient.onMessage(onMessageCallback);
|
||||
gotifyClient.connect(url);
|
||||
|
||||
if (gotifyClient.available())
|
||||
Serial.println("Connected to Gotify WebSocket");
|
||||
else
|
||||
Serial.println("Failed to connect to Gotify WebSocket");
|
||||
}
|
||||
|
||||
|
||||
void loopGotifyWebSocket() {
|
||||
while (true) {
|
||||
if (!gotifyClient.available()) {
|
||||
unsigned long currentTime = millis();
|
||||
if (currentTime - lastAttemptTime > reconnectInterval) {
|
||||
Serial.println("Attempting to reconnect to Gotify WebSocket...");
|
||||
lastAttemptTime = currentTime;
|
||||
connectToGotify();
|
||||
}
|
||||
} else {
|
||||
gotifyClient.poll();
|
||||
}
|
||||
|
||||
delay(10);
|
||||
}
|
||||
}
|
17
src/Gotify.h
Normal file
17
src/Gotify.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <ArduinoWebsockets.h>
|
||||
|
||||
#include "Speaker.h"
|
||||
|
||||
using namespace websockets;
|
||||
|
||||
extern WebsocketsClient gotifyClient;
|
||||
extern const char* gotify_server;
|
||||
extern const char* gotify_token;
|
||||
extern unsigned long lastAttemptTime;
|
||||
extern const unsigned long reconnectInterval;
|
||||
|
||||
void onMessageCallback(WebsocketsMessage message);
|
||||
void connectToGotify();
|
||||
void loopGotifyWebSocket();
|
81
src/IRC.cpp
Normal file
81
src/IRC.cpp
Normal file
@ -0,0 +1,81 @@
|
||||
#include "IRC.h"
|
||||
|
||||
|
||||
unsigned long joinChannelTime = 0;
|
||||
bool readyToJoinChannel = false;
|
||||
|
||||
WiFiClient* client;
|
||||
|
||||
|
||||
void action(String target, String message) {
|
||||
sendIRC("PRIVMSG " + String(target) + " :\001ACTION " + message + "\001");
|
||||
}
|
||||
|
||||
|
||||
bool connectToIRC() {
|
||||
if (irc_tls) {
|
||||
Serial.println("Connecting to IRC with TLS: " + String(irc_server) + ":" + String(irc_port));
|
||||
client = new WiFiClientSecure();
|
||||
static_cast<WiFiClientSecure*>(client)->setInsecure();
|
||||
return static_cast<WiFiClientSecure*>(client)->connect(irc_server.c_str(), irc_port);
|
||||
} else {
|
||||
Serial.println("Connecting to IRC: " + String(irc_server) + ":" + String(irc_port));
|
||||
client = new WiFiClient();
|
||||
return client->connect(irc_server.c_str(), irc_port);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void handleIRC() {
|
||||
while (client->available()) {
|
||||
String line = client->readStringUntil('\n');
|
||||
|
||||
if (line.length() > 512) {
|
||||
Serial.println("WARNING: IRC line length exceeds 512 characters!");
|
||||
line = line.substring(0, 512);
|
||||
}
|
||||
|
||||
Serial.println("IRC: " + line);
|
||||
|
||||
int firstSpace = line.indexOf(' ');
|
||||
int secondSpace = line.indexOf(' ', firstSpace + 1);
|
||||
|
||||
if (firstSpace != -1 && secondSpace != -1) {
|
||||
String prefix = line.substring(0, firstSpace);
|
||||
String command = line.substring(firstSpace + 1, secondSpace);
|
||||
|
||||
if (command == "001") {
|
||||
joinChannelTime = millis() + 2500;
|
||||
readyToJoinChannel = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (line.startsWith("PING")) {
|
||||
String pingResponse = "PONG " + line.substring(line.indexOf(' ') + 1);
|
||||
sendIRC(pingResponse);
|
||||
} else {
|
||||
parseAndDisplay(line);
|
||||
lastActivityTime = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void sendIRC(String command) {
|
||||
if (command.length() > 510) {
|
||||
Serial.println("Failed to send: Command too long");
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->connected()) {
|
||||
if (client->println(command)) {
|
||||
Serial.println("IRC: >>> " + command);
|
||||
} else {
|
||||
Serial.println("Failed to send: " + command);
|
||||
}
|
||||
} else {
|
||||
Serial.println("Failed to send: Not connected to IRC");
|
||||
}
|
||||
}
|
||||
|
||||
|
15
src/IRC.h
Normal file
15
src/IRC.h
Normal file
@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
#include "Display.h"
|
||||
#include "Storage.h"
|
||||
|
||||
extern WiFiClient* client;
|
||||
extern unsigned long joinChannelTime;
|
||||
extern bool readyToJoinChannel;
|
||||
|
||||
void action(String target, String message);
|
||||
bool connectToIRC();
|
||||
void handleIRC();
|
||||
void sendIRC(String command);
|
125
src/Lora.cpp
Normal file
125
src/Lora.cpp
Normal file
@ -0,0 +1,125 @@
|
||||
#include "Lora.h"
|
||||
|
||||
|
||||
SX1262 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
|
||||
|
||||
|
||||
void recvLoop() {
|
||||
String recv;
|
||||
|
||||
while (true) {
|
||||
if (radio.available()) {
|
||||
int state = radio.readData(recv);
|
||||
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
playNotificationSound();
|
||||
Serial.print(F("[RADIO] Received packet!"));
|
||||
Serial.print(F(" Data:"));
|
||||
Serial.print(recv);
|
||||
Serial.print(F(" RSSI:"));
|
||||
Serial.print(radio.getRSSI());
|
||||
Serial.print(F(" dBm"));
|
||||
Serial.print(F(" SNR:"));
|
||||
Serial.print(radio.getSNR());
|
||||
Serial.println(F(" dB"));
|
||||
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
|
||||
Serial.println(F("CRC error!"));
|
||||
} else {
|
||||
Serial.print(F("Failed, code "));
|
||||
Serial.println(state);
|
||||
}
|
||||
} else {
|
||||
Serial.println(F("Radio became unavailable!"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool setupRadio() {
|
||||
pinMode(RADIO_CS_PIN, OUTPUT);
|
||||
digitalWrite(RADIO_CS_PIN, HIGH);
|
||||
|
||||
int state = radio.begin(RADIO_FREQ);
|
||||
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
Serial.println("Start Radio success!");
|
||||
} else {
|
||||
Serial.print("Start Radio failed, code: ");
|
||||
Serial.println(state);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (radio.setFrequency(RADIO_FREQ) == RADIOLIB_ERR_INVALID_FREQUENCY) {
|
||||
Serial.println(F("Selected frequency is invalid for this module!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (radio.setBandwidth(125.0) == RADIOLIB_ERR_INVALID_BANDWIDTH) {
|
||||
Serial.println(F("Selected bandwidth is invalid for this module!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (radio.setSpreadingFactor(10) == RADIOLIB_ERR_INVALID_SPREADING_FACTOR) {
|
||||
Serial.println(F("Selected spreading factor is invalid for this module!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (radio.setCodingRate(6) == RADIOLIB_ERR_INVALID_CODING_RATE) {
|
||||
Serial.println(F("Selected coding rate is invalid for this module!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (radio.setSyncWord(0xAB) != RADIOLIB_ERR_NONE) {
|
||||
Serial.println(F("Unable to set sync word!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set output power to 17 dBm (accepted range is -17 - 22 dBm)
|
||||
if (radio.setOutputPower(17) == RADIOLIB_ERR_INVALID_OUTPUT_POWER) {
|
||||
Serial.println(F("Selected output power is invalid for this module!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set over current protection limit to 140 mA (accepted range is 45 - 140 mA)
|
||||
if (radio.setCurrentLimit(140) == RADIOLIB_ERR_INVALID_CURRENT_LIMIT) {
|
||||
Serial.println(F("Selected current limit is invalid for this module!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set LoRa preamble length to 15 symbols (accepted range is 0 - 65535)
|
||||
if (radio.setPreambleLength(15) == RADIOLIB_ERR_INVALID_PREAMBLE_LENGTH) {
|
||||
Serial.println(F("Selected preamble length is invalid for this module!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (radio.setCRC(false) == RADIOLIB_ERR_INVALID_CRC_CONFIGURATION) {
|
||||
Serial.println(F("Selected CRC is invalid for this module!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool transmit() {
|
||||
int state = radio.transmit("Hello World!");
|
||||
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
Serial.println(F("Radio transmission successful!"));
|
||||
Serial.print(F("[SX1262] Datarate:\t"));
|
||||
Serial.print(radio.getDataRate());
|
||||
Serial.println(F(" bps"));
|
||||
return true;
|
||||
} else if (state == RADIOLIB_ERR_PACKET_TOO_LONG) {
|
||||
Serial.println(F("Radio packet too long")); // 256 bytes is the maximum packet length
|
||||
return false;
|
||||
} else if (state == RADIOLIB_ERR_TX_TIMEOUT) {
|
||||
Serial.println(F("Radio timeout"));
|
||||
return false;
|
||||
} else {
|
||||
Serial.print(F("Radio error failed, code "));
|
||||
Serial.println(state);
|
||||
return false;
|
||||
}
|
||||
}
|
12
src/Lora.h
Normal file
12
src/Lora.h
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <RadioLib.h>
|
||||
|
||||
#include "pins.h"
|
||||
#include "Speaker.h"
|
||||
|
||||
extern SX1262 radio;
|
||||
|
||||
void recvLoop();
|
||||
bool setupRadio();
|
||||
bool transmit();
|
261
src/Network.cpp
Normal file
261
src/Network.cpp
Normal file
@ -0,0 +1,261 @@
|
||||
#include "Network.h"
|
||||
|
||||
|
||||
std::vector<WiFiNetwork> wifiNetworks;
|
||||
int selectedNetworkIndex = 0;
|
||||
|
||||
WireGuard wg;
|
||||
|
||||
|
||||
void connectToWiFi(String ssid, String password) {
|
||||
wifiNetworks.clear();
|
||||
Serial.println("Connecting to WiFi network: " + ssid);
|
||||
WiFi.begin(ssid.c_str(), password.c_str());
|
||||
|
||||
int attempts = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
||||
delay(500);
|
||||
displayCenteredText("CONNECTING TO " + ssid);
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
displayCenteredText("CONNECTED TO " + ssid);
|
||||
|
||||
updateTimeFromNTP();
|
||||
|
||||
preferences.begin("config", false);
|
||||
preferences.putString("wifi_ssid", ssid);
|
||||
preferences.putString("wifi_password", password);
|
||||
preferences.end();
|
||||
Serial.println("Stored WiFi credentials updated");
|
||||
|
||||
wifi_ssid = ssid;
|
||||
wifi_password = password;
|
||||
} else {
|
||||
Serial.println("Failed to connect to WiFi network: " + ssid);
|
||||
displayCenteredText("WIFI CONNECTION FAILED");
|
||||
|
||||
preferences.begin("config", false);
|
||||
preferences.putString("wifi_ssid", "");
|
||||
preferences.putString("wifi_password", "");
|
||||
preferences.end();
|
||||
Serial.println("Stored WiFi credentials removed");
|
||||
|
||||
wifi_ssid = "";
|
||||
wifi_password = "";
|
||||
|
||||
scanWiFiNetworks();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void displayPasswordInputLine() {
|
||||
tft.fillRect(0, SCREEN_HEIGHT - INPUT_LINE_HEIGHT, SCREEN_WIDTH, INPUT_LINE_HEIGHT, TFT_BLACK);
|
||||
tft.setCursor(0, SCREEN_HEIGHT - INPUT_LINE_HEIGHT);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
tft.setTextSize(1);
|
||||
tft.print("> " + inputBuffer);
|
||||
}
|
||||
|
||||
|
||||
void displayWiFiNetworks() {
|
||||
tft.fillRect(0, STATUS_BAR_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT - STATUS_BAR_HEIGHT, TFT_BLACK);
|
||||
|
||||
tft.setTextSize(1);
|
||||
tft.setTextColor(TFT_CYAN);
|
||||
tft.setCursor(0, STATUS_BAR_HEIGHT);
|
||||
tft.printf("# CH RSSI ENC SSID\n");
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
|
||||
int maxDisplayedLines = (SCREEN_HEIGHT - STATUS_BAR_HEIGHT - CHAR_HEIGHT) / (CHAR_HEIGHT + LINE_SPACING);
|
||||
int startIdx = selectedNetworkIndex >= maxDisplayedLines ? selectedNetworkIndex - maxDisplayedLines + 1 : 0;
|
||||
|
||||
for (int i = startIdx; i < wifiNetworks.size() && i < startIdx + maxDisplayedLines; ++i) {
|
||||
displayWiFiNetwork(i, i - startIdx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void displayWiFiNetwork(int index, int displayIndex) {
|
||||
int y = STATUS_BAR_HEIGHT + displayIndex * (CHAR_HEIGHT + LINE_SPACING);
|
||||
tft.setCursor(0, y);
|
||||
|
||||
if (index == selectedNetworkIndex)
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
else
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
|
||||
WiFiNetwork net = wifiNetworks[index];
|
||||
|
||||
uint16_t rssiColor = getColorFromPercentage((int32_t)net.rssi);
|
||||
tft.printf("%-2d %-3d ", net.index, net.channel);
|
||||
|
||||
tft.setTextColor(rssiColor, TFT_BLACK);
|
||||
tft.printf("%-5d ", net.rssi);
|
||||
|
||||
tft.setTextColor(index == selectedNetworkIndex ? TFT_GREEN : TFT_WHITE, TFT_BLACK);
|
||||
tft.printf("%-8s %s", net.encryption.c_str(), net.ssid.c_str());
|
||||
}
|
||||
|
||||
|
||||
String getEncryptionType(wifi_auth_mode_t encryptionType) {
|
||||
switch (encryptionType) {
|
||||
case (WIFI_AUTH_OPEN):
|
||||
return "Open";
|
||||
case (WIFI_AUTH_WEP):
|
||||
return "WEP";
|
||||
case (WIFI_AUTH_WPA_PSK):
|
||||
return "WPA_PSK";
|
||||
case (WIFI_AUTH_WPA2_PSK):
|
||||
return "WPA2_PSK";
|
||||
case (WIFI_AUTH_WPA_WPA2_PSK):
|
||||
return "WPA_WPA2_PSK";
|
||||
case (WIFI_AUTH_WPA2_ENTERPRISE):
|
||||
return "WPA2_ENTERPRISE";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void handlePasswordInput(char key) {
|
||||
if (key == '\n' || key == '\r') {
|
||||
wifi_password = inputBuffer;
|
||||
inputBuffer = "";
|
||||
connectToWiFi(wifi_ssid, wifi_password);
|
||||
} else if (key == '\b') {
|
||||
if (inputBuffer.length() > 0) {
|
||||
inputBuffer.remove(inputBuffer.length() - 1);
|
||||
displayPasswordInputLine();
|
||||
}
|
||||
} else if (inputBuffer.length() < 63) {
|
||||
inputBuffer += key;
|
||||
displayPasswordInputLine();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void handleWiFiSelection(char key) {
|
||||
if (key == 'u') {
|
||||
updateSelectedNetwork(-1);
|
||||
} else if (key == 'd') {
|
||||
updateSelectedNetwork(1);
|
||||
} else if (key == '\n' || key == '\r') {
|
||||
wifi_ssid = wifiNetworks[selectedNetworkIndex].ssid;
|
||||
if (wifiNetworks[selectedNetworkIndex].encryption == "Secured") {
|
||||
inputBuffer = "";
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextSize(1);
|
||||
tft.setTextColor(TFT_WHITE);
|
||||
tft.setCursor(0, STATUS_BAR_HEIGHT);
|
||||
tft.println("ENTER PASSWORD:");
|
||||
displayPasswordInputLine();
|
||||
|
||||
while (true) {
|
||||
char incoming = getKeyboardInput();
|
||||
if (incoming != 0) {
|
||||
handlePasswordInput(incoming);
|
||||
if (incoming == '\n' || incoming == '\r')
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
wifi_password = "";
|
||||
connectToWiFi(wifi_ssid, wifi_password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void initializeNetwork() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setHostname("acid-drop"); // Turn into a preference
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
randomizeMacAddress();
|
||||
}
|
||||
|
||||
|
||||
void randomizeMacAddress() {
|
||||
Serial.println("Current MAC Address: " + WiFi.macAddress());
|
||||
|
||||
uint8_t new_mac[6];
|
||||
|
||||
for (int i = 0; i < 6; ++i)
|
||||
new_mac[i] = random(0x00, 0xFF);
|
||||
|
||||
if (esp_wifi_set_mac(WIFI_IF_STA, new_mac) == ESP_OK)
|
||||
Serial.println("New MAC Address: " + WiFi.macAddress());
|
||||
else
|
||||
Serial.println("Failed to set new MAC Address");
|
||||
}
|
||||
|
||||
|
||||
void scanWiFiNetworks() {
|
||||
Serial.println("Scanning for WiFi networks...");
|
||||
displayCenteredText("SCANNING WIFI");
|
||||
delay(1000);
|
||||
|
||||
wifiNetworks.clear();
|
||||
|
||||
int n = WiFi.scanNetworks();
|
||||
|
||||
if (n == 0) {
|
||||
Serial.println("No WiFi networks found");
|
||||
displayCenteredText("NO NETWORKS FOUND");
|
||||
delay(2000);
|
||||
scanWiFiNetworks();
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.print("Total number of networks found: ");
|
||||
Serial.println(n);
|
||||
|
||||
for (int i = 0; i < n && i < 100; i++) {
|
||||
WiFiNetwork net;
|
||||
net.index = i + 1;
|
||||
net.channel = WiFi.channel(i);
|
||||
net.rssi = WiFi.RSSI(i);
|
||||
net.encryption = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? "Open" : "Secured";
|
||||
String ssid = WiFi.SSID(i).substring(0, 32);
|
||||
net.ssid = ssid;
|
||||
wifiNetworks.push_back(net);
|
||||
}
|
||||
|
||||
displayWiFiNetworks();
|
||||
}
|
||||
|
||||
|
||||
void updateSelectedNetwork(int delta) {
|
||||
int newIndex = selectedNetworkIndex + delta;
|
||||
|
||||
if (newIndex >= 0 && newIndex < wifiNetworks.size()) {
|
||||
selectedNetworkIndex = newIndex;
|
||||
displayWiFiNetworks();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void wgConnect(const IPAddress& localIp, const char* privateKey, const char* endpointAddress, const char* publicKey, uint16_t endpointPort) {
|
||||
wg.begin(localIp, privateKey, endpointAddress, publicKey, endpointPort);
|
||||
}
|
||||
|
||||
|
||||
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
switch (event) {
|
||||
case SYSTEM_EVENT_STA_CONNECTED:
|
||||
Serial.println("WiFi connected");
|
||||
break;
|
||||
case SYSTEM_EVENT_STA_DISCONNECTED:
|
||||
Serial.println("WiFi disconnected");
|
||||
break;
|
||||
case SYSTEM_EVENT_STA_GOT_IP:
|
||||
Serial.println("WiFi got IP address: " + WiFi.localIP().toString());
|
||||
break;
|
||||
case SYSTEM_EVENT_STA_LOST_IP:
|
||||
Serial.println("WiFi lost IP address");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
37
src/Network.h
Normal file
37
src/Network.h
Normal file
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <esp_wifi.h>
|
||||
#include <WireGuard-ESP32.h>
|
||||
|
||||
#include "Display.h"
|
||||
#include "Storage.h"
|
||||
#include "Utilities.h"
|
||||
#include "WiFi.h"
|
||||
|
||||
struct WiFiNetwork {
|
||||
int index;
|
||||
int channel;
|
||||
int rssi;
|
||||
String encryption;
|
||||
String ssid;
|
||||
};
|
||||
|
||||
extern std::vector<WiFiNetwork> wifiNetworks;
|
||||
extern int selectedNetworkIndex;
|
||||
extern WireGuard wg;
|
||||
|
||||
void connectToWiFi(String ssid, String password);
|
||||
void displayPasswordInputLine();
|
||||
void displayWiFiNetworks();
|
||||
void displayWiFiNetwork(int index, int displayIndex);
|
||||
String getEncryptionType(wifi_auth_mode_t encryptionType);
|
||||
void handlePasswordInput(char key);
|
||||
void handleWiFiSelection(char key);
|
||||
void initializeNetwork();
|
||||
void randomizeMacAddress();
|
||||
void scanWiFiNetworks();
|
||||
void updateSelectedNetwork(int delta);
|
||||
void wgConnect(const IPAddress& localIp, const char* privateKey, const char* endpointAddress, const char* publicKey, uint16_t endpointPort);
|
||||
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
|
75
src/Speaker.cpp
Normal file
75
src/Speaker.cpp
Normal file
@ -0,0 +1,75 @@
|
||||
#include "Speaker.h"
|
||||
|
||||
|
||||
void playNotificationSound() {
|
||||
playTone(1000, 50);
|
||||
delay(100);
|
||||
playTone(1500, 50);
|
||||
delay(100);
|
||||
playTone(2000, 50);
|
||||
delay(100);
|
||||
playTone(500, 50);
|
||||
}
|
||||
|
||||
|
||||
void playRTTTL(const char* rtttl) {
|
||||
static AudioGeneratorRTTTL *rtttlGenerator = new AudioGeneratorRTTTL();
|
||||
static AudioOutputI2S *audioOutput = new AudioOutputI2S();
|
||||
static AudioFileSourcePROGMEM *fileSource = new AudioFileSourcePROGMEM(rtttl, strlen(rtttl));
|
||||
|
||||
audioOutput->begin();
|
||||
rtttlGenerator->begin(fileSource, audioOutput);
|
||||
|
||||
while (rtttlGenerator->isRunning())
|
||||
rtttlGenerator->loop();
|
||||
|
||||
rtttlGenerator->stop();
|
||||
fileSource->close();
|
||||
}
|
||||
|
||||
|
||||
void playTone(float frequency, int duration, int volume) {
|
||||
volume = constrain(volume, 0, 32767);
|
||||
const int wave_period = SAMPLE_RATE / frequency;
|
||||
int16_t sample_buffer[wave_period];
|
||||
|
||||
for (int i = 0; i < wave_period; ++i)
|
||||
sample_buffer[i] = (i < wave_period / 2) ? volume : -volume;
|
||||
|
||||
int total_samples = SAMPLE_RATE * duration / 1000;
|
||||
int samples_written = 0;
|
||||
|
||||
while (samples_written < total_samples) {
|
||||
int to_write = min(wave_period, total_samples - samples_written);
|
||||
i2s_write(BOARD_I2S_PORT, sample_buffer, to_write * sizeof(int16_t), (size_t *)&to_write, portMAX_DELAY);
|
||||
samples_written += to_write;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void setupI2S() {
|
||||
i2s_config_t i2s_config = {
|
||||
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
|
||||
.sample_rate = SAMPLE_RATE,
|
||||
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
||||
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||
.intr_alloc_flags = 0,
|
||||
.dma_buf_count = 8,
|
||||
.dma_buf_len = 64,
|
||||
.use_apll = false,
|
||||
.tx_desc_auto_clear = true,
|
||||
.fixed_mclk = 0
|
||||
};
|
||||
|
||||
i2s_pin_config_t pin_config = {
|
||||
.bck_io_num = BOARD_I2S_BCK,
|
||||
.ws_io_num = BOARD_I2S_WS,
|
||||
.data_out_num = BOARD_I2S_DOUT,
|
||||
.data_in_num = I2S_PIN_NO_CHANGE
|
||||
};
|
||||
|
||||
i2s_driver_install(BOARD_I2S_PORT, &i2s_config, 0, NULL);
|
||||
i2s_set_pin(BOARD_I2S_PORT, &pin_config);
|
||||
i2s_set_clk(BOARD_I2S_PORT, SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
|
||||
}
|
17
src/Speaker.h
Normal file
17
src/Speaker.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <AudioFileSourcePROGMEM.h>
|
||||
#include <AudioGeneratorRTTTL.h>
|
||||
#include <AudioOutputI2S.h>
|
||||
#include <driver/i2s.h>
|
||||
|
||||
#include "pins.h"
|
||||
|
||||
#define BOARD_I2S_PORT I2S_NUM_0
|
||||
#define SAMPLE_RATE 44100
|
||||
|
||||
void playNotificationSound();
|
||||
void playRTTTL(const char* rtttl);
|
||||
void playTone(float frequency, int duration, int volume = 16383);
|
||||
void setupI2S();
|
121
src/Storage.cpp
Normal file
121
src/Storage.cpp
Normal file
@ -0,0 +1,121 @@
|
||||
#include "Storage.h"
|
||||
|
||||
|
||||
Preferences preferences;
|
||||
|
||||
// Config variables
|
||||
String irc_nickname;
|
||||
String irc_username;
|
||||
String irc_realname;
|
||||
String irc_server;
|
||||
int irc_port;
|
||||
bool irc_tls;
|
||||
String irc_channel;
|
||||
String irc_nickserv;
|
||||
String wifi_ssid;
|
||||
String wifi_password;
|
||||
|
||||
|
||||
void loadPreferences() {
|
||||
preferences.begin("config", false);
|
||||
|
||||
// IRC preferences
|
||||
if (!preferences.isKey("irc_nickname"))
|
||||
preferences.putString("irc_nickname", "ACID_" + String(random(1000, 10000)));
|
||||
irc_nickname = preferences.getString("irc_nickname");
|
||||
|
||||
if (!preferences.isKey("irc_username"))
|
||||
preferences.putString("irc_username", "tdeck");
|
||||
irc_username = preferences.getString("irc_username");
|
||||
|
||||
if (!preferences.isKey("irc_realname"))
|
||||
preferences.putString("irc_realname", "ACID DROP Firmware");
|
||||
irc_realname = preferences.getString("irc_realname");
|
||||
|
||||
if (!preferences.isKey("irc_server"))
|
||||
preferences.putString("irc_server", "irc.supernets.org");
|
||||
irc_server = preferences.getString("irc_server");
|
||||
|
||||
if (!preferences.isKey("irc_port"))
|
||||
preferences.putInt("irc_port", 6667);
|
||||
irc_port = preferences.getInt("irc_port");
|
||||
|
||||
if (!preferences.isKey("irc_tls"))
|
||||
preferences.putBool("irc_tls", false);
|
||||
irc_tls = preferences.getBool("irc_tls");
|
||||
|
||||
if (!preferences.isKey("irc_channel"))
|
||||
preferences.putString("irc_channel", "#comms");
|
||||
irc_channel = preferences.getString("irc_channel");
|
||||
|
||||
if (!preferences.isKey("irc_nickserv"))
|
||||
preferences.putString("irc_nickserv", "");
|
||||
irc_nickserv = preferences.getString("irc_nickserv");
|
||||
|
||||
// WiFi preferences
|
||||
if (!preferences.isKey("wifi_ssid"))
|
||||
preferences.putString("wifi_ssid", "");
|
||||
wifi_ssid = preferences.getString("wifi_ssid");
|
||||
|
||||
if (!preferences.isKey("wifi_password"))
|
||||
preferences.putString("wifi_password", "");
|
||||
wifi_password = preferences.getString("wifi_password");
|
||||
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
|
||||
bool mountSD() {
|
||||
if (SD.begin(BOARD_SDCARD_CS, SPI, 800000U)) {
|
||||
uint8_t cardType = SD.cardType();
|
||||
|
||||
if (cardType == CARD_NONE) {
|
||||
Serial.println("No SD card attached");
|
||||
return false;
|
||||
} else {
|
||||
Serial.print("SD Card Type: ");
|
||||
if (cardType == CARD_MMC)
|
||||
Serial.println("MMC");
|
||||
else if (cardType == CARD_SD)
|
||||
Serial.println("SDSC");
|
||||
else if (cardType == CARD_SDHC)
|
||||
Serial.println("SDHC");
|
||||
else
|
||||
Serial.println("UNKNOWN");
|
||||
|
||||
uint32_t cardSize = SD.cardSize() / (1024 * 1024);
|
||||
uint32_t cardTotal = SD.totalBytes() / (1024 * 1024);
|
||||
uint32_t cardUsed = SD.usedBytes() / (1024 * 1024);
|
||||
Serial.printf("SD Card Size: %lu MB\n", cardSize);
|
||||
Serial.printf("Total space: %lu MB\n", cardTotal);
|
||||
Serial.printf("Used space: %lu MB\n", cardUsed);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void setupSD() {
|
||||
pinMode(BOARD_SDCARD_CS, OUTPUT);
|
||||
digitalWrite(BOARD_SDCARD_CS, HIGH);
|
||||
pinMode(BOARD_SPI_MISO, INPUT_PULLUP);
|
||||
SPI.begin(BOARD_SPI_SCK, BOARD_SPI_MISO, BOARD_SPI_MOSI);
|
||||
}
|
||||
|
||||
|
||||
void wipeNVS() {
|
||||
esp_err_t err = nvs_flash_erase();
|
||||
if (err == ESP_OK)
|
||||
Serial.println("NVS flash erase successful.");
|
||||
else
|
||||
Serial.println("Error erasing NVS flash!");
|
||||
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_OK)
|
||||
Serial.println("NVS flash init successful.");
|
||||
else
|
||||
Serial.println("Error initializing NVS flash!");
|
||||
}
|
26
src/Storage.h
Normal file
26
src/Storage.h
Normal file
@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "nvs_flash.h"
|
||||
#include <Preferences.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include "pins.h"
|
||||
|
||||
extern Preferences preferences;
|
||||
|
||||
extern String irc_nickname;
|
||||
extern String irc_username;
|
||||
extern String irc_realname;
|
||||
extern String irc_server;
|
||||
extern int irc_port;
|
||||
extern bool irc_tls;
|
||||
extern String irc_channel;
|
||||
extern String irc_nickserv;
|
||||
extern String wifi_ssid;
|
||||
extern String wifi_password;
|
||||
|
||||
void loadPreferences();
|
||||
bool mountSD();
|
||||
void setupSD();
|
||||
void wipeNVS();
|
148
src/Utilities.cpp
Normal file
148
src/Utilities.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "Utilities.h"
|
||||
|
||||
|
||||
Pangodream_18650_CL BL(BOARD_BAT_ADC, CONV_FACTOR, READS);
|
||||
|
||||
|
||||
String formatBytes(size_t bytes) {
|
||||
if (bytes < 1024)
|
||||
return String(bytes) + " B";
|
||||
else if (bytes < (1024 * 1024))
|
||||
return String(bytes / 1024.0, 2) + " KB";
|
||||
else if (bytes < (1024 * 1024 * 1024))
|
||||
return String(bytes / 1024.0 / 1024.0, 2) + " MB";
|
||||
else
|
||||
return String(bytes / 1024.0 / 1024.0 / 1024.0, 2) + " GB";
|
||||
}
|
||||
|
||||
|
||||
char getKeyboardInput() {
|
||||
char incoming = 0;
|
||||
Wire.requestFrom(LILYGO_KB_SLAVE_ADDRESS, 1);
|
||||
if (Wire.available()) {
|
||||
incoming = Wire.read();
|
||||
if (incoming != (char)0x00) {
|
||||
return incoming;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void setBrightness(uint8_t value) {
|
||||
static uint8_t level = 16;
|
||||
static uint8_t steps = 16;
|
||||
value = constrain(value, 0, steps); // Ensure the brightness value is within the valid range
|
||||
|
||||
if (value == 0) {
|
||||
digitalWrite(BOARD_BL_PIN, 0);
|
||||
delay(3);
|
||||
level = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (level == 0) {
|
||||
digitalWrite(BOARD_BL_PIN, 1);
|
||||
level = steps;
|
||||
delayMicroseconds(30);
|
||||
}
|
||||
|
||||
int from = steps - level;
|
||||
int to = steps - value;
|
||||
int num = (steps + to - from) % steps;
|
||||
for (int i = 0; i < num; i++) {
|
||||
digitalWrite(BOARD_BL_PIN, 0);
|
||||
digitalWrite(BOARD_BL_PIN, 1);
|
||||
}
|
||||
|
||||
level = value;
|
||||
}
|
||||
|
||||
|
||||
void updateTimeFromNTP() {
|
||||
Serial.println("Syncing time with NTP server...");
|
||||
configTime(-5 * 3600, 3600, "pool.ntp.org", "north-america.pool.ntp.org", "time.nist.gov");
|
||||
|
||||
for (int i = 0; i < 10; ++i) { // Try up to 10 times
|
||||
delay(2000);
|
||||
struct tm timeinfo;
|
||||
if (getLocalTime(&timeinfo)) {
|
||||
Serial.println(&timeinfo, "Time synchronized: %A, %B %d %Y %H:%M:%S");
|
||||
return;
|
||||
} else {
|
||||
Serial.println("Failed to synchronize time, retrying...");
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("Failed to synchronize time after multiple attempts."); // What do we do if we can't sync with the NTP server?
|
||||
}
|
||||
|
||||
|
||||
void printDeviceInfo() {
|
||||
Serial.println("Gathering device info...");
|
||||
|
||||
// Get MAC Address
|
||||
uint8_t mac[6];
|
||||
esp_efuse_mac_get_default(mac);
|
||||
String macAddress = String(mac[0], HEX) + ":" + String(mac[1], HEX) + ":" + String(mac[2], HEX) + ":" + String(mac[3], HEX) + ":" + String(mac[4], HEX) + ":" + String(mac[5], HEX);
|
||||
|
||||
// Get Chip Info
|
||||
uint32_t chipId = ESP.getEfuseMac();
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
String chipInfo = String(chip_info.model) + " Rev " + String(chip_info.revision) + ", " + String(chip_info.cores) + " cores, " + String(ESP.getCpuFreqMHz()) + " MHz";
|
||||
|
||||
// Get Flash Info
|
||||
size_t flashSize = spi_flash_get_chip_size();
|
||||
size_t flashUsed = ESP.getFlashChipSize() - ESP.getFreeSketchSpace();
|
||||
String flashInfo = formatBytes(flashUsed) + " / " + formatBytes(flashSize);
|
||||
|
||||
// Get PSRAM Info
|
||||
size_t total_psram = ESP.getPsramSize();
|
||||
size_t free_psram = ESP.getFreePsram();
|
||||
String psramInfo = formatBytes(total_psram - free_psram) + " / " + formatBytes(total_psram);
|
||||
|
||||
// Get Heap Info
|
||||
size_t total_heap = ESP.getHeapSize();
|
||||
size_t free_heap = ESP.getFreeHeap();
|
||||
String heapInfo = formatBytes(total_heap - free_heap) + " / " + formatBytes(total_heap);
|
||||
|
||||
// Get WiFi Info
|
||||
String wifiInfo = "Not connected";
|
||||
String wifiSSID = "";
|
||||
String wifiChannel = "";
|
||||
String wifiSignal = "";
|
||||
String wifiLocalIP = "";
|
||||
String wifiGatewayIP = "";
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
wifiSSID = WiFi.SSID();
|
||||
wifiChannel = String(WiFi.channel());
|
||||
wifiSignal = String(WiFi.RSSI()) + " dBm";
|
||||
wifiLocalIP = WiFi.localIP().toString();
|
||||
wifiGatewayIP = WiFi.gatewayIP().toString();
|
||||
}
|
||||
|
||||
Serial.println("MCU: ESP32-S3FN16R8");
|
||||
Serial.println("LoRa Tranciever: Semtech SX1262 (915 MHz)"); // Need to set the frequency in pins.h
|
||||
Serial.println("LCD: ST7789 SPI"); // Update this
|
||||
Serial.println("Chip ID: " + String(chipId, HEX));
|
||||
Serial.println("MAC Address: " + macAddress);
|
||||
Serial.println("Chip Info: " + chipInfo);
|
||||
Serial.println("Battery:");
|
||||
Serial.println(" Pin Value: " + String(analogRead(34)));
|
||||
//Serial.println(" Average Pin Value: " + String(BL.pinRead()));
|
||||
//Serial.println(" Volts: " + String(BL.getBatteryVolts()));
|
||||
//Serial.println(" Charge Level: " + String(BL.getBatteryChargeLevel()) + "%");
|
||||
Serial.println("Memory:");
|
||||
Serial.println(" Flash: " + flashInfo);
|
||||
Serial.println(" PSRAM: " + psramInfo);
|
||||
Serial.println(" Heap: " + heapInfo);
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("WiFi Info: " + wifiInfo);
|
||||
Serial.println(" SSID: " + wifiSSID);
|
||||
Serial.println(" Channel: " + wifiChannel);
|
||||
Serial.println(" Signal: " + wifiSignal);
|
||||
Serial.println(" Local IP: " + wifiLocalIP);
|
||||
Serial.println(" Gateway IP: " + wifiGatewayIP);
|
||||
}
|
||||
}
|
17
src/Utilities.h
Normal file
17
src/Utilities.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "WiFi.h"
|
||||
#include "Wire.h"
|
||||
#include <Pangodream_18650_CL.h> // Power management
|
||||
|
||||
#include "pins.h"
|
||||
|
||||
extern Pangodream_18650_CL BL;
|
||||
|
||||
String formatBytes(size_t bytes);
|
||||
char getKeyboardInput();
|
||||
void printDeviceInfo();
|
||||
void setBrightness(uint8_t value);
|
||||
void updateTimeFromNTP();
|
||||
|
@ -1,5 +1,6 @@
|
||||
#define logo_width 199
|
||||
#define logo_height 165
|
||||
|
||||
static unsigned char logo_bits[] = {
|
||||
0x00, 0xF8, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
1142
src/main.ino
1142
src/main.ino
File diff suppressed because it is too large
Load Diff
19
src/pins.h
19
src/pins.h
@ -1,13 +1,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
// board peripheral power control pin needs to be set to HIGH when using the peripheral
|
||||
|
||||
// Board pin definitions ------------------------
|
||||
#define BOARD_POWERON 10
|
||||
|
||||
// Speaker
|
||||
#define BOARD_I2S_WS 5
|
||||
#define BOARD_I2S_BCK 7
|
||||
#define BOARD_I2S_DOUT 6
|
||||
#define BOARD_I2S_BCK 7
|
||||
|
||||
#define BOARD_I2C_SDA 18
|
||||
#define BOARD_I2C_SCL 8
|
||||
@ -46,9 +46,18 @@
|
||||
|
||||
#define BOARD_BL_PIN 42
|
||||
|
||||
#define GPS_RX_PIN 44
|
||||
#define GPS_TX_PIN 43
|
||||
|
||||
|
||||
// Other definitions ----------------------------
|
||||
#define SCREEN_WIDTH 320
|
||||
#define SCREEN_HEIGHT 240
|
||||
|
||||
// Battery definitions
|
||||
#define CONV_FACTOR 1.8 // Conversion factor for the ADC to voltage conversion
|
||||
#define READS 20 // Number of readings for averaging
|
||||
|
||||
#define LILYGO_KB_SLAVE_ADDRESS 0x55
|
||||
|
||||
#define GPS_RX_PIN 44
|
||||
#define GPS_TX_PIN 43
|
||||
#define RADIO_FREQ 903.0
|
Loading…
Reference in New Issue
Block a user