Zum Hauptinhalt springen

TRMNL BYOS client

Was dieses Beispiel zeigt​

Verwenden Sie Inkplate 7SPECTRA als Client eines TRMNL-kompatiblen „Bring Your Own Server“-Servers (BYOS). Der Sketch verbindet sich mit dem WLAN, registriert sich über /api/setup beim Server, ruft dann /api/display ab, zeichnet das vom Server gelieferte Bild und geht in den Deep Sleep, bis zur nächsten Aktualisierung. Der Server, für den der Sketch geschrieben ist, ist Terminus, der offizielle BYOS-Server von TRMNL, der mit Docker betrieben wird.

Der Header des Sketches beschreibt dieses Verhalten:

  • Beim ersten Start zeigt der Bildschirm ein Banner Connecting to WiFi... und danach die MAC-Adresse des Geräts. Sie brauchen die MAC-Adresse, um das Gerät auf dem Server hinzuzufügen.
  • Der api_key des Servers wird im RTC-Speicher abgelegt, der den Deep Sleep übersteht, sodass sich das Gerät nur einmal registriert. Ein vollständiger Stromverlust löscht ihn; der Server liefert für eine bekannte MAC-Adresse denselben Schlüssel zurück, sodass sich das Gerät von selbst erneut koppelt.
  • Bei jedem späteren Aufwachen sendet der Sketch den Schlüssel zusammen mit der Akkuspannung, der WLAN-Signalstärke (RSSI), dem Modell, der Auflösung des Displays und der Ursache des Aufwachens. Er zeichnet das gelieferte Bild und schläft für die Aktualisierungsrate, die der Server vorgibt. Liefert der Server noch das Bild, das bereits auf dem Display ist, überspringt der Sketch den Download und die Aktualisierung.
  • Wenn Sie während des Deep Sleep die Aufwecktaste (GPIO 18) drücken, wacht das Gerät sofort für eine Aktualisierung auf.
  • Wenn das WLAN oder der Server ausfällt, geht der Sketch in den Deep Sleep und versucht es nach einer zunehmend längeren Wartezeit erneut. Schlägt das WLAN dauerhaft fehl, schläft das Gerät, bis die Aufwecktaste gedrückt wird.

Inkplate 7SPECTRA hat keine schnelle partielle Aktualisierung, daher gibt der Sketch die Punkte des WLAN-Verbindungsaufbaus nur im Serial Monitor aus. Mit image.draw() gezeichnete Bilder werden auf die sechs Farben des Displays gedithert.

Vor dem Hochladen​

  • Installieren Sie die Bibliothek ArduinoJson im Library Manager der Arduino IDE.
  • Richten Sie den BYOS-Server ein. Der Header des Sketches nennt für Terminus diese Schritte: Docker installieren, für einen ersten Versuch das Quick-Start-Skript von TRMNL ausführen (curl https://raw.githubusercontent.com/usetrmnl/terminus/refs/heads/main/scripts/docker/quick.sh | bash, unter Windows in WSL2 oder Git Bash) oder für den dauerhaften Einsatz https://github.com/usetrmnl/terminus klonen und bin/setup ausführen, dann in einem Browser Port 2300 des Rechners öffnen, auf dem Terminus läuft, und auf Register klicken, um Ihr Login anzulegen. Führen Sie das Quick-Start-Skript nur einmal aus.
  • Ermitteln Sie die LAN-IP-Adresse Ihres Computers, damit Inkplate den Server erreichen kann. Sie muss mit dem API_URI-Wert übereinstimmen, den Terminus verwendet; er steht in der .env-Datei, die bei der Einrichtung erstellt wird.
  • Fügen Sie im Terminus-Dashboard das Gerät unter Devices mit Add Device hinzu: Wählen Sie das nächstliegende Modell oder legen Sie unter Models eines an, geben Sie die WLAN-MAC-Adresse ein, die Inkplate nach dem Verbinden auf dem Bildschirm anzeigt, und legen Sie die Aktualisierungsrate in Sekunden fest. Erstellen Sie dann den Inhalt: Legen Sie unter Designs eine Vorlage an, prüfen Sie das gerenderte PNG unter Screens, fügen Sie es unter Playlists einer Playlist hinzu und weisen Sie die Playlist unter Devices Ihrem Gerät zu.
  • Tragen Sie im Sketch Ihren Netzwerknamen und Ihr Passwort zwischen die Anführungszeichen der Defines ssid und pass ein und ersetzen Sie YOUR_SERVER_IP in BYOS_SERVER durch die IP-Adresse des Servers, ohne abschließenden Schrägstrich.

Der Quick-Start-Befehl lädt ein Skript aus dem main-Branch des terminus-Repositorys herunter, das sich jederzeit ändern kann, und führt es sofort aus. Öffnen Sie die Adresse des Skripts in Ihrem Browser und lesen Sie es, bevor Sie den Befehl ausführen.

Beispielcode​

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

Dieses Beispiel verwendet außerdem WiFi, HTTPClient und die ESP32-Sleep-Funktionen (arduino-esp32), die zum ESP32-Board-Paket gehören und nicht Teil der Inkplate-Bibliothek sind. Es verwendet auch ArduinoJson (arduinojson.org), das ebenfalls nicht Teil der Inkplate-Bibliothek ist. Ihre Funktionen werden unten so beschrieben, wie der Sketch sie verwendet.

Verwendete Funktionen​

Inkplate()returns None

Erstellt das Inkplate-Objekt. Inkplate 7SPECTRA hat nur einen Sechs-Farben-Modus, daher erhält der Konstruktor kein Modus-Argument.

ReturnsKonstruktor; kein Rückgabewert.

display.begin()returns void

Initialisiert die Bibliothek, die Peripherie und den Framebuffer. Rufen Sie sie einmal in setup() auf.

ReturnsNichts.

display.readBattery()returns double

Misst die Akkuspannung. Der Sketch liest sie, bevor das WLAN startet, damit der Strom des Funkmoduls den Wert, den er an den Server meldet, nicht senkt.

ReturnsAkkuspannung in Volt.

display.clearDisplay()returns void

Füllt den Framebuffer mit Weiß. Das Display wird dadurch nicht verändert.

ReturnsNichts.

display.setTextSize(uint8_t s)returns void

Legt die Textvergrößerung fest, hier 1.

ReturnsNichts.

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

Legt fest, wo der nächste ausgegebene Text beginnt, hier die obere linke Ecke.

ReturnsNichts.

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

Legt die Textfarbe und die hinter jedem Zeichen gezeichnete Hintergrundfarbe fest, hier Schwarz auf Weiß.

ReturnsNichts.

display.println(value)returns size_t

Gibt Text in den Framebuffer aus und wechselt in die nächste Zeile, wie Serial.println().

ReturnsAnzahl der ausgegebenen Bytes.

display.print(value)returns size_t

Gibt Text in den Framebuffer aus, wie Serial.print().

ReturnsAnzahl der ausgegebenen Bytes.

display.display(bool _leaveOn = 0)returns void

Sendet den gesamten Framebuffer an das Display und aktualisiert es.

ReturnsNichts.

display.width()returns int16_t

Gibt die Breite des Displays in Pixeln zurück. Der Sketch sendet sie im Header Width an den Server.

ReturnsBreite des Displays in Pixeln.

display.height()returns int16_t

Gibt die Höhe des Displays in Pixeln zurück. Der Sketch sendet sie im Header Height an den Server.

ReturnsHöhe des Displays in Pixeln.

display.image.draw(const char *path, int x, int y, bool dither = 1, bool invert = 0)returns bool

Zeichnet ein Bild bei x, y. Der Sketch übergibt ihr die Bild-URL aus der Antwort des Servers, Dithering ein und Invertierung aus. Die Bibliothek wählt den Decoder anhand der Dateiendung der Adresse, daher akzeptiert der Sketch nur bmp, dib, png, jpg und jpeg und lehnt eine Adresse mit Query-String oder ohne Dateiendung ab.

Returnstrue, wenn das Bild gezeichnet wurde.

WiFi.mode(wifi_mode_t mode)returns bool

Legt den WLAN-Modus fest: WIFI_STA, um sich mit einem Netzwerk zu verbinden, WIFI_OFF vor dem Schlafen.

Returnstrue, wenn der Modus gesetzt wurde.

WiFi.begin(const char *ssid, const char *pass)returns wl_status_t

Startet den Verbindungsaufbau mit dem WLAN-Netzwerk.

ReturnsDer Verbindungsstatus.

WiFi.status()returns wl_status_t

Gibt den Verbindungsstatus zurück. Der Sketch wartet, bis er WL_CONNECTED ist, höchstens 30 Sekunden lang.

ReturnsDer Verbindungsstatus.

WiFi.macAddress()returns String

Gibt die WLAN-MAC-Adresse als Text zurück. TRMNL identifiziert das Gerät anhand dieser Adresse.

ReturnsDie MAC-Adresse.

WiFi.RSSI()returns int8_t

Gibt die WLAN-Signalstärke zurück, die der Sketch im Header RSSI sendet.

ReturnsSignalstärke in dBm.

WiFi.disconnect(bool wifioff)returns bool

Trennt die Verbindung zum Netzwerk. Der Sketch übergibt vor dem Deep Sleep true, wodurch auch das WLAN-Funkmodul ausgeschaltet wird.

Returnstrue, wenn die Verbindung getrennt wurde.

HTTPClientreturns None

HTTP-Client-Klasse. Der Sketch erstellt in jeder der beiden Anfragefunktionen ein Objekt http.

ReturnsTyp; kein Rückgabewert.

HTTPClient.begin(String url)returns bool

Startet eine HTTP-Anfrage an die Adresse.

Returnstrue, wenn die Anfrage gestartet werden konnte.

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

Fügt der Anfrage einen Header hinzu. Der Sketch sendet ID, Access-Token, Content-Type, Update-Source, Refresh-Rate, Battery-Voltage, RSSI, Model, Width, Height und Image-Cached.

ReturnsNichts.

HTTPClient.GET()returns int

Sendet die GET-Anfrage.

ReturnsDer HTTP-Statuscode oder bei einem Verbindungsfehler eine negative Zahl.

HTTPClient.getString()returns String

Gibt den Body der Antwort als Text zurück.

ReturnsDer Body der Antwort.

HTTPClient.end()returns void

Beendet die Anfrage und gibt ihre Ressourcen frei.

ReturnsNichts.

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

ArduinoJson-Funktion, die JSON-Text in ein Dokument parst. Der Sketch vergleicht das Ergebnis mit DeserializationError::Ok.

ReturnsDeserializationError::Ok, wenn der Text geparst wurde.

JsonDocumentreturns None

ArduinoJson-Dokument, das die geparste Antwort enthält. Der Sketch liest daraus api_key, refresh_rate, status, image_url, update_firmware und filename.

ReturnsTyp; kein Rückgabewert.

esp_sleep_get_wakeup_cause()returns esp_sleep_wakeup_cause_t

Gibt zurück, was den ESP32 aufgeweckt hat. Der Sketch meldet im Header Update-Source ESP_SLEEP_WAKEUP_TIMER als timer, ESP_SLEEP_WAKEUP_EXT0 als button und alles andere als powercycle.

ReturnsDie Ursache des Aufwachens.

esp_sleep_enable_timer_wakeup(uint64_t time_in_us)returns esp_err_t

Legt den Deep-Sleep-Timer in Mikrosekunden fest. Der Sketch rechnet die Aktualisierungsrate des Servers aus Sekunden um.

ReturnsESP_OK bei Erfolg.

esp_sleep_enable_ext0_wakeup(gpio_num_t gpio_num, int level)returns esp_err_t

Weckt den ESP32 aus dem Deep Sleep, wenn der Pin den Pegel erreicht. Der Sketch verwendet GPIO_NUM_18, die Aufwecktaste, bei LOW.

ReturnsESP_OK bei Erfolg.

esp_deep_sleep_start()returns void

Startet den Deep Sleep. Die Funktion kehrt nicht zurück; das Board führt beim Aufwachen setup() erneut aus.

ReturnsNichts.

Serial.begin(unsigned long baud)returns void

Startet die serielle Kommunikation mit der angegebenen Baudrate, hier 115200.

ReturnsNichts.

Serial.println(value)returns size_t

Gibt Text gefolgt von einem Zeilenumbruch im Serial Monitor aus.

ReturnsAnzahl der ausgegebenen Bytes.

Was Sie erwarten können​

Beim ersten Start zeigt der Bildschirm Connecting to WiFi..., dann Connected. ID: gefolgt von der MAC-Adresse des Boards. Sobald das Gerät registriert ist und der Server ein Bild für es hat, zeigt der Bildschirm dieses Bild und das Board schläft bis zur nächsten Aktualisierung. Im Serial Monitor gibt der Sketch beim Verbinden Fortschrittspunkte und für jeden Schritt eine Zeile aus, zum Beispiel Setup response:, Registered with server; api_key stored und Display response: gefolgt von der Antwort des Servers.

🔗Vollständiges Beispiel öffnen

examples/Inkplate7SPECTRA/Advanced/TRMNL/Inkplate7SPECTRA_TRMNL_BYOS

→