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:

  1. Read books! I mean thats why I made it.
  2. 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 ;)
  3. Single click -> next page.
  4. Double click -> prev page
  5. 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

J6SwY2qZLUHcGkY.jpg

HjIUZRCNz12WTVc.jpg

xy047HFEdazQPzu.jpg

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);  
}