Skip to main content

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_key posluž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 klonirajte https://github.com/usetrmnl/terminus i pokrenite bin/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_URI koju Terminus koristi, a nalazi se u datoteci .env nastaloj 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 ssid i pass, a YOUR_SERVER_IP u BYOS_SERVER zamijenite 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​

Select Board
Inkplate7SPECTRA_TRMNL_BYOS.ino
#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 None

Stvara 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 void

Inicijalizira biblioteku, periferiju i međuspremnik slike. Pozovite je jednom u setup().

ReturnsNišta.

display.readBattery()returns double

Mjeri 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 void

Ispunjava međuspremnik slike bijelom bojom. Ne mijenja zaslon.

ReturnsNišta.

display.setTextSize(uint8_t s)returns void

Postavlja povećanje teksta, ovdje 1.

ReturnsNišta.

display.setCursor(int16_t x, int16_t y)returns void

Postavlja mjesto na kojem počinje sljedeći ispisani tekst, ovdje gornji lijevi kut.

ReturnsNišta.

display.setTextColor(uint16_t c, uint16_t bg)returns void

Postavlja boju teksta i boju pozadine iscrtane iza svakog znaka, ovdje crno na bijelom.

ReturnsNišta.

display.println(value)returns size_t

Ispisuje tekst u međuspremnik slike i prelazi u sljedeći redak, kao Serial.println().

ReturnsBroj ispisanih bajtova.

display.print(value)returns size_t

Ispisuje 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_t

Vraća širinu zaslona u pikselima. Skica je šalje poslužitelju u zaglavlju Width.

ReturnsŠirina zaslona u pikselima.

display.height()returns int16_t

Vrać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 bool

Iscrtava 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 bool

Postavlja 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_t

Počinje spajanje na Wi-Fi mrežu.

ReturnsStatus veze.

WiFi.status()returns wl_status_t

Vraća status veze. Skica čeka da postane WL_CONNECTED, najviše 30 sekundi.

ReturnsStatus veze.

WiFi.macAddress()returns String

Vraća Wi-Fi MAC adresu kao tekst. TRMNL po njoj prepoznaje uređaj.

ReturnsMAC adresa.

WiFi.RSSI()returns int8_t

Vraća jačinu Wi-Fi signala, koju skica šalje u zaglavlju RSSI.

ReturnsJačina signala u dBm.

WiFi.disconnect(bool wifioff)returns bool

Prekida vezu s mrežom. Skica prije dubokog sna prosljeđuje true, što ujedno isključuje Wi-Fi radio.

Returnstrue ako je veza prekinuta.

HTTPClientreturns None

HTTP klijentska klasa. Skica stvara objekt http u svakoj od dviju funkcija zahtjeva.

ReturnsTip; nema povratne vrijednosti.

HTTPClient.begin(String url)returns bool

Započinje HTTP zahtjev prema adresi.

Returnstrue ako se zahtjev mogao započeti.

HTTPClient.addHeader(const String &name, const String &value)returns void

Dodaje 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 String

Vraća tijelo odgovora kao tekst.

ReturnsTijelo odgovora.

HTTPClient.end()returns void

Završava zahtjev i oslobađa njegove resurse.

ReturnsNišta.

deserializeJson(JsonDocument &doc, const String &input)returns DeserializationError

Funkcija biblioteke ArduinoJson koja parsira JSON tekst u dokument. Skica uspoređuje rezultat s DeserializationError::Ok.

ReturnsDeserializationError::Ok kada je tekst parsiran.

JsonDocumentreturns None

ArduinoJson 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_t

Vrać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_t

Postavlja 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_t

Budi 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 void

Započinje duboki san. Ne vraća se; pločica pri buđenju ponovno izvršava setup().

ReturnsNišta.

Serial.begin(unsigned long baud)returns void

Pokreće serijsku komunikaciju zadanom brzinom prijenosa (baud), ovdje 115200.

ReturnsNišta.

Serial.println(value)returns size_t

Ispisuje 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

→