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 otehr books. But not The Wandering Inn ;)
- Single click -> next page.
- Double click -> prev page
- Tripple 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);
}
You must log in or # to comment.

