Hope its ok to post here. Its not exactly a meshtastic project per-say but I think its nifty so thought I would share.
TLDR: Heltek V3 Ebook reader
History
Its an ebook reader from an old heltek v3 that has been running meshtastic for a couple of years now.
Its been my debug node for a LONG time playing around with the alpha builds. Lately though I have some better hardware to debug/code with. I saw a project on youtube (cant find the link now?) involving a Heltek being a web server and a ebook reader. But with a eink display. So I spent an afternoon making something similar. The code is my own and its been a couple of decades since ive seriously done C++.
Things you can do with it:
- Read books! I mean thats why I made it.
- Upload txt files via a web server! Very simple html to push a book to the device. Limit is currently 500kb so enough for a medium sized book. Technically this could be bigger, but I had some issues with guesstimating the amount of space I had. Enough for H.G. “The Time Machine” and other books. But not The Wandering Inn ;)
- Single click -> next page.
- Double click -> prev page
- Triple click -> IP Address for the web server. Im tempted to make this also turn on the web server instead of having it auto-turn on in the setup. Or making this the area where admin things go.
Pictures
Code
#include <Wire.h>
#include "HT_SSD1306Wire.h"
#include <EEPROM.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
// ===== OLED =====
static SSD1306Wire display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);
#define Vext 25 // adjust if needed
// ===== Button =====
#define BUTTON_PIN 0
bool lastButtonState = HIGH;
unsigned long lastClickTime = 0;
int clickCount = 0;
#define CLICK_DELAY 300 // ms for double/triple click
// ===== EEPROM =====
#define EEPROM_SIZE 4
#define PAGE_ADDR 0
// ===== WiFi Upload =====
// const char* ssid = "YOUR_WIFI";
// const char* password = "YOUR_PASSWORD";
WebServer server(80);
#define MAX_BOOK_SIZE 500000 // 500 KB limit
File bookFile;
// ===== Book Reading =====
int totalPages = 0;
int currentPage = 0;
#define PAGE_LINES 6 // lines per page
#define LINE_HEIGHT 10 // OLED font height
// ===== Functions =====
void VextON() {
pinMode(Vext, OUTPUT);
digitalWrite(Vext, LOW);
}
void drawPage(String pageText) {
display.clear();
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFont(ArialMT_Plain_10);
display.drawStringMaxWidth(0, 0, 128, pageText);
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(128, 54, String(currentPage + 1) + "/" + String(totalPages));
display.display();
}
String getPage(int pageNumber) {
if (!SPIFFS.exists("/book.txt")) return ""; // No book
File file = SPIFFS.open("/book.txt");
if (!file) return "Error opening book";
String page = "";
int lineCount = 0;
int startLine = pageNumber * PAGE_LINES;
int currentLine = 0;
while (file.available()) {
String line = file.readStringUntil('\n');
if (currentLine >= startLine && lineCount < PAGE_LINES) {
page += line + "\n";
lineCount++;
}
currentLine++;
if (lineCount >= PAGE_LINES) break;
}
file.close();
return page;
}
// ===== Web Server Upload Page =====
String uploadPage = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Upload Book (.txt)</h2>
<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="book">
<input type="submit" value="Upload">
</form>
</body>
</html>
)rawliteral";
void handleUpload() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
bookFile = SPIFFS.open("/book.txt", FILE_WRITE);
}
else if (upload.status == UPLOAD_FILE_WRITE) {
if (upload.totalSize > MAX_BOOK_SIZE) {
if (bookFile) bookFile.close();
SPIFFS.remove("/book.txt");
Serial.println("File too large!");
return;
}
if (bookFile) bookFile.write(upload.buf, upload.currentSize);
}
else if (upload.status == UPLOAD_FILE_END) {
if (bookFile) bookFile.close();
Serial.println("Upload complete");
// calculate total pages
File f = SPIFFS.open("/book.txt");
int lineCount = 0;
while (f.available()) {
f.readStringUntil('\n');
lineCount++;
}
f.close();
totalPages = (lineCount + PAGE_LINES - 1) / PAGE_LINES;
currentPage = 0;
EEPROM.write(PAGE_ADDR, currentPage);
EEPROM.commit();
}
}
// ===== Setup =====
void setup() {
Serial.begin(115200);
VextON();
delay(100);
pinMode(BUTTON_PIN, INPUT_PULLUP);
display.init();
display.setFont(ArialMT_Plain_10);
EEPROM.begin(EEPROM_SIZE);
currentPage = EEPROM.read(PAGE_ADDR);
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS mount failed");
}
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println(WiFi.localIP());
server.on("/", HTTP_GET, []() {
server.send(200, "text/html", uploadPage);
});
server.on("/upload", HTTP_POST, []() {
server.send(200, "text/plain", "Upload complete");
}, handleUpload);
server.begin();
// calculate total pages if book exists
if (SPIFFS.exists("/book.txt")) {
File f = SPIFFS.open("/book.txt");
int lineCount = 0;
while (f.available()) {
f.readStringUntil('\n');
lineCount++;
}
f.close();
totalPages = (lineCount + PAGE_LINES - 1) / PAGE_LINES;
}
}
// ===== Loop =====
void loop() {
server.handleClient();
bool currentButtonState = digitalRead(BUTTON_PIN);
// detect press
if (lastButtonState == HIGH && currentButtonState == LOW) {
clickCount++;
lastClickTime = millis();
}
lastButtonState = currentButtonState;
// process clicks after delay
if (clickCount > 0 && (millis() - lastClickTime) > CLICK_DELAY) {
if (clickCount == 1) {
if (SPIFFS.exists("/book.txt")) {
currentPage = (currentPage + 1) % totalPages;
}
}
else if (clickCount == 2) {
if (SPIFFS.exists("/book.txt")) {
currentPage--;
if (currentPage < 0) currentPage = totalPages - 1;
}
}
else if (clickCount >= 3) {
// Triple click message
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_10);
// display.drawString(64, 30, "Triple click detected!");
display.drawString(64, 40, WiFi.localIP().toString());
display.display();
delay(1000); // show message for 1s
}
if (SPIFFS.exists("/book.txt")) {
EEPROM.write(PAGE_ADDR, currentPage);
EEPROM.commit();
}
clickCount = 0;
}
// Display page or IP if no book
if (SPIFFS.exists("/book.txt")) {
drawPage(getPage(currentPage));
} else {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_10);
display.drawString(64, 20, "No book uploaded");
display.drawString(64, 40, WiFi.localIP().toString());
display.display();
}
delay(50);
}
I feel like there is more I can do with this that involve the lora side:
- Sync book pages with some sort of server(?)
- Update book with lora instead of wifi
- Meshtastic type node where you can ask the node to give you a page at a time (multiple pages?). Then you could read a book slowly through the mesh. Click “n” for next or something of that nature.
- Better hardware? I still have a nice ebook reader that has better battery life than this.
- Some way to put the device in deep sleep. Right now its designed to be turned off and keep its page. But doing something like meshtastic where the screen goes to sleep after a while might be better?
- Throw the code onto git…somewhere. With instructions and all that. Its Sat and it took effort just to post this. So im leaving it alone for now.
I dont know if ill ever get to the above, but it was fun to try out. Again hope its not against the community rules! It WAS a meshtastic node and someday may go back to again.
Could you make a LORA remote? like it would turn the page upon receiving a n command.
Cool project!
I suppose it could. It would be hard to implement I think…unless it does a cache at some point?