TRMNL BYOS klijent
Što ovaj primjer prikazuje
Koristite Inkplate 7SPECTRA kao klijent TRMNL-kompatibilnog poslužitelja „Bring Your Own Server" (BYOS). Skica se spaja na Wi-Fi, registrira se na poslužitelju preko /api/setup, zatim dohvaća /api/display, iscrtava sliku koju poslužitelj vrati i odlazi u duboki san do sljedećeg osvježavanja. Poslužitelj za koji je skica napisana je Terminus, službeni TRMNL-ov BYOS poslužitelj, pokrenut pomoću Dockera.
Zaglavlje skice opisuje ovo ponašanje:
- Pri prvom pokretanju zaslon prikazuje natpis Connecting to WiFi..., a zatim MAC adresu uređaja. MAC adresa vam je potrebna za dodavanje uređaja na poslužitelju.
api_keyposlužitelja pohranjuje se u RTC memoriji koja preživljava duboki san, pa se uređaj registrira samo jednom. Potpuni gubitak napajanja briše ga, a poslužitelj za poznatu MAC adresu vraća isti ključ, pa se uređaj ponovno sam upari.- Pri svakom sljedećem buđenju skica šalje ključ zajedno s naponom baterije, jačinom Wi-Fi signala (RSSI), modelom, rezolucijom zaslona i uzrokom buđenja uređaja. Iscrtava vraćenu sliku i spava onoliko koliko iznosi brzina osvježavanja koju daje poslužitelj. Ako poslužitelj i dalje poslužuje sliku koja je već na zaslonu, skica preskače preuzimanje i osvježavanje.
- Pritisak na tipku za buđenje (GPIO 18) tijekom dubokog sna odmah budi uređaj radi osvježavanja.
- Kada Wi-Fi ili poslužitelj zakažu, skica odlazi u duboki san i ponovno pokušava nakon sve duljeg odmora. Ako Wi-Fi stalno zakazuje, uređaj spava dok se ne pritisne tipka za buđenje.
Inkplate 7SPECTRA nema brzo djelomično osvježavanje, pa skica točkice Wi-Fi spajanja ispisuje samo u serijski monitor. Slike iscrtane pomoću image.draw() dithering se primjenjuje na šest boja zaslona.
Prije učitavanja
- Instalirajte biblioteku ArduinoJson u Library Manageru Arduino IDE-a.
- Postavite BYOS poslužitelj. Zaglavlje skice za Terminus daje ove korake: instalirajte Docker, za prvo isprobavanje pokrenite TRMNL-ovu skriptu za brzi početak (
curl https://raw.githubusercontent.com/usetrmnl/terminus/refs/heads/main/scripts/docker/quick.sh | bash, na Windowsu unutar WSL2 ili Git Basha) ili za trajnu uporabu klonirajtehttps://github.com/usetrmnl/terminusi pokrenitebin/setup, zatim u pregledniku otvorite port 2300 računala na kojem radi Terminus i kliknite Register da biste izradili prijavu. Skriptu za brzi početak pokrenite samo jednom. - Pronađite LAN IP adresu svojeg računala kako bi je Inkplate mogao dosegnuti. Mora odgovarati vrijednosti
API_URIkoju Terminus koristi, a nalazi se u datoteci.envnastaloj tijekom postavljanja. - U Terminusovoj nadzornoj ploči dodajte uređaj pod Devices pomoću Add Device: odaberite najbliži model ili izradite novi pod Models, unesite Wi-Fi MAC adresu koju Inkplate prikaže na zaslonu nakon spajanja i postavite brzinu osvježavanja u sekundama. Zatim izradite sadržaj: izradite predložak pod Designs, provjerite generirani PNG pod Screens, dodajte ga na popis za reprodukciju pod Playlists i dodijelite popis svojem uređaju pod Devices.
- U skici upišite naziv svoje mreže i lozinku između navodnika u definicijama
ssidipass, aYOUR_SERVER_IPuBYOS_SERVERzamijenite IP adresom poslužitelja, bez završne kose crte.
Naredba za brzi početak preuzima skriptu iz glavne grane (main) repozitorija terminus, koja se može promijeniti u bilo kojem trenutku, i odmah je izvršava. Otvorite adresu skripte u pregledniku i pročitajte je prije nego pokrenete naredbu.
Primjer koda
#include "Inkplate.h"
#include <HTTPClient.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#define ssid "" // Your WiFi SSID
#define pass "" // Your WiFi password
#define BYOS_SERVER "http://YOUR_SERVER_IP:2300"
// WAKE button (same pin as the Inkplate7SPECTRA_Wake_Up_Button example): pressing
// it during deep sleep wakes the device for an immediate refresh.
#define WAKE_BUTTON GPIO_NUM_18
// How long to wait for a WiFi connection before backing off (ms)
#define WIFI_TIMEOUT_MS 30000
// Escalating backoff schedules (seconds). Once WIFI_RETRY_TIMES is exhausted
// the device sleeps until the WAKE button is pressed so a dead AP can't
// drain the battery; once API_RETRY_TIMES is exhausted it settles at
// API_FALLBACK_SLEEP.
const int WIFI_RETRY_TIMES[] = {60, 180, 300};
const int API_RETRY_TIMES[] = {15, 30, 60};
#define API_FALLBACK_SLEEP 900
#define NOT_READY_SLEEP 60 // short poll while the server reports not-ready (202/500)
Inkplate display;
String deviceId = "";
// Access-Token issued by /api/setup; preserved across deep sleep so the
// device only registers once. Lost only on full power loss, which is fine -
// the server returns the same key for a known MAC address.
RTC_DATA_ATTR char apiKey[64] = "";
// Last server-issued refresh cadence (seconds), reported back on the next
// request via the Refresh-Rate header
RTC_DATA_ATTR int lastRefreshRate = 900;
// Escalating backoff positions, preserved across deep sleep
RTC_DATA_ATTR int wifiRetryCount = 0;
RTC_DATA_ATTR int apiRetryCount = 0;
// Name of the image currently on the panel: e-paper keeps showing it through
// deep sleep, so an unchanged image needs no download or refresh at all
RTC_DATA_ATTR char lastImageName[256] = "";
double batteryVoltage = 0.0; // read before WiFi so radio current doesn't skew it
// Reported to the server via the Update-Source header
const char *wakeupSource()
{
switch (esp_sleep_get_wakeup_cause())
{
case ESP_SLEEP_WAKEUP_TIMER:
return "timer";
case ESP_SLEEP_WAKEUP_EXT0:
return "button";
default:
return "powercycle";
}
}
void setup()
{
Serial.begin(115200);
display.begin();
// Read the battery BEFORE the radio is energized so WiFi current doesn't
// depress the voltage reported to the server.
batteryVoltage = display.readBattery();
// Painting the panel here would wipe the current image between routine
// refreshes, so only show the connect/pairing banner while the device
// isn't registered yet (it displays the MAC address needed to add the
// device on the server).
bool showBanner = (strlen(apiKey) == 0);
if (showBanner)
{
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.setTextColor(INKPLATE_BLACK, INKPLATE_WHITE);
display.println("Connecting to WiFi...");
display.display();
}
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED)
{
if (millis() - start > WIFI_TIMEOUT_MS)
{
if (showBanner)
{
display.println("WiFi failed; will retry with backoff");
// This panel has no partial refresh, so a one-off full refresh
// is acceptable here before the long backoff sleep.
display.display();
}
wifiErrorSleep(); // deep-sleeps and never returns
}
delay(1000);
Serial.print('.'); // No fast partial refresh on this panel, so progress goes to Serial only
}
wifiRetryCount = 0; // connected: restart the escalation from the top
deviceId = WiFi.macAddress(); // TRMNL identifies devices by MAC address
if (showBanner)
{
display.clearDisplay();
display.setCursor(0, 0);
display.print("Connected. ID: ");
display.print(deviceId);
display.display();
}
if (strlen(apiKey) == 0)
doSetup(); // Register with the BYOS server (first boot only)
doDisplay(); // Fetch and show the first screen
}
void loop()
{
// Everything happens in setup() + deep sleep below;
// ESP32 wakes up, runs setup() again, fetches next screen.
}
void doSetup()
{
HTTPClient http;
String url = String(BYOS_SERVER) + "/api/setup";
if (http.begin(url))
{
http.addHeader("ID", deviceId);
int code = http.GET();
if (code == 200)
{
String payload = http.getString();
Serial.println("Setup response: " + payload);
JsonDocument doc;
if (deserializeJson(doc, payload) == DeserializationError::Ok)
{
const char *key = doc["api_key"] | "";
if (strlen(key) > 0)
{
strncpy(apiKey, key, sizeof(apiKey) - 1);
apiKey[sizeof(apiKey) - 1] = '\0';
Serial.println("Registered with server; api_key stored");
}
}
}
else
{
Serial.printf("Setup request failed, HTTP %d\n", code);
}
http.end();
}
}
// image.draw() picks its decoder from the URL's file extension, so a query
// string or extension-less URL would fail without even downloading; reject
// those up front with a clear log message.
bool hasDrawableExtension(const char *url)
{
String u(url);
String ext = u.substring(u.lastIndexOf('.') + 1);
ext.toLowerCase();
return ext == "bmp" || ext == "dib" || ext == "png" || ext == "jpg" || ext == "jpeg";
}
void doDisplay()
{
HTTPClient http;
String url = String(BYOS_SERVER) + "/api/display";
if (!http.begin(url))
{
Serial.println("http.begin() failed");
apiErrorSleep(); // deep-sleeps and never returns
}
http.addHeader("ID", deviceId);
if (strlen(apiKey) > 0)
http.addHeader("Access-Token", apiKey);
http.addHeader("Content-Type", "application/json");
http.addHeader("Update-Source", wakeupSource());
http.addHeader("Refresh-Rate", String(lastRefreshRate));
http.addHeader("Battery-Voltage", String(batteryVoltage, 2));
http.addHeader("RSSI", String(WiFi.RSSI()));
http.addHeader("Model", "inkplate_7spectra");
http.addHeader("Width", String(display.width()));
http.addHeader("Height", String(display.height()));
http.addHeader("Image-Cached", strlen(lastImageName) > 0 ? "true" : "false");
int code = http.GET();
if (code <= 0 || code >= 400)
{
Serial.printf("Display request failed, HTTP %d\n", code);
http.end();
apiErrorSleep();
}
String payload = http.getString();
http.end();
Serial.println("Display response: " + payload);
JsonDocument doc;
if (deserializeJson(doc, payload) != DeserializationError::Ok)
{
Serial.println("JSON parse failed");
apiErrorSleep();
}
long refreshRate = doc["refresh_rate"].as<long>(); // integer, not string
if (refreshRate <= 0)
refreshRate = 900;
lastRefreshRate = (int)refreshRate;
// The device API reports state in the "status" body field: 0 = image
// ready, 202 = registered but no screen assigned yet, 500 = server error.
int status = doc["status"].as<int>();
if (status != 0)
{
apiRetryCount = 0; // the server answered, so the API path is healthy
if (status == 202 || status == 500)
{
Serial.printf("Server not ready (status %d); polling again shortly\n", status);
goToSleep(NOT_READY_SLEEP);
}
Serial.printf("Unknown status %d; keeping current image\n", status);
goToSleep(refreshRate);
}
apiRetryCount = 0;
String imageUrl = doc["image_url"].as<String>();
bool updateFirmware = doc["update_firmware"].as<bool>();
if (imageUrl.length() == 0)
{
Serial.println("No image_url in response");
apiErrorSleep();
}
// Prefer the stable filename as the identity of the image, falling back
// to the URL
const char *filename = doc["filename"] | "";
String imageName = strlen(filename) > 0 ? String(filename) : imageUrl;
if (imageName == lastImageName)
{
// The e-paper still shows this exact image; skip the download and
// the costly full refresh entirely.
Serial.println("Image unchanged; skipping redraw");
goToSleep(refreshRate);
}
if (!hasDrawableExtension(imageUrl.c_str()))
{
Serial.println("image_url has no drawable extension: " + imageUrl);
apiErrorSleep();
}
display.clearDisplay();
bool ok = display.image.draw(imageUrl.c_str(), 0, 0, true, false);
if (!ok)
{
display.setCursor(0, 0);
display.print("Failed to draw image from URL");
display.display();
apiErrorSleep(); // lastImageName is untouched, so next wake retries
}
display.display();
strncpy(lastImageName, imageName.c_str(), sizeof(lastImageName) - 1);
lastImageName[sizeof(lastImageName) - 1] = '\0';
if (updateFirmware)
{
Serial.println("Firmware update flagged - not implemented, skipping.");
}
goToSleep(refreshRate);
}
void goToSleep(long seconds)
{
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
esp_sleep_enable_timer_wakeup((uint64_t)seconds * 1000000ULL);
esp_sleep_enable_ext0_wakeup(WAKE_BUTTON, LOW);
esp_deep_sleep_start();
}
// Sleep with no timer so only a WAKE press revives the device
void goToSleepButtonOnly()
{
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
esp_sleep_enable_ext0_wakeup(WAKE_BUTTON, LOW);
esp_deep_sleep_start();
}
// Escalating sleep after an API/HTTP/parse failure
void apiErrorSleep()
{
int seconds;
if (apiRetryCount < (int)(sizeof(API_RETRY_TIMES) / sizeof(API_RETRY_TIMES[0])))
seconds = API_RETRY_TIMES[apiRetryCount++];
else
seconds = API_FALLBACK_SLEEP;
goToSleep(seconds);
}
// Escalating sleep after a WiFi connect failure
void wifiErrorSleep()
{
if (wifiRetryCount >= (int)(sizeof(WIFI_RETRY_TIMES) / sizeof(WIFI_RETRY_TIMES[0])))
goToSleepButtonOnly();
goToSleep(WIFI_RETRY_TIMES[wifiRetryCount++]);
}
Ovaj primjer koristi i WiFi, HTTPClient i funkcije spavanja za ESP32 (arduino-esp32), koje dolaze s paketom ESP32 pločica i nisu dio biblioteke Inkplate. Koristi i ArduinoJson (arduinojson.org), koji također nije dio biblioteke Inkplate. Njihove funkcije u nastavku opisane su onako kako ih skica koristi.
Korištene funkcije
Inkplate()returns NoneStvara objekt Inkplate. Inkplate 7SPECTRA ima jedan način rada sa šest boja, pa konstruktor ne prima argument načina rada.
ReturnsKonstruktor; nema povratne vrijednosti.
display.begin()returns voidInicijalizira biblioteku, periferiju i međuspremnik slike. Pozovite je jednom u setup().
ReturnsNišta.
display.readBattery()returns doubleMjeri napon baterije. Skica ga čita prije pokretanja Wi-Fi-ja, kako struja radija ne bi snizila vrijednost koju šalje poslužitelju.
ReturnsNapon baterije u voltima.
display.clearDisplay()returns voidIspunjava međuspremnik slike bijelom bojom. Ne mijenja zaslon.
ReturnsNišta.
display.setTextSize(uint8_t s)returns voidPostavlja povećanje teksta, ovdje 1.
ReturnsNišta.
display.setCursor(int16_t x, int16_t y)returns voidPostavlja mjesto na kojem počinje sljedeći ispisani tekst, ovdje gornji lijevi kut.
ReturnsNišta.
display.setTextColor(uint16_t c, uint16_t bg)returns voidPostavlja boju teksta i boju pozadine iscrtane iza svakog znaka, ovdje crno na bijelom.
ReturnsNišta.
display.println(value)returns size_tIspisuje tekst u međuspremnik slike i prelazi u sljedeći redak, kao Serial.println().
ReturnsBroj ispisanih bajtova.
display.print(value)returns size_tIspisuje tekst u međuspremnik slike, kao Serial.print().
ReturnsBroj ispisanih bajtova.
display.display(bool _leaveOn = 0)returns voidŠalje cijeli međuspremnik slike na zaslon i osvježava ga.
ReturnsNišta.
display.width()returns int16_tVraća širinu zaslona u pikselima. Skica je šalje poslužitelju u zaglavlju Width.
ReturnsŠirina zaslona u pikselima.
display.height()returns int16_tVraća visinu zaslona u pikselima. Skica je šalje poslužitelju u zaglavlju Height.
ReturnsVisina zaslona u pikselima.
display.image.draw(const char *path, int x, int y, bool dither = 1, bool invert = 0)returns boolIscrtava sliku na položaju x, y. Skica joj daje URL slike iz odgovora poslužitelja, s uključenim ditheringom i isključenom inverzijom. Biblioteka dekoder bira prema ekstenziji datoteke u adresi, pa skica prihvaća samo bmp, dib, png, jpg i jpeg, a odbacuje adresu s upitnim nizom (query string) ili bez ekstenzije.
Returnstrue ako je slika iscrtana.
WiFi.mode(wifi_mode_t mode)returns boolPostavlja Wi-Fi način rada: WIFI_STA za spajanje na mrežu, WIFI_OFF prije spavanja.
Returnstrue ako je način rada postavljen.
WiFi.begin(const char *ssid, const char *pass)returns wl_status_tPočinje spajanje na Wi-Fi mrežu.
ReturnsStatus veze.
WiFi.status()returns wl_status_tVraća status veze. Skica čeka da postane WL_CONNECTED, najviše 30 sekundi.
ReturnsStatus veze.
WiFi.macAddress()returns StringVraća Wi-Fi MAC adresu kao tekst. TRMNL po njoj prepoznaje uređaj.
ReturnsMAC adresa.
WiFi.RSSI()returns int8_tVraća jačinu Wi-Fi signala, koju skica šalje u zaglavlju RSSI.
ReturnsJačina signala u dBm.
WiFi.disconnect(bool wifioff)returns boolPrekida vezu s mrežom. Skica prije dubokog sna prosljeđuje true, što ujedno isključuje Wi-Fi radio.
Returnstrue ako je veza prekinuta.
HTTPClientreturns NoneHTTP klijentska klasa. Skica stvara objekt http u svakoj od dviju funkcija zahtjeva.
ReturnsTip; nema povratne vrijednosti.
HTTPClient.begin(String url)returns boolZapočinje HTTP zahtjev prema adresi.
Returnstrue ako se zahtjev mogao započeti.
HTTPClient.addHeader(const String &name, const String &value)returns voidDodaje zaglavlje zahtjevu. Skica šalje ID, Access-Token, Content-Type, Update-Source, Refresh-Rate, Battery-Voltage, RSSI, Model, Width, Height i Image-Cached.
ReturnsNišta.
HTTPClient.GET()returns intŠalje GET zahtjev.
ReturnsHTTP statusni kod ili negativan broj kod pogreške veze.
HTTPClient.getString()returns StringVraća tijelo odgovora kao tekst.
ReturnsTijelo odgovora.
HTTPClient.end()returns voidZavršava zahtjev i oslobađa njegove resurse.
ReturnsNišta.
deserializeJson(JsonDocument &doc, const String &input)returns DeserializationErrorFunkcija biblioteke ArduinoJson koja parsira JSON tekst u dokument. Skica uspoređuje rezultat s DeserializationError::Ok.
ReturnsDeserializationError::Ok kada je tekst parsiran.
JsonDocumentreturns NoneArduinoJson dokument koji sadrži parsirani odgovor. Skica iz njega čita api_key, refresh_rate, status, image_url, update_firmware i filename.
ReturnsTip; nema povratne vrijednosti.
esp_sleep_get_wakeup_cause()returns esp_sleep_wakeup_cause_tVraća što je probudilo ESP32. Skica u zaglavlju Update-Source prijavljuje ESP_SLEEP_WAKEUP_TIMER kao timer, ESP_SLEEP_WAKEUP_EXT0 kao button, a sve ostalo kao powercycle.
ReturnsUzrok buđenja.
esp_sleep_enable_timer_wakeup(uint64_t time_in_us)returns esp_err_tPostavlja mjerač vremena dubokog sna, u mikrosekundama. Skica pretvara brzinu osvježavanja poslužitelja iz sekundi.
ReturnsESP_OK u slučaju uspjeha.
esp_sleep_enable_ext0_wakeup(gpio_num_t gpio_num, int level)returns esp_err_tBudi ESP32 iz dubokog sna kada pin dosegne zadanu razinu. Skica koristi GPIO_NUM_18, tipku za buđenje, na razini LOW.
ReturnsESP_OK u slučaju uspjeha.
esp_deep_sleep_start()returns voidZapočinje duboki san. Ne vraća se; pločica pri buđenju ponovno izvršava setup().
ReturnsNišta.
Serial.begin(unsigned long baud)returns voidPokreće serijsku komunikaciju zadanom brzinom prijenosa (baud), ovdje 115200.
ReturnsNišta.
Serial.println(value)returns size_tIspisuje tekst iza kojeg slijedi novi redak u serijski monitor.
ReturnsBroj ispisanih bajtova.
Što očekivati
Pri prvom pokretanju zaslon prikazuje Connecting to WiFi..., a zatim Connected. ID: iza čega slijedi MAC adresa pločice. Nakon što se uređaj registrira i poslužitelj za njega ima sliku, zaslon prikazuje tu sliku, a pločica spava do sljedećeg osvježavanja. U serijskom monitoru skica ispisuje točkice napretka tijekom spajanja i redak za svaki korak, na primjer Setup response:, Registered with server; api_key stored i Display response: iza čega slijedi odgovor poslužitelja.
🔗Otvorite cijeli primjer
examples/Inkplate7SPECTRA/Advanced/TRMNL/Inkplate7SPECTRA_TRMNL_BYOS