TinyC Sprachreferenz¶
TinyC ist eine Untermenge von C, die zu Bytecode fuer eine stackbasierte virtuelle Maschine kompiliert wird. Es laeuft sowohl im Browser (JavaScript-VM) als auch auf ESP32/ESP8266 (als Tasmota-Treiber XDRV_124).
Inhaltsverzeichnis¶
- Datentypen
- Literale
- Variablen & Gueltigkeitsbereich
- Operatoren
- Kontrollfluss
- Funktionen
- Callback-Funktionen
- Tasmota-Systemvariablen
- Arrays
- Zeichenketten
- Praeprozessor
- Kommentare
- Typumwandlung
- Eingebaute Funktionen
- Multi-VM Slots (ESP32)
- VM-Grenzen
- Geraetedateiverwaltung (IDE)
- Tastenkuerzel (IDE)
- Beispiele
Datentypen¶
| Typ | Groesse | Beschreibung |
|---|---|---|
int |
32-Bit | Vorzeichenbehaftete Ganzzahl |
float |
32-Bit | IEEE 754 Gleitkommazahl |
char |
8-Bit | Vorzeichenloses Zeichen (maskiert auf 0xFF) |
bool |
32-Bit | Boolescher Wert (0 = false, ungleich 0 = true) |
void |
— | Kein Wert (Rueckgabetyp fuer Funktionen) |
Typ-Aliase¶
| Alias | Entspricht |
|---|---|
int32_t |
int |
uint32_t |
int |
unsigned int |
int |
uint8_t |
char |
Literale¶
Ganzzahl-Literale¶
Gleitkomma-Literale¶
Zeichen-Literale¶
Unterstuetzte Escape-Sequenzen: \n \t \r \\ \' \" \0
Zeichenketten-Literale¶
Zeichenketten-Literale werden fuer die Initialisierung vonchar-Arrays und als Argumente fuer Zeichenketten-Funktionen verwendet.
Boolesche Literale¶
Variablen & Gueltigkeitsbereich¶
Globale Variablen¶
Ausserhalb jeder Funktion deklariert. Von allen Funktionen aus zugaenglich.
Persistente Variablen¶
Globale Variablen mit dem Schluesselwort persist werden automatisch im Flash gespeichert und beim Programmstart wiederhergestellt. Dies entspricht den p: Variablen im Tasmota Scripter.
persist float totalEnergy = 0.0; // wird ueber Neustarts gespeichert
persist int bootCount; // Skalar — 4 Bytes in Datei
persist char deviceName[32]; // Array — 32 Slots in Datei
- Nur globale Variablen koennen
persistsein (keine lokalen Variablen oder Funktionsparameter) - Persist-Variablen werden automatisch geladen aus einer
.pvs-Datei (abgeleitet vom.tcb-Dateinamen, z.B./weather.pvsfuer/weather.tcb) beim Programmstart - Persist-Variablen werden automatisch gespeichert beim Stoppen des Programms (
TinyCStop) saveVars()aufrufen zum manuellen Speichern (z.B. nach Mitternachts-Zaehleraktualisierung)- Maximal 32 Persist-Eintraege pro Programm
- Binaerformat — kompakt und schnell (rohe int32 Werte, Floats als bit-cast int32)
persist float dval = 0.0;
persist float mval = 0.0;
void EverySecond() {
if (tasm_hour == 0 && last_hr != 0) {
dval = smlGet(2); // Tageszaehler aktualisieren
saveVars(); // sofort speichern
}
}
Watch-Variablen (Aenderungserkennung)¶
Globale Variablen mit dem Schluesselwort watch verfolgen automatisch Aenderungen. Bei jedem Schreibzugriff wird der alte Wert als Schattenwert gespeichert — unverzichtbar fuer IOT-Monitoring.
- Nur skalare Globals koennen
watchsein (int, float — keine Arrays oder lokale Variablen) - Jeder Schreibzugriff speichert automatisch den vorherigen Wert und setzt ein Written-Flag
- Benoetigt 2 zusaetzliche Global-Slots pro Watch-Variable (Schatten + Written-Flag)
Intrinsische Funktionen:
| Funktion | Rueckgabe | Beschreibung |
|---|---|---|
changed(var) |
int |
1 wenn aktueller Wert vom Schattenwert abweicht |
delta(var) |
int/float |
aktuell - Schatten (vorzeichenbehaftete Differenz) |
written(var) |
int |
1 wenn Variable seit letztem snapshot() zugewiesen wurde |
snapshot(var) |
void |
Schatten = aktuell, Written-Flag loeschen |
watch float power;
void EverySecond() {
power = sensorGet("ENERGY#Power");
if (changed(power)) {
float diff = delta(power);
// auf Leistungsaenderung reagieren
snapshot(power); // Aenderung bestaetigen
}
}
Lokale Variablen¶
Innerhalb von Funktionen oder Bloecken deklariert. Blockbasierter Gueltigkeitsbereich (neuer Bereich pro { }).
void myFunc() {
int x = 10; // lokal in myFunc
if (x > 5) {
int y = 20; // lokal in diesem Block
}
// y ist hier nicht zugaenglich
}
Funktionsparameter¶
Skalare werden als Wert uebergeben, Arrays als Referenz.
Operatoren¶
Arithmetisch¶
| Op | Beschreibung | Typen |
|---|---|---|
+ |
Addition | int, float, char[] |
- |
Subtraktion | int, float |
* |
Multiplikation | int, float |
/ |
Division | int, float |
% |
Modulo | nur int |
- |
Unaere Negation | int, float |
Hinweis: Fuer char[]-Variablen fuehrt + eine Zeichenkettenverkettung durch (siehe Zeichenketten).
Vergleich¶
| Op | Beschreibung |
|---|---|
== |
Gleich |
!= |
Ungleich |
< |
Kleiner als |
> |
Groesser als |
<= |
Kleiner oder gleich |
>= |
Groesser oder gleich |
Logisch¶
| Op | Beschreibung |
|---|---|
&& |
Logisches UND (Kurzschluss) |
\|\| |
Logisches ODER (Kurzschluss) |
! |
Logisches NICHT |
Bitweise¶
| Op | Beschreibung |
|---|---|
& |
UND |
\| |
ODER |
^ |
XOR |
~ |
NICHT |
<< |
Linksverschiebung |
>> |
Rechtsverschiebung |
Zuweisung¶
| Op | Beschreibung |
|---|---|
= |
Zuweisen (fuer char[]: Zeichenketten-Kopie) |
+= |
Addieren und zuweisen (fuer char[]: Zeichenkette anfuegen) |
-= |
Subtrahieren und zuweisen |
*= |
Multiplizieren und zuweisen |
/= |
Dividieren und zuweisen |
Inkrement / Dekrement¶
Operatorvorrang (hoechste bis niedrigste Prioritaet)¶
- Postfix:
x++x--a[i]f()(type) - Unaer:
++x--x-x!x~x - Multiplikativ:
*/% - Additiv:
+- - Verschiebung:
<<>> - Relational:
<><=>= - Gleichheit:
==!= - Bitweises UND:
& - Bitweises XOR:
^ - Bitweises ODER:
| - Logisches UND:
&& - Logisches ODER:
|| - Zuweisung:
=+=-=*=/=
Kontrollfluss¶
if / else¶
if (condition) {
// ...
}
if (condition) {
// ...
} else {
// ...
}
if (a > 0) {
// ...
} else if (a == 0) {
// ...
} else {
// ...
}
while-Schleife¶
for-Schleife¶
for (int i = 0; i < 10; i++) {
// ...
}
// alle Teile optional:
for (;;) {
// Endlosschleife
break;
}
switch / case¶
Hinweis: Faelle fallen durch, sofern nichtbreak verwendet wird (wie in Standard-C).
break / continue¶
break;— verlasse die innerste Schleife oder switch-Anweisungcontinue;— springe zur naechsten Iteration der innersten Schleife
Funktionen¶
Deklaration¶
Einstiegspunkt¶
Jedes Programm muss eine main()-Funktion haben:
Rekursion¶
Vollstaendig unterstuetzt:
Array-Parameter¶
Arrays werden als Referenz uebergeben:
Callback-Funktionen¶
TinyC unterstuetzt Callback-Funktionen, die Tasmota automatisch bei bestimmten Ereignissen aufruft. Definieren Sie einfach Funktionen mit diesen bekannten Namen — keine Registrierung erforderlich.
Verfuegbare Callbacks¶
| Funktion | Tasmota-Hook | Wann aufgerufen | Anwendungsfall |
|---|---|---|---|
EveryLoop() |
FUNC_LOOP | Jede Hauptschleifen-Iteration (~1–5 ms) | Ultraschnelles Polling, Bit-Banging, zeitkritische E/A |
Every50ms() |
FUNC_EVERY_50_MSECOND | Alle 50 ms (20x/Sek.) | Schnelles Polling, Funkempfang, Sensorabtastung |
Every100ms() |
FUNC_EVERY_100_MSECOND | Alle 100 ms (10x/Sek.) | Mittleres Polling, Display-Aktualisierungen, Entprellen |
EverySecond() |
FUNC_EVERY_SECOND | Jede Sekunde | Periodische Aufgaben, Zaehler, langsames Polling |
JsonCall() |
FUNC_JSON_APPEND | Telemetriezyklus (~300s) | JSON zu MQTT-Telemetrie hinzufuegen |
WebPage() |
FUNC_WEB_ADD_MAIN_BUTTON | Seitenladen (einmalig) | Diagramme, benutzerdefiniertes HTML, Skripte |
WebCall() |
FUNC_WEB_SENSOR | Webseitenaktualisierung (~1s) | Sensorzeilen zur Tasmota-Weboberflaeche hinzufuegen |
WebUI() |
AJAX /tc_ui Aktualisierung | Alle 2s + bei Widget-Aenderung | Interaktives Widget-Dashboard (Schaltflaechen, Regler usw.) |
UdpCall() |
UDP-Paket empfangen | Bei jeder Multicast-Variable | Eingehende UDP-Variablen verarbeiten |
WebOn() |
Benutzerdefinierter HTTP-Endpunkt | Bei Anfrage an webOn()-URL |
REST-APIs, JSON-Endpunkte, Webhooks |
TaskLoop() |
FreeRTOS-Task (ESP32) | Kontinuierliche Schleife im eigenen Task | Hintergrundverarbeitung, unabhaengig vom Haupt-Thread |
CleanUp() |
FUNC_SAVE_BEFORE_RESTART | Vor Geraete-Neustart | Dateien schliessen, Daten sichern, Ressourcen freigeben |
TouchButton(btn, val) |
Touch-Ereignis | Bei GFX-Button/Slider-Beruehrung | Touch-Button-Druecke und Slider-Aenderungen behandeln |
HomeKitWrite(dev, var, val) |
HomeKit-Schreibzugriff | Wenn Apple Home einen Wert aendert | Licht, Schalter, Steckdose von Apple Home steuern |
Command(char cmd[]) |
Benutzerdefinierter Konsolenbefehl | Wenn Benutzer registriertes Praefix in Konsole eingibt | Benutzerdefinierte Tasmota-Befehle verarbeiten (z.B. MP3Play, MP3Stop) |
Event(char cmd[]) |
Tasmota-Event-Regel-Trigger | Bei Event-Befehl aus Regeln oder Konsole |
Auf Tasmota-Regel-Events reagieren |
OnExit() |
Skript-Stopp | Wenn VM gestoppt oder Skript ersetzt wird | Serielle Ports schliessen, Ressourcen freigeben |
OnMqttConnect() |
FUNC_MQTT_INIT | MQTT-Broker verbunden | Topics abonnieren, Status publizieren |
OnMqttDisconnect() |
mqtt_disconnected Flag | MQTT-Broker getrennt | Offline-Status setzen, Publizierung stoppen |
OnMqttData(char topic[], char payload[]) |
FUNC_MQTT_DATA | Nachricht auf abonniertem Topic eingetroffen | Fernbefehle verarbeiten, Sensordaten empfangen |
OnInit() |
Erstes FUNC_NETWORK_UP | Einmal nach erster WiFi-Verbindung | Einmalige Init: Dienste starten, MQTT abonnieren |
OnWifiConnect() |
FUNC_NETWORK_UP | WiFi/Netzwerk verbunden (jedes Mal) | Reconnect-Behandlung |
OnWifiDisconnect() |
FUNC_NETWORK_DOWN | WiFi/Netzwerk getrennt | Netzwerkabhaengige Aufgaben pausieren |
OnTimeSet() |
FUNC_TIME_SYNCED | NTP-Zeit synchronisiert | Zeitbasierte Aktionen planen |
Ausfuehrungsmodell¶
main()laeuft zuerst in einem FreeRTOS-Task (ESP32) —delay()funktioniert als echte blockierende Verzoegerung- Nach dem Anhalten von main bleiben Globale und Heap erhalten — sie werden NICHT freigegeben
- Tasmota ruft periodisch Ihre Callbacks auf, die Globale lesen/aendern koennen
- Callbacks laufen synchron mit einer Instruktionsbegrenzung — kein
delay()erlaubt - Wenn
TaskLoop()definiert ist, laeuft es im selben FreeRTOS-Task nach dem Anhalten von main() —delay()funktioniert, laeuft unabhaengig von Tasmota's Haupt-Thread
Tasmota-Ausgabefunktionen¶
Verwenden Sie diese Funktionen in Callbacks, um Daten an Tasmota zu senden:
| Funktion | Beschreibung | Verwenden in |
|---|---|---|
responseAppend(buf) |
Char-Array an JSON-Telemetrie anfuegen (-> ResponseAppend_P) |
JsonCall() |
responseAppend("literal") |
Zeichenketten-Literal an JSON-Telemetrie anfuegen | JsonCall() |
webSend(buf) |
Char-Array an Webseite senden (-> WSContentSend) |
WebPage() / WebCall() / WebOn() |
webSend("literal") |
Zeichenketten-Literal an Webseite senden | WebPage() / WebCall() / WebOn() |
webFlush() |
Web-Inhaltspuffer zum Client leeren (-> WSContentFlush) |
WebPage() / WebCall() / WebOn() |
webSendFile("filename") |
Dateiinhalt vom Dateisystem an Webseite senden | WebPage() / WebCall() / WebUI() / WebOn() |
addCommand("prefix") |
Benutzerdefiniertes Konsolen-Befehlspraefix registrieren (z.B. "MP3" -> MP3Play, MP3Stop) |
main() |
responseCmnd(buf) |
Char-Array als Konsolenbefehls-Antwort senden | Command() |
responseCmnd("literal") |
Zeichenketten-Literal als Konsolenbefehls-Antwort senden | Command() |
Webseitenformat¶
Verwenden Sie Tasmota's {s} {m} {e} Makros in webSend(), um Tabellenzeilen zu erstellen:
- {s} — Zeile beginnen (Beschriftungsspalte)
- {m} — Mitte (Wertspalte)
- {e} — Zeile beenden
Beispiel: "{s}Temperature{m}25.3 °C{e}" wird als beschriftete Zeile auf der Webseite dargestellt.
JSON-Telemetrieformat¶
Verwenden Sie responseAppend(), um JSON-Fragmente hinzuzufuegen. Beginnen Sie mit einem Komma:
- ",\"Sensor\":{\"Temp\":25}" wird an das Telemetrie-JSON angefuegt
Beispiel¶
int counter = 0;
void EverySecond() {
counter++;
}
void JsonCall() {
// Fuegt an Tasmota MQTT-Telemetrie-JSON an
char buf[64];
sprintf(buf, ",\"TinyC\":{\"Count\":%d}", counter);
responseAppend(buf);
}
void WebCall() {
// Fuegt eine Zeile zur Tasmota-Webseite hinzu
char buf[64];
sprintf(buf, "{s}TinyC Counter{m}%d{e}", counter);
webSend(buf);
}
int main() {
counter = 0;
return 0;
}
Ergebnis: Nach dem Hochladen und Ausfuehren zeigt die Tasmota-Webseite eine "TinyC Counter"-Zeile, die jede Sekunde hochzaehlt, und die MQTT-Telemetrie enthaelt ,"TinyC":{"Count":N}.
Benutzerdefinierte Konsolenbefehle¶
Skripte koennen benutzerdefinierte Tasmota-Konsolenbefehle mit addCommand("prefix") registrieren. Wenn ein Benutzer z.B. MP3Play Sound.mp3 in der Konsole eingibt, erkennt Tasmota das Praefix "MP3", extrahiert den Unterbefehl "PLAY SOUND.MP3" und ruft Command("PLAY SOUND.MP3") im Skript auf.
Hinweis: Tasmota konvertiert das Befehls-Topic in Grossbuchstaben, daher kommen Unterbefehle als "PLAY", "STOP" usw. an. Daten nach einem Leerzeichen (Dateinamen, Zahlen) behalten ihre urspruengliche Gross-/Kleinschreibung.
int volume = 15;
void Command(char cmd[]) {
char buf[64];
if (strFind(cmd, "PLAY") == 0) {
// Play verarbeiten
responseCmnd("Playing");
} else if (strFind(cmd, "STOP") == 0) {
responseCmnd("Stopped");
} else if (strFind(cmd, "VOL") == 0) {
char arg[16];
strSub(arg, cmd, 4, 0); // alles nach "VOL " extrahieren
volume = atoi(arg);
sprintf(buf, "Volume: %d", volume);
responseCmnd(buf);
} else {
responseCmnd("Unknown: Play|Stop|Vol");
}
}
int main() {
addCommand("MP3"); // Praefix "MP3" registrieren
return 0;
}
Ergebnis: Die Eingabe von MP3Play in der Tasmota-Konsole ruft Command("PLAY") auf, MP3Vol 20 ruft Command("VOL 20") auf.
TaskLoop-Beispiel (ESP32)¶
int counter = 0;
void TaskLoop() {
counter++;
char buf[64];
sprintf(buf, "TaskLoop count=%d", counter);
addLog(buf); // erscheint im Tasmota-Konsolenlog
delay(1000); // echte 1-Sekunden-Verzoegerung, blockiert Tasmota nicht
}
void JsonCall() {
char buf[64];
sprintf(buf, ",\"TinyC\":{\"Count\":%d}", counter);
responseAppend(buf);
}
int main() {
addLog("TaskLoop demo starting");
return 0;
}
Ergebnis: TaskLoop() laeuft unabhaengig in einem FreeRTOS-Task und erhoeht den Zaehler jede Sekunde. JsonCall() meldet den Zaehler in der MQTT-Telemetrie. Beide laufen gleichzeitig — der Mutex stellt sicheren VM-Zugriff sicher.
Wichtige Hinweise¶
- Callbacks muessen schnell sein — maximal 200.000 Instruktionen (ESP32) / 20.000 (ESP8266) pro Aufruf
- Kein
delay()in Callbacks (begrenzt auf 100ms falls aufgerufen) — ausserTaskLoop(), das echte Verzoegerungen unterstuetzt main()muss zurueckkehren (nicht endlos schleifen), damit Callbacks aktiviert werden- Nur die acht oben genannten bekannten Namen werden erkannt
- Der Compiler erkennt diese Funktionsnamen automatisch und bettet sie in die Binaerdatei ein
EveryLoop()laeuft bei jeder Hauptschleifen-Iteration (~1–5 ms) — halten Sie es sehr kurz, um Tasmota nicht zu blockierenEvery50ms()ist ideal fuer schnelles, nicht-blockierendes E/A-Polling (SPI-Funk, GPIO usw.)Every100ms()eignet sich fuer Display-Aktualisierungen, Tasten-Entprellen und mittlere Sensorleseraten- Verwenden Sie
WebPage()fuer einmaligen Seiteninhalt (Diagramme, Skripte) — wird einmal beim Laden der Seite aufgerufen - Verwenden Sie
WebCall()fuer Sensor-aehnliche Zeilen, die periodisch aktualisiert werden - Verwenden Sie
UdpCall()zur Verarbeitung eingehender UDP-Multicast-Variablen TaskLoop()laeuft in einem eigenen FreeRTOS-Task (nur ESP32) — kanndelay()frei verwenden, VM-Zugriff ist Mutex-serialisiert mit Haupt-Thread-Callbacks
Dynamische Task-Erzeugung (ESP32)¶
Ueber den festen TaskLoop() hinaus koennen bis zu 4 zusaetzliche Hintergrund-Tasks dynamisch per Namen gestartet werden. Jeder gespawnte Task teilt sich den VM-Zustand des aufrufenden Slots — Globals, Heap, Konstanten — und laeuft parallel zu main(), Callbacks und TaskLoop(). Ideal als Ersatz fuer einmalige Timer, verzoegerte Jobs oder lang laufende Hintergrundarbeiter.
| Funktion | Beschreibung |
|---|---|
spawnTask(char name[]) |
Startet einen FreeRTOS-Task, der name() aufruft. Gibt Pool-Slot 0..3 zurueck oder -1 bei Fehler. Stack-Default 5 KB |
spawnTask(char name[], int stack_kb) |
Gleich, aber mit anpassbarer Stack-Groesse (auf 3..16 KB begrenzt) |
killTask(char name[]) |
Kooperativer Stopp: setzt ein Flag, das der Task beim naechsten Instruktions- oder delay()-Boundary erkennt. Gibt 0 bei Signal, -1 falls nicht laufend |
taskRunning(char name[]) |
Gibt 1 zurueck, wenn ein Task mit diesem Namen im aktuellen Slot laeuft, sonst 0 |
Name muss ein String-Literal sein — der Compiler erzwingt dies und traegt das Ziel zur Compile-Zeit in die Bytecode-Funktionstabelle ein. Dynamische Namen (Variablen/Ausdruecke) werden nicht unterstuetzt. Ein Literal, das keine im Programm definierte Funktion benennt, ist ein Compile-Fehler.
Stack-Groesse-Leitfaden (Default 5 KB reicht fuer die meisten Worker):
- 3 KB — absolutes Minimum, nur sicher wenn der Worker kein addLog / sprintf* / VM-Syscalls ausser Arithmetik und delay() nutzt
- 5 KB (Default) — triviale Worker mit addLog + delay-Schleifen
- 6–8 KB — httpGet ueber HTTP (ohne TLS), kleines JSON-Parsing
- 10–16 KB — httpGet ueber HTTPS/TLS, grosses JSON-Parsing, komplexe Worker-Pipelines
Semantik:
- Geteilte VM: Gespawnte Tasks sehen und veraendern die gleichen Globals/Heap wie
main(). Ideal fuer Worker-Jobs, die globalen Zustand aktualisieren. - Ein Name pro Slot: Ein zweites
spawnTask("foo")waehrendfoonoch laeuft gibt -1 zurueck. ErstkillTask("foo")+taskRunning("foo")pollen. - Kooperatives Toeten:
killTaskist nicht-blockierend. Der Task terminiert am naechsten Instruktions-Boundary oder nachdem das laufendedelay()aufwacht. Verwenden Siewhile (taskRunning("foo")) delay(10);zum Warten. - Mutex-Disziplin: SpawnTasks ehren denselben Mutex wie
TaskLoop().delay()in einem SpawnTask gibt den Mutex frei, sodass andere Tasks und Callbacks laufen koennen. - Auto-Cleanup: Beim Stoppen des Scripts (TinyCStop) werden alle gespawnten Tasks signalisiert und erhalten 2 s zum Beenden.
- Keine Argumente: Die gespawnte Funktion nimmt keine Parameter; ihr Rueckgabewert wird ignoriert.
Beispiel — einmaliger verzoegerter Job:
void Blinker() {
for (int i = 0; i < 5; i++) {
gpioWrite(2, 1); delay(200);
gpioWrite(2, 0); delay(200);
}
}
void Command(char s[]) {
if (strcmp(s, "BLINK") == 0) {
if (taskRunning("Blinker")) {
addLog("Blinker bereits aktiv");
} else {
spawnTask("Blinker");
}
}
}
int main() { return 0; }
TinyCCmd BLINK in der Konsole spawnt den Blinker ohne die Konsole zu blockieren. Ein zweites TinyCCmd BLINK waehrend des Blinkens wird abgelehnt.
Beispiel — paralleler Hintergrund-Downloader:
char url[] = "http://example.com/data.json";
int download_done = 0;
char body[2048];
void Downloader() {
int rc = httpGet(url, body, sizeof(body));
download_done = (rc > 0) ? 1 : -1;
}
void EverySecond() {
if (download_done == 1) {
addLog("Download ok");
download_done = 0;
} else if (download_done == -1) {
addLog("Download fehlgeschlagen");
download_done = 0;
}
}
int main() {
spawnTask("Downloader", 6); // 6 KB Stack fuer HTTPS
return 0;
}
Beispiel — toetbarer Worker:
int worker_ticks = 0;
void Worker() {
while (1) {
worker_ticks++;
delay(500);
}
}
void Command(char s[]) {
if (strcmp(s, "START") == 0 && !taskRunning("Worker")) spawnTask("Worker");
if (strcmp(s, "STOP") == 0) killTask("Worker");
}
int main() { return 0; }
Grenzen:
- Max. 4 gleichzeitige gespawnte Tasks pro Geraet (gemeinsamer Pool ueber alle VM-Slots)
- Funktionsname max. 23 Zeichen
- Stack 2..12 KB, Default 3 KB — fuer HTTPS / JSON / grosse Puffer auf 6+ erhoehen
- ESP8266: Alle vier Aufrufe geben -1 zurueck (nicht unterstuetzt)
Tasmota-Systemvariablen¶
TinyC stellt virtuelle tasm_*-Variablen bereit, die den Tasmota-Systemzustand direkt lesen/schreiben. Sie werden wie normale Variablen verwendet — keine Funktionsaufrufe noetig. Der Compiler uebersetzt sie automatisch in Syscalls.
Verfuegbare Variablen¶
| Variable | Typ | L/S | Beschreibung |
|---|---|---|---|
tasm_wifi |
int | lesen | WiFi-Status (1 = verbunden, 0 = getrennt) |
tasm_mqttcon |
int | lesen | MQTT-Verbindungsstatus (1 = verbunden) |
tasm_teleperiod |
int | lesen/schreiben | Telemetrieperiode in Sekunden (10–3600, begrenzt) |
tasm_uptime |
int | lesen | Geraete-Betriebszeit in Sekunden |
tasm_heap |
int | lesen | Freier Heap-Speicher in Bytes |
tasm_power |
int | lesen/schreiben | Relais-Schaltzustand (Bitmaske, Schreiben schaltet Relais) |
tasm_dimmer |
int | lesen/schreiben | Dimmer-Pegel 0–100 (Schreiben sendet Dimmer-Befehl) |
tasm_temp |
float | lesen | Temperatur vom Tasmota-Sensor (globales TempRead()) |
tasm_hum |
float | lesen | Luftfeuchtigkeit vom Tasmota-Sensor (globales HumRead()) |
tasm_hour |
int | lesen | Aktuelle Stunde (0–23, von RTC) |
tasm_minute |
int | lesen | Aktuelle Minute (0–59, von RTC) |
tasm_second |
int | lesen | Aktuelle Sekunde (0–59, von RTC) |
tasm_year |
int | lesen | Aktuelles Jahr (z.B. 2026, von RTC) |
tasm_month |
int | lesen | Aktueller Monat (1–12, von RTC) |
tasm_day |
int | lesen | Tag des Monats (1–31, von RTC) |
tasm_wday |
int | lesen | Wochentag (1=So, 2=Mo, ... 7=Sa) |
tasm_cw |
int | lesen | ISO-Kalenderwoche (1–53) |
tasm_sunrise |
int | lesen | Sonnenaufgang, Minuten seit Mitternacht (erfordert USE_SUNRISE) |
tasm_sunset |
int | lesen | Sonnenuntergang, Minuten seit Mitternacht (erfordert USE_SUNRISE) |
tasm_time |
int | lesen | Aktuelle Uhrzeit, Minuten seit Mitternacht |
tasm_pheap |
int | lesen | Freier PSRAM-Speicher in Bytes (nur ESP32) |
tasm_smlj |
int | lesen/schreiben | SML JSON-Ausgabe aktivieren/deaktivieren (erfordert USE_SML_M) |
tasm_npwr |
int | lesen | Anzahl der Power-Geraete (Relais) |
Indizierte Tasmota-Zustandsfunktionen¶
| Funktion | Beschreibung |
|---|---|
int tasmPower(int index) |
Power-Zustand des Relais index (0-basiert). Gibt 0 oder 1 zurueck |
int tasmSwitch(int index) |
Schalter-Zustand (0-basiert, Switch1 = Index 0). Gibt -1 bei ungueltigem Index zurueck |
int tasmCounter(int index) |
Impulszaehler-Wert (0-basiert, Counter1 = Index 0). Erfordert USE_COUNTER |
Tasmota String-Info¶
int tasmInfo(int sel, char buf[]) — fuellt buf mit einem Tasmota-Info-String. Gibt Stringlaenge zurueck.
| sel | Inhalt |
|---|---|
| 0 | MQTT-Topic |
| 1 | MAC-Adresse |
| 2 | Lokale IP-Adresse |
| 3 | Friendly Name |
| 4 | Device Name |
| 5 | MQTT Group-Topic |
| 6 | Reset-Grund (String) |
Beispiel:
char topic[64];
tasmInfo(0, topic); // MQTT-Topic holen
char ip[20];
tasmInfo(2, ip); // lokale IP holen
Verwendung¶
// Systemzustand lesen
if (tasm_wifi) {
printStr("WiFi connected\n");
}
// Sensorwerte lesen (float)
float t = tasm_temp;
float h = tasm_hum;
// Echtzeituhr lesen
int h = tasm_hour; // 0–23
int m = tasm_minute; // 0–59
int s = tasm_second; // 0–59
int y = tasm_year; // z.B. 2026
int mo = tasm_month; // 1–12
int d = tasm_day; // 1–31
int wd = tasm_wday; // 1=So..7=Sa
int cw = tasm_cw; // ISO-Kalenderwoche 1–53
// Sonnenauf-/untergangsautomatisierung
int now = tasm_time; // Minuten seit Mitternacht
if (now > tasm_sunset || now < tasm_sunrise) {
tasm_power = 1; // Nacht — Licht einschalten
}
// Systemzustand schreiben
tasm_teleperiod = 60; // Telemetrie auf 60 Sekunden setzen
tasm_power = 1; // Relais EIN schalten
tasm_dimmer = 50; // Dimmer auf 50% setzen
Hinweise¶
- Keine Deklaration noetig —
tasm_*-Namen werden vom Compiler automatisch erkannt - Kein globaler Slot belegt — sie verbrauchen keinen globalen Variablenspeicher
- Nur-Lesen-Erzwingung — Schreiben auf Nur-Lesen-Variablen (z.B.
tasm_wifi = 1) erzeugt einen Kompilierfehler - Float-Typinferenz —
tasm_tempundtasm_humsind in Ausdruecken korrekt alsfloattypisiert - Schreib-Seiteneffekte —
tasm_powerfuehrt denPower-Befehl aus,tasm_dimmerfuehrt denDimmer-Befehl aus,tasm_teleperiodaktualisiert Tasmota's Einstellungen direkt - In der Browser-IDE geben alle Variablen simulierte Werte zurueck
Beispiel — Automatische Leistungssteuerung¶
void EverySecond() {
// Relais ausschalten wenn Temperatur zu hoch
if (tasm_temp > 30.0) {
tasm_power = 0;
}
// Per Web melden
char buf[64];
sprintf(buf, "{s}Temp{m}%.1f C{e}", tasm_temp);
webSend(buf);
}
int main() {
tasm_teleperiod = 30; // schnelle Telemetrie zum Testen
return 0;
}
Arrays¶
Deklaration & Initialisierung¶
int data[10]; // nicht initialisiert
int primes[5] = {2, 3, 5, 7, 11}; // mit Initialisierer
float values[3] = {1.5, 2.5}; // teilweise initialisiert
char name[32] = "TinyC"; // Zeichenketten-Initialisierung (null-terminiert)
char greeting[] = "Hello World"; // Groesse aus String abgeleitet (12)
int flags[] = {1, 0, 1, 1}; // Groesse aus Initialisierer abgeleitet (4)
Wenn die Groesse weggelassen wird ([]), leitet der Compiler sie automatisch ab:
- String-Initialisierer: Groesse = Stringlaenge + 1 (fuer Null-Terminator)
- Array-Initialisierer: Groesse = Anzahl der Elemente
Zugriff¶
Gueltigkeitsbereich¶
- Kleine Arrays (≤16 Elemente) — inline im globalen Datenspeicher oder lokalen Rahmen (schneller Direktzugriff)
- Grosse Arrays (>16 Elemente) — automatisch auf dem VM-Heap allokiert
Array-Speicher¶
Arrays mit bis zu 16 Elementen werden inline im globalen oder lokalen Rahmen gespeichert fuer schnellen Direktzugriff. Arrays mit mehr als 16 Elementen werden vom Compiler automatisch auf dem VM-Heap allokiert — keine spezielle Syntax noetig:
int rgb[3]; // inline (3 ≤ 16) — schneller Direktzugriff
char buf[128]; // Heap (128 > 16) — automatische Allokation
float data[2000]; // Heap (2000 > 16)
int main() {
rgb[0] = 255; // direkter Rahmenzugriff
buf[0] = 'H'; // Heap-Zugriff — gleiche Syntax
data[1999] = 3.14; // Heap-Zugriff
return 0;
}
Sowohl Inline- als auch Heap-Arrays unterstuetzen alle gleichen Operationen: Elementzugriff, Zeichenkettenoperationen auf char[], Uebergabe an Funktionen usw.
Heap-Grenzen:
| Plattform | Max. Heap-Slots | Max. Handles |
|---|---|---|
| ESP8266 | 2.048 (8 KB) | 8 |
| ESP32 | 8.192 (32 KB) | 16 |
| Browser | 16.384 (64 KB) | 32 |
Zeichenketten¶
Zeichenketten in TinyC sind char-Arrays mit Null-Terminierung.
Deklaration¶
Zeichenkettenzuweisung & Verkettung mit +¶
Die Operatoren = und += funktionieren mit char[]-Variablen fuer intuitive Zeichenkettenverarbeitung:
char buf[64];
char name[16] = "World";
// Zeichenketten-Literal oder char-Array zuweisen
buf = "Hello"; // entspricht strcpy(buf, "Hello")
buf = name; // entspricht strcpy(buf, name)
// Mit += anfuegen
buf += " "; // entspricht strcat(buf, " ")
buf += name; // entspricht strcat(buf, name)
// Mit + verketten
buf = buf + "!"; // entspricht strcat(buf, "!")
buf = buf + name; // entspricht strcat(buf, name)
Hinweis: Der +-Operator funktioniert nur, wenn die linke Seite von = dieselbe Variable wie die linke Seite von + ist (d.h. buf = buf + ...). Variablenueberschreitende Verkettung wie a = b + c wird nicht unterstuetzt — verwenden Sie dafuer strcpy + strcat.
Eingebaute Zeichenketten-Funktionen¶
int len = strlen(greeting); // Laenge (ohne \0)
strcpy(buffer, greeting); // Array in Array kopieren
strcpy(buffer, "World"); // Literal in Array kopieren
strcat(buffer, greeting); // Array anfuegen
strcat(buffer, "!"); // Literal anfuegen
int cmp = strcmp(greeting, buffer); // Vergleich: -1, 0 oder 1
printString(greeting); // Zeichenkette ausgeben
Formatierte Zeichenkettenausgabe (sprintf)¶
sprintf unterstuetzt mehrere Werte in einem einzigen Aufruf. Der Compiler erkennt den Typ jedes Wertes automatisch:
char buf[128];
int id = 1;
float temp = 23.5;
char name[] = "sensor";
// Mehrere Werte in einem Aufruf:
sprintf(buf, "id=%d temp=%.1f name=%s", id, temp, name);
// buf = "id=1 temp=23.5 name=sensor"
// Einzelner Wert wie bisher:
sprintf(buf, "x = %d", 42); // "x = 42"
sprintf(buf, "pi = %.2f", 3.14); // "pi = 3.14"
Mehrteilige Zeichenketten erstellen (sprintfAppend)¶
Verwenden Sie sprintfAppend, um Werte an eine bestehende Zeichenkette anzuhaengen:
char report[128];
sprintf(report, "Sensor %d", 1); // "Sensor 1"
sprintfAppend(report, " val=%.1f", 3.14); // "Sensor 1 val=3.1"
printString(report);
| Funktion | Beschreibung |
|---|---|
sprintf(char dst[], "fmt", val, ...) |
Wert(e) in dst formatieren (ueberschreibt). Typ wird automatisch erkannt. |
sprintfAppend(char dst[], "fmt", val, ...) |
Wert(e) formatieren und an dst anfuegen. Typ wird automatisch erkannt. |
Alte Varianten:
sprintfInt,sprintfFloat,sprintfStr,sprintfAppendInt,sprintfAppendFloat,sprintfAppendStrfunktionieren weiterhin.
Format-Spezifikatoren: %d (int), %f %.2f %e %g (float), %s (Zeichenkette).
Zeichenkettenmanipulation¶
char src[64] = "hello,world,test";
char dst[32];
// N-tes Token (1-basiert) nach Trennzeichen extrahieren
int len = strToken(dst, src, ',', 2); // dst = "world", len = 5
// Teilzeichenkette (0-basierte Position, Laenge)
strSub(dst, src, 6, 5); // dst = "world"
strSub(dst, src, -4, 4); // dst = "test" (negativ = vom Ende)
// Teilzeichenketten-Position finden (-1 wenn nicht gefunden)
int pos = strFind(src, "world"); // pos = 6
int no = strFind(src, "xyz"); // no = -1
| Funktion | Beschreibung |
|---|---|
strToken(char dst[], char src[], int delim, int n) |
N-tes Token (1-basiert) durch Trennzeichen delim in dst kopieren. Gibt die Token-Laenge zurueck. |
strSub(char dst[], char src[], int pos, int len) |
len Zeichen ab pos (0-basiert, negativ=vom Ende) in dst kopieren. len=0 kopiert bis zum Ende der Zeichenkette. Gibt die tatsaechliche Laenge zurueck. |
strFind(char haystack[], char needle[]) |
Erstes Vorkommen von needle in haystack finden. Gibt die Position (0-basiert) oder -1 zurueck, wenn nicht gefunden. |
int strToInt(char str[]) |
String in Integer umwandeln (wie atoi) |
float strToFloat(char str[]) |
String in Float umwandeln (wie atof) |
Array-Sortierung¶
| Funktion | Beschreibung |
|---|---|
sortArray(int arr[], int count, int flags) |
Array sortieren. flags: 0=int aufsteigend, 1=float aufsteigend, 2=int absteigend, 3=float absteigend |
arrayFill(int arr[], int value, int count) |
Erste count Elemente mit value fuellen |
arrayCopy(int dst[], int src[], int count) |
count Elemente von src nach dst kopieren |
int smlCopy(int arr[], int count) |
SML-Decoderwerte in Float-Array kopieren. Gibt Anzahl zurueck (erfordert USE_SML_M) |
Zeichenzugriff¶
Escape-Sequenzen in Zeichenketten¶
| Escape | Zeichen |
|---|---|
\n |
Zeilenumbruch |
\t |
Tabulator |
\r |
Wagenruecklauf |
\\ |
Backslash |
\" |
Anfuehrungszeichen |
\' |
Einfaches Anfuehrungszeichen |
\0 |
Null-Terminator |
Praeprozessor¶
#define — Kompilierzeit-Konstanten¶
Einfache Kompilierzeit-Konstanten (keine Makro-Expansion):
Eigenschaften:
- Der Wert muss ein konstanter Ausdruck sein
- Unterstuetzt Arithmetik mit anderen #define-Werten: +, -, *, /
- Verwendet fuer Array-Groessen, Funktionsargumente usw.
- Gueltigkeitsbereich: gesamtes Programm
- Wertlose Definitionen fuer Bedingungen erlaubt: #define ESP32
Einschraenkungen:
- Kein #include
Funktionsaehnliche Makros¶
Parametrisierte Makros fuehren Textersetzung vor der Kompilierung durch:
Verwendung:
LOG("sensor init"); // -> addLog("sensor init")
int v = CLAMP(reading, 100); // -> int v = min(max(reading, 0), 100)
int s = SQUARE(5); // -> int s = (5 * 5)
Eigenschaften:
- Parameter werden durch Ganzwort-Abgleich ersetzt (ersetzt keine Teilbezeichner)
- Verschachtelte Klammern in Argumenten werden korrekt behandelt: LOG(foo(1,2)) funktioniert
- Zeichenketten-Literal-Argumente bleiben erhalten: LOG("hello, world") — das Komma innerhalb der Anfuehrungszeichen wird nicht als Argumenttrenner behandelt
- Verschachtelte Makro-Expansion: Makros im expandierten Rumpf werden expandiert (bis zu 10 Iterationen)
- Mehrere Parameter unterstuetzt: #define ADD(A, B) (A + B)
Makros mit leerem Rumpf — Debug-Entfernung:
#define DBG(M) // leerer Rumpf — kein Ersetzungstext
DBG("checkpoint 1"); // -> vollstaendig entfernt (einschliesslich Semikolon)
int x = 42; // diese Zeile bleibt unberuehrt
Makros mit leerem Rumpf entfernen den gesamten Aufruf einschliesslich des abschliessenden Semikolons. Dies ist nuetzlich zum Entfernen von Debug-Aufrufen in Produktionsversionen:
#ifdef DEBUG
#define DBG(M) addLog(M)
#else
#define DBG(M)
#endif
DBG("init done"); // loggt im Debug, entfernt im Release
Bedingte Kompilierung¶
#define ESP32
#define USE_SENSOR
#ifdef ESP32
int pin = 8; // eingeschlossen — ESP32 ist definiert
#else
int pin = 2; // ausgeschlossen
#endif
#ifndef USE_DISPLAY
// eingeschlossen — USE_DISPLAY ist nicht definiert
#endif
| Direktive | Beschreibung |
|---|---|
#define NAME |
Einen Namen definieren (ohne Wert, fuer Bedingungen) |
#define NAME value |
Einen Namen mit einem konstanten Wert definieren |
#define NAME(A) body |
Funktionsaehnliches Makro mit Textersetzung |
#undef NAME |
Einen zuvor definierten Namen aufheben |
#ifdef NAME |
Block einschliessen, wenn NAME definiert ist |
#ifndef NAME |
Block einschliessen, wenn NAME NICHT definiert ist |
#if EXPR |
Block einschliessen, wenn Ausdruck ungleich Null |
#else |
Alternativer Block |
#endif |
Bedingten Block beenden |
#if-Ausdruecke unterstuetzen:
- Ganzzahl-Literale: #if 1, #if 0
- Definierte Namen (1 wenn definiert, 0 wenn nicht): #if ESP32
- defined(NAME)-Operator: #if defined(ESP32)
- Logische Operatoren: &&, ||, !
- Vergleich: ==, !=, >, <, >=, <=
- Klammern zur Gruppierung
Hinweise: - Bedingungen koennen verschachtelt werden - Uebersprungener Code wird nicht kompiliert (muss keine gueltige Syntax sein) - Zeilennummern in Fehlermeldungen bleiben erhalten
Kommentare¶
Typumwandlung¶
Explizite Umwandlungen¶
float f = 3.14;
int i = (int)f; // schneidet auf 3 ab
int x = 42;
float y = (float)x; // konvertiert zu 42.0
int ch = 321;
char c = (char)ch; // maskiert auf 0xFF -> 65 ('A')
int b = (bool)42; // ungleich Null -> 1
Implizite Konvertierungen¶
Wenn int und float in einem Ausdruck gemischt werden, wird der int-Operand automatisch zu float heraufgestuft:
sizeof-Operator¶
sizeof ist ein Uebersetzungszeit-Operator, der zu einer Ganzzahl-Konstante aufgeloest wird. Keine Laufzeitkosten — der Compiler faltet den Wert direkt in den Bytecode.
Formen¶
sizeof(typ) // int, float, char, bool, struct Tag oder Typedef-Name
sizeof(name) // deklarierte Variable, Array oder Struct
sizeof name // wie oben, ohne Klammern
Groessen (Bytes, nach C-Konvention)¶
| Objekt | sizeof |
|---|---|
int, float |
4 |
char, bool |
1 |
char buf[40] |
40 |
int arr[10] |
40 |
float ff[5] |
20 |
struct Foo |
Summe der Member-Bytes |
struct Foo v[N] |
N x sizeof(struct Foo) |
Hinweis: In TinyCs VM belegt jeder Skalar intern einen 32-Bit-Slot, aber sizeof liefert immer Bytes wie in Standard-C. sizeof(char) ist 1, nicht 4.
Beispiele¶
char buf[80];
int arr[10];
int a = sizeof(int); // 4
int b = sizeof(buf); // 80
int n = sizeof(arr) / sizeof(int); // 10 (Elementzahl-Idiom)
struct Frame { int id; char name[8]; float v; };
int s = sizeof(struct Frame); // 16 (4 + 8 + 4)
In konstanten Ausdruecken¶
sizeof kann in Array-Groessen verwendet werden, da es zu einer Konstante faltet:
Nicht unterstuetzt¶
Alternative: fuer die Elementgroesse eines Arrays sizeof(typ) direkt nutzen, z.B. sizeof(arr) / sizeof(int).
Eingebaute Funktionen¶
Ausgabe¶
| Funktion | Beschreibung |
|---|---|
print(int value) |
Ganzzahl + Zeilenumbruch ausgeben |
print("literal") |
String-Literal ausgeben (automatisch erkannt) |
print(char buf[]) |
Char-Array als String ausgeben (automatisch erkannt) |
printStr("literal") |
Zeichenketten-Literal ausgeben (explizit) |
printString(char arr[]) |
Null-terminiertes Char-Array ausgeben (explizit) |
Hinweis:
print()erkennt den Argumenttyp automatisch. Bei einem String-Literal wird der String ausgegeben. Bei einemchar[]-Array wird der Inhalt als String ausgegeben. Bei einemintwird der numerische Wert ausgegeben. Die expliziten FunktionenprintStr/printStringsind weiterhin verfügbar, aber selten nötig.
GPIO¶
| Funktion | Beschreibung |
|---|---|
pinMode(int pin, int mode) |
Pin-Modus setzen (1=INPUT, 3=OUTPUT, 5=INPUT_PULLUP, 9=INPUT_PULLDOWN) |
digitalWrite(int pin, int value) |
HIGH(1) oder LOW(0) schreiben |
int digitalRead(int pin) |
Pin-Zustand lesen |
int analogRead(int pin) |
Analogwert lesen (0–4095) |
analogWrite(int pin, int value) |
PWM-Wert schreiben |
gpioInit(int pin, int mode) |
Pin von Tasmota freigeben + pinMode |
Zeitsteuerung¶
| Funktion | Beschreibung |
|---|---|
delay(int ms) |
Millisekunden warten |
delayMicroseconds(int us) |
Mikrosekunden warten |
int millis() |
Millisekunden seit Programmstart |
int micros() |
Mikrosekunden seit Programmstart |
Software-Timer¶
4 unabhaengige Countdown-Timer (IDs 0-3) basierend auf millis(). Timer laufen unabhaengig von Callbacks — setzen Sie einen Timer in main() oder einem beliebigen Callback, pruefen Sie ihn in EveryLoop().
| Funktion | Beschreibung |
|---|---|
timerStart(int id, int ms) |
Timer id (0-3) mit ms Millisekunden Timeout starten |
int timerDone(int id) |
Gibt 1 zurueck wenn Timer abgelaufen (oder nie gestartet), 0 wenn laufend |
timerStop(int id) |
Timer abbrechen |
int timerRemaining(int id) |
Verbleibende Millisekunden (0 wenn abgelaufen/gestoppt) |
Beispiel — Wiederholender Timer mit Timeout:
int counter;
void main() {
counter = 0;
timerStart(0, 5000); // Timer 0: alle 5 Sekunden
timerStart(1, 60000); // Timer 1: nach 1 Minute stoppen
}
void EveryLoop() {
if (timerDone(0)) {
counter++;
print(counter);
timerStart(0, 5000); // fuer naechstes Intervall neu starten
}
if (timerDone(1)) {
timerStop(0); // wiederholenden Timer stoppen
}
}
Seriell¶
Es koennen bis zu 3 serielle Ports gleichzeitig geoeffnet sein. serialBegin() gibt einen Handle (0–2) zurueck, der an alle anderen seriellen Funktionen uebergeben werden muss. Bei Fehler wird -1 zurueckgegeben.
| Funktion | Beschreibung |
|---|---|
int serialBegin(int rx, int tx, int baud, int config, int bufsize) |
Seriellen Port oeffnen, gibt Handle (0–2) oder -1 bei Fehler |
serialPrint(int h, "literal") |
Zeichenkette auf Port h ausgeben |
serialPrintInt(int h, int value) |
Ganzzahl auf Port h ausgeben |
serialPrintFloat(int h, float value) |
Gleitkommazahl auf Port h ausgeben |
serialPrintln(int h, "literal") |
Zeichenkette + Zeilenumbruch auf Port h |
int serialRead(int h) |
Byte von Port h lesen (-1 wenn keines verfuegbar) |
int serialAvailable(int h) |
Verfuegbare Bytes auf Port h |
serialClose(int h) |
Port h schliessen |
serialWriteByte(int h, int b) |
Einzelnes Byte an Port h senden |
serialWrite(int h, char str[]) |
Char-Array an Port h senden (binaer-sicher) |
serialWriteBytes(int h, char buf[], int len) |
len Bytes aus Buffer an Port h senden |
serialBegin Parameter:
- rx — GPIO-Pin fuer Empfang (-1 zum Deaktivieren, z.B. nur-TX Geraete)
- tx — GPIO-Pin fuer Senden (-1 zum Deaktivieren, z.B. nur-RX Geraete)
- baud — Baudrate (z.B. 9600, 115200)
- config — Serielles Frame-Format (siehe Tabelle), Standard 3 = 8N1
- bufsize — Empfangspuffer-Groesse in Bytes (64–2048)
Serielle Konfigurations-Werte:
| Wert | Format | Wert | Format | Wert | Format |
|---|---|---|---|---|---|
| 0 | 5N1 | 8 | 5E1 | 16 | 5O1 |
| 1 | 6N1 | 9 | 6E1 | 17 | 6O1 |
| 2 | 7N1 | 10 | 7E1 | 18 | 7O1 |
| 3 | 8N1 | 11 | 8E1 | 19 | 8O1 |
| 4 | 5N2 | 12 | 5E2 | 20 | 5O2 |
| 5 | 6N2 | 13 | 6E2 | 21 | 6O2 |
| 6 | 7N2 | 14 | 7E2 | 22 | 7O2 |
| 7 | 8N2 | 15 | 8E2 | 23 | 8O2 |
Beispiel — ein Port:
// LD2410 Radar: RX=Pin 16, TX=Pin 17, 256000 Baud, 8N1, 256 Byte Puffer
int ser = serialBegin(16, 17, 256000, 3, 256);
if (ser < 0) { addLog("Seriell Fehler"); }
// Nur-TX fuer MP3-Modul: kein RX, TX=Pin 4, 9600 Baud
int mp3 = serialBegin(-1, 4, 9600, 3, 64);
serialWriteByte(mp3, 0x7E);
Beispiel — zwei Ports gleichzeitig:
int radar = serialBegin(16, 17, 256000, 3, 256); // Handle 0
int gps = serialBegin(18, 19, 9600, 3, 256); // Handle 1
void EverySecond() {
while (serialAvailable(gps) > 0) {
int b = serialRead(gps);
// GPS-Byte verarbeiten...
}
}
1-Wire¶
| Funktion | Beschreibung |
|---|---|
owSetPin(int pin) |
GPIO-Pin fuer nativen 1-Wire-Bus setzen |
int owReset() |
Reset-Puls senden, 1 bei Praesenz-Erkennung |
owWrite(int byte) |
Ein Byte auf den Bus schreiben |
int owRead() |
Ein Byte vom Bus lesen |
owWriteBit(int bit) |
Ein einzelnes Bit schreiben (0 oder 1) |
int owReadBit() |
Ein einzelnes Bit lesen |
owSearchReset() |
ROM-Suchstatus zuruecksetzen |
int owSearch(char rom[]) |
Naechstes Geraet finden, 8-Byte-ROM in rom[] speichern, 1 bei Erfolg |
Die nativen 1-Wire-Funktionen verwenden hardware-getimtes Bit-Banging in C — keine externe Bibliothek noetig. Ein 4,7 kΩ Pull-up-Widerstand auf der Datenleitung ist erforderlich. Fuer lange Busse oder stoeranfaellige Umgebungen kann eine DS2480B Seriell-zu-1-Wire-Bruecke verwendet werden (siehe
examples/onewire.tc).
Mathematik¶
| Funktion | Beschreibung |
|---|---|
int abs(int value) |
Absolutwert |
int min(int a, int b) |
Minimum zweier Werte |
int max(int a, int b) |
Maximum zweier Werte |
int map(int val, int fLo, int fHi, int tLo, int tHi) |
Wert von einem Bereich auf einen anderen abbilden |
int random(int min, int max) |
Zufaellige Ganzzahl im Bereich |
float sqrt(float x) |
Quadratwurzel |
float sin(float x) |
Sinus (Bogenmass) |
float cos(float x) |
Kosinus (Bogenmass) |
float exp(float x) |
Exponentialfunktion (e^x) |
float log(float x) |
Natürlicher Logarithmus (ln x) |
float pow(float basis, float exp) |
Potenz (basis^exp) |
float acos(float x) |
Arkuskosinus (Bogenmass) |
float intBitsToFloat(int bits) |
Int als IEEE 754 Float interpretieren |
int floor(float x) |
Ganzzahlanteil (Richtung −∞) |
int ceil(float x) |
Ganzzahlanteil + 1 (Richtung +∞) |
int round(float x) |
Auf naechste Ganzzahl runden |
Zeichenketten¶
| Funktion | Beschreibung |
|---|---|
int strlen(char arr[]) |
Zeichenkettenlaenge (ohne Null) |
strcpy(char dst[], char src[]) |
Zeichenkette kopieren |
strcpy(char dst[], "literal") |
Literal in Array kopieren |
strcat(char dst[], char src[]) |
Zeichenkette verketten |
strcat(char dst[], "literal") |
Literal verketten |
int strcmp(char a[], char b[]) |
Vergleich: gibt -1, 0 oder 1 zurueck |
printString(char arr[]) |
Zeichenkette ausgeben |
Zeichenketten-Operatoren: char[]-Variablen unterstuetzen auch =, += und + fuer Zeichenkettenzuweisung und -verkettung — siehe Abschnitt Zeichenketten.
sprintf — Formatierte Zeichenketten¶
Einen oder mehrere Werte in einem einzigen Aufruf in ein char-Array formatieren. Der Compiler erkennt den Typ jedes Wertes automatisch und expandiert mehrere Argumente zur Compilezeit.
| Funktion | Beschreibung |
|---|---|
int sprintf(char dst[], "fmt", val, ...) |
Wert(e) in dst formatieren (ueberschreibt). Typ automatisch erkannt. |
int sprintfAppend(char dst[], "fmt", val, ...) |
Wert(e) formatieren, an Ende von dst anfuegen. Typ automatisch erkannt. |
Alte Varianten:
sprintfInt,sprintfFloat,sprintfStr,sprintfAppendInt,sprintfAppendFloat,sprintfAppendStrfunktionieren weiterhin.
Format-Spezifikatoren: %d %i %x (int), %f %.Nf %e %g (float), %s (Zeichenkette).
Alle Funktionen geben die Gesamtlaenge der Zeichenkette zurueck.
char buf[128];
char name[] = "sensor";
int id = 1;
float temp = 23.5;
// Mehrere Werte in einem Aufruf:
sprintf(buf, "id=%d temp=%.1f name=%s", id, temp, name);
// buf = "id=1 temp=23.5 name=sensor"
// sprintfAppend haengt an bestehenden Inhalt an:
sprintf(buf, "ID=%d", id);
sprintfAppend(buf, " val=%.1f", temp);
// buf = "ID=1 val=23.5"
Datei-E/A¶
Dateien auf dem ESP32-Dateisystem (LittleFS) lesen und schreiben. In der Browser-IDE werden Dateien in einem virtuellen Dateisystem simuliert.
| Funktion | Beschreibung |
|---|---|
int fileOpen("path", mode) |
Datei oeffnen, gibt Handle (0–3) oder -1 bei Fehler zurueck |
int fileClose(handle) |
Datei-Handle schliessen, gibt 0 oder -1 zurueck |
int fileRead(handle, char buf[], max) |
Bis zu max Bytes in buf lesen, gibt Anzahl zurueck |
int fileWrite(handle, char buf[], len) |
len Bytes aus buf schreiben, gibt Anzahl zurueck |
int fileExists("path") |
Pruefen ob Datei existiert: 1=ja, 0=nein |
int fileDelete("path") |
Datei loeschen, gibt 0=ok, -1=Fehler zurueck |
int fileSize("path") |
Dateigroesse in Bytes, -1 bei Fehler |
int fileSeek(handle, offset, whence) |
Zur Position springen. Gibt 1=ok, 0=Fehler zurueck |
int fileTell(handle) |
Aktuelle Position in Datei, -1 bei Fehler |
int fsInfo(int sel) |
Dateisystem-Info: sel=0 → Gesamtgroesse KB, sel=1 → frei KB |
int fileOpenDir("path") |
Verzeichnis zum Auflisten oeffnen, gibt Handle oder -1 zurueck |
int fileReadDir(handle, char name[]) |
Naechsten Dateinamen in name lesen. Gibt 1=Eintrag, 0=Ende zurueck |
Dateimodi: 0 = Lesen, 1 = Schreiben (Erstellen/Abschneiden), 2 = Anfuegen
Seek-Modus (whence): 0 = SEEK_SET (vom Anfang), 1 = SEEK_CUR (von aktueller Position), 2 = SEEK_END (vom Ende)
Hinweise:
- Dateipfade können Zeichenketten-Literale oder char[]-Variablen sein (z.B. "/data.txt")
- Dateisystem-Auswahl (Scripter-kompatibel): Standard ist SD-Karte (ufsp). Praefix /ffs/ fuer Flash, /sdfs/ fuer SD-Karte explizit: fileOpen("/ffs/config.txt", 0) oeffnet von Flash, fileOpen("/data.txt", 0) oeffnet von SD-Karte
- Maximal 4 gleichzeitig geoeffnete Dateien (ESP32), 8 im Browser
- Puffer-Argumente (buf) muessen char-Arrays sein, keine Zeichenketten-Literale
- fileRead gibt die tatsaechlich gelesene Byteanzahl zurueck (kann weniger als max sein)
- Schliessen Sie Dateien immer, wenn Sie fertig sind, um Handles freizugeben
// Beispiel: Schreiben und zuruecklesen
char data[32];
char buf[32];
strcpy(data, "Hello!\n");
int f = fileOpen("/test.txt", 1); // Schreibmodus
fileWrite(f, data, strlen(data));
fileClose(f);
f = fileOpen("/test.txt", 0); // Lesemodus
int n = fileRead(f, buf, 31);
buf[n] = 0;
fileClose(f);
printString(buf); // gibt "Hello!" aus
fileDelete("/test.txt"); // aufraeumen
// Beispiel: Dateien in einem Verzeichnis auflisten
char fname[64];
int dir = fileOpenDir("/images");
if (dir >= 0) {
while (fileReadDir(dir, fname)) {
printString(fname);
print("\n");
}
fileClose(dir);
}
Hinweise zur Verzeichnisauflistung:
- fileOpenDir belegt einen Datei-Handle-Slot (gleicher Pool wie fileOpen), mit fileClose schliessen
- fileReadDir gibt nur Dateinamen zurueck (kein Pfad-Praefix), ueberspringt Unterverzeichnisse
- Pfad-Argument kann ein String-Literal oder eine char-Array-Variable sein
Erweiterte Dateioperationen¶
Dateisystemverwaltung, strukturierte Array-Ein-/Ausgabe und Logdatei-Rotation.
| Funktion | Beschreibung |
|---|---|
int fileFormat() |
LittleFS-Dateisystem formatieren (loescht alle Daten). Gibt 0=ok zurueck |
int fileMkdir("pfad") |
Verzeichnis erstellen. Gibt 1=ok, 0=Fehler zurueck |
int fileRmdir("pfad") |
Verzeichnis entfernen. Gibt 1=ok, 0=Fehler zurueck |
int fileReadArray(int arr[], handle) |
Eine Tab-getrennte Zeile in Int-Array lesen. Gibt Elementanzahl zurueck |
fileWriteArray(int arr[], handle) |
Array als Tab-getrennte Zeile mit Zeilenumbruch schreiben |
fileWriteArray(int arr[], handle, append) |
Mit Append-Flag: 1=Zeilenumbruch weglassen (zum Anhaengen mehrerer Arrays in einer Zeile) |
int fileLog("datei", char str[], limit) |
String + Zeilenumbruch an Datei anhaengen. Erste Zeile entfernen wenn Datei limit Bytes ueberschreitet. Gibt Dateigroesse zurueck |
int fileDownload("datei", char url[]) |
URL-Inhalt in Datei herunterladen. Gibt HTTP-Statuscode zurueck (200=ok). Kompatibel mit Scripters frw() |
int fileGetStr(char dst[], handle, "delim", index, endChar) |
Datei von Anfang nach N-tem Vorkommen des Trennzeichens durchsuchen, String bis endChar extrahieren. Gibt Stringlaenge zurueck. Kompatibel mit Scripters fcs() |
fileReadArray / fileWriteArray Format: Werte werden als Dezimaltext durch TAB-Zeichen getrennt gespeichert, ein Array pro Zeile. Kompatibel mit Scripters fra()/fwa() Format.
// Beispiel: Array-Daten speichern und laden
int values[5];
values[0] = 100; values[1] = 200; values[2] = 300;
values[3] = 400; values[4] = 500;
int f = fileOpen("/data.tab", 1); // Schreibmodus
fileWriteArray(values, f); // schreibt "100\t200\t300\t400\t500\n"
fileClose(f);
int loaded[5];
f = fileOpen("/data.tab", 0); // Lesemodus
int n = fileReadArray(loaded, f); // n = 5
fileClose(f);
// Beispiel: Rotierende Logdatei (max 4096 Bytes)
char msg[64];
strcpy(msg, "Sensorwert: 23.5C");
fileLog("/log.txt", msg, 4096);
// Haengt Zeile an, entfernt aelteste Zeile wenn Datei > 4096 Bytes
// Beispiel: Datei aus dem Web herunterladen
char url[128];
strcpy(url, "http://192.168.1.100/data.csv");
int status = fileDownload("/data.csv", url);
// status = 200 bei Erfolg, negativ bei Fehler
// Beispiel: 2. komma-getrenntes Feld aus CSV-Datei extrahieren
// Dateiinhalt: "name,temperature,humidity\nSensor1,23.5,65\n"
int f = fileOpen("/data.csv", 0); // zum Lesen oeffnen
char value[32];
int len = fileGetStr(value, f, ",", 2, '\n');
// value = "23.5", len = 4 (Inhalt zwischen 2. Komma und Zeilenumbruch)
fileClose(f);
Datei-Datenextraktion (IoT-Zeitreihen)¶
Einen Zeitbereich aus Tab-getrennten CSV-Datendateien in Float-Arrays extrahieren fuer die Analyse. Entwickelt fuer IoT-Datensammler die Sensorwerte in regelmaessigen Intervallen protokollieren.
Dateiformat: Erste Spalte ist ein Zeitstempel (ISO oder deutsches Format), gefolgt von Tab-getrennten Float-Werten. Erste Zeile kann eine Kopfzeile sein (wird automatisch uebersprungen).
| Funktion | Beschreibung |
|---|---|
int fileExtract(handle, char from[], char to[], col_offs, accum, int arr1[], ...) |
Zeilen extrahieren wo from <= Zeitstempel <= to. Sucht immer vom Dateianfang. Gibt Zeilenanzahl zurueck |
int fileExtractFast(handle, char from[], char to[], col_offs, accum, int arr1[], ...) |
Wie oben, merkt sich Dateiposition fuer effiziente sequenzielle Zeitbereichsabfragen |
Parameter:
- handle — offener Datei-Handle (von fileOpen)
- from, to — Zeitbereich als char[] (ISO 2024-01-15T12:00:00 oder Deutsch 15.1.24 12:00)
- col_offs — so viele Datenspalten ueberspringen bevor Arrays gefuellt werden (0 = ab erster Datenspalte)
- accum — 0: Werte speichern, 1: zu bestehenden Array-Werten addieren (zum Kombinieren mehrerer Extraktionen)
- arr1, arr2, ... — variable Anzahl Int-Arrays, eines pro zu extrahierender Spalte (max. 16). Werte werden als IEEE 754 Float-Bitmuster gespeichert — Float-Variablen oder Casts zum Lesen verwenden
// Beispiel: Temperatur und Luftfeuchtigkeit fuer einen Tag extrahieren
int temp[96], hum[96]; // 96 = 24h * 4 (15-Min-Intervalle)
char from[24], to[24];
strcpy(from, "15.12.21 00:00");
strcpy(to, "16.12.21 00:00");
int f = fileOpen("/daily.csv", 0);
// col_offs=4 ueberspringt WB,WR1,WR2,WR3 → startet bei ATMP_a (5. Datenspalte)
int rows = fileExtract(f, from, to, 4, 0, temp, hum);
fileClose(f);
// rows = Anzahl 15-Min-Abtastungen, temp[] und hum[] mit Floats gefuellt
// Beispiel: Sequenzielle Tagesabfragen mit fileExtractFast
int energy[96];
char from[24], to[24];
int f = fileOpen("/yearly.csv", 0);
strcpy(from, "1.1.24 00:00");
strcpy(to, "2.1.24 00:00");
int r1 = fileExtractFast(f, from, to, 0, 0, energy);
// Naechster Tag — fileExtractFast ueberspringt bereits gescannte Daten
strcpy(from, "2.1.24 00:00");
strcpy(to, "3.1.24 00:00");
int r2 = fileExtractFast(f, from, to, 0, 0, energy);
fileClose(f);
Zeit- / Zeitstempel-Funktionen¶
Zeitstempel-Konvertierung und -Arithmetik. Unterstuetzt ISO-Webformat (2024-01-15T12:30:45) und deutsches Gebietsformat (15.1.24 12:30). Kompatibel mit Scripters tstamp, cts, tso, tsn, s2t.
| Funktion | Beschreibung |
|---|---|
int timeStamp(char buf[]) |
Aktuellen Tasmota-Zeitstempel in buf schreiben. Gibt 0 zurueck |
int timeConvert(char buf[], flg) |
Zeitstempel-Format in-place konvertieren. 0=Deutsch→Web, 1=Web→Deutsch. Gibt 0 zurueck |
int timeOffset(char buf[], days) |
days Tage zum Zeitstempel in buf addieren (in-place). Gibt 0 zurueck |
int timeOffset(char buf[], days, zeroFlag) |
Mit zeroFlag=1: zusaetzlich Uhrzeit auf Null setzen (HH:MM:SS→00:00:00) |
int timeToSecs(char buf[]) |
Zeitstempel-String in Epochensekunden umwandeln. Gibt Sekunden zurueck |
int secsToTime(char buf[], secs) |
Epochensekunden in ISO-Zeitstempel-String in buf umwandeln. Gibt 0 zurueck |
Format-Erkennung: timeConvert und timeOffset erkennen das Eingabeformat automatisch (ISO wenn T enthalten, sonst Deutsch) und konvertieren entsprechend.
// Beispiel: Aktuelle Zeit holen und Formate konvertieren
char ts[24];
timeStamp(ts); // ts = "2024-06-15T14:30:00"
char de[24];
strcpy(de, ts);
timeConvert(de, 1); // de = "15.6.24 14:30"
timeConvert(de, 0); // de = "2024-06-15T14:30:00" (zurueck zu Web)
// Beispiel: Datumsarithmetik
char ts[24];
timeStamp(ts); // "2024-06-15T14:30:00"
timeOffset(ts, 7); // "2024-06-22T14:30:00" (+ 7 Tage)
timeOffset(ts, -3, 1); // "2024-06-19T00:00:00" (- 3 Tage, Zeit nullen)
// Beispiel: In Sekunden umwandeln und zurueck
char ts[24];
timeStamp(ts);
int secs = timeToSecs(ts); // Epochensekunden
secs = secs + 3600; // 1 Stunde addieren
secsToTime(ts, secs); // zurueck zu Zeitstempel-String
Tasmota-Befehl¶
Einen beliebigen Tasmota-Konsolenbefehl ausfuehren und die JSON-Antwort erfassen.
| Funktion | Beschreibung |
|---|---|
int tasmCmd("command", char response[]) |
Befehl ausfuehren, Antwort speichern, Laenge zurueckgeben |
int tasmCmd(char cmd[], char response[]) |
Befehl ausfuehren (char-Array), Antwort speichern |
Hinweise:
- Befehl kann ein String-Literal oder ein char[]-Array sein
- Der Antwortpuffer sollte ein char-Array sein (empfohlene Groesse: 256)
- Gibt die Laenge der Antwortzeichenkette zurueck, oder -1 bei Fehler
- In der Browser-IDE wird eine simulierte Scheinantwort zurueckgegeben
- Auf dem ESP32 werden echte Tasmota-Befehle ausgefuehrt und die JSON-Antwort erfasst
char resp[256];
int len = tasmCmd("Status 0", resp);
if (len > 0) {
printString(resp); // gibt JSON-Antwort aus
}
Sensor-JSON-Parsing¶
Einen beliebigen Tasmota-Sensorwert ueber seinen JSON-Pfad auslesen. Pfadsegmente werden durch # getrennt (gleiche Konvention wie Tasmota Scripter).
| Funktion | Beschreibung |
|---|---|
float sensorGet("Sensor#Key") |
Sensorwert lesen, gibt float zurueck |
Die Funktion loest intern eine Sensor-Statusabfrage aus und navigiert durch den JSON-Baum. Unterstuetzt bis zu 3 Verschachtelungsebenen.
// BME280-Sensor lesen
float temp = sensorGet("BME280#Temperature");
float hum = sensorGet("BME280#Humidity");
float press = sensorGet("BME280#Pressure");
// SHT3X an Adresse 0x44 lesen
float t = sensorGet("SHT3X_0x44#Temperature");
// Energiezaehler lesen (wenn USE_ENERGY_SENSOR definiert)
float power = sensorGet("ENERGY#Power");
float voltage = sensorGet("ENERGY#Voltage");
float today = sensorGet("ENERGY#Today");
// Verschachtelt: Zigbee-Geraet
float zt = sensorGet("ZbReceived#0x2342#Temperature");
Hinweise:
- Der Pfad muss ein Zeichenketten-Literal sein (wird zur Kompilierzeit aufgeloest)
- Gibt 0.0 zurueck, wenn der Sensor oder Schluessel nicht gefunden wird
- Gibt einen Float zurueck — weisen Sie ihn einer float-Variable zu
- In der Browser-IDE werden Temperature=22.5, Humidity=55.0, Pressure=1013.25 simuliert
Lokalisierte Zeichenketten¶
Lokalisierte Anzeigetexte von Tasmota zur Laufzeit abrufen. Die Texte entsprechen der Spracheinstellung der Firmware (z.B. en_GB.h, de_DE.h). Fuer Web-UI-Beschriftungen verwenden; JSON-Schluessel bleiben auf Englisch.
| Funktion | Beschreibung |
|---|---|
int LGetString(int index, char dst[]) |
Lokalisierten Text nach dst kopieren, gibt Laenge zurueck (0 bei ungueltigem Index) |
Index-Tabelle:
| Index | Tasmota-Define | Englisch | Deutsch |
|---|---|---|---|
| 0 | D_TEMPERATURE | Temperature | Temperatur |
| 1 | D_HUMIDITY | Humidity | Feuchtigkeit |
| 2 | D_PRESSURE | Pressure | Luftdruck |
| 3 | D_DEWPOINT | Dew point | Taupunkt |
| 4 | D_CO2 | Carbon dioxide | Kohlendioxid |
| 5 | D_ECO2 | eCO2 | eCO2 |
| 6 | D_TVOC | TVOC | TVOC |
| 7 | D_VOLTAGE | Voltage | Spannung |
| 8 | D_CURRENT | Current | Strom |
| 9 | D_POWERUSAGE | Power | Leistung |
| 10 | D_POWER_FACTOR | Power Factor | Leistungsfaktor |
| 11 | D_ENERGY_TODAY | Energy Today | Energie Heute |
| 12 | D_ENERGY_YESTERDAY | Energy Yesterday | Energie Gestern |
| 13 | D_ENERGY_TOTAL | Energy Total | Energie Gesamt |
| 14 | D_FREQUENCY | Frequency | Frequenz |
| 15 | D_ILLUMINANCE | Illuminance | Beleuchtungsstaerke |
| 16 | D_DISTANCE | Distance | Entfernung |
| 17 | D_MOISTURE | Moisture | Feuchtigkeit |
| 18 | D_LIGHT | Light | Licht |
| 19 | D_SPEED | Speed | Geschwindigkeit |
| 20 | D_ABSOLUTE_HUMIDITY | Abs Humidity | Abs Feuchtigkeit |
Beispiel:
char lbl[32];
char buf[80];
void web_row(int idx, float val, char unit[]) {
LGetString(idx, lbl);
strcpy(buf, "{s}");
strcat(buf, lbl);
strcat(buf, "{m}");
webSend(buf);
sprintf(buf, "%.1f ", val);
strcat(buf, unit);
strcat(buf, "{e}");
webSend(buf);
}
void WebCall() {
web_row(0, temperature, "°C"); // "Temperatur" (bei de_DE)
web_row(1, humidity, "%"); // "Feuchtigkeit"
web_row(2, pressure, "hPa"); // "Luftdruck"
}
Tasmota-Ausgabe (Callbacks)¶
Daten direkt an Tasmota's Telemetrie- und Websysteme aus Callback-Funktionen senden.
| Funktion | Beschreibung |
|---|---|
void responseAppend(char buf[]) |
Zeichenkette an MQTT-JSON-Telemetrie anfuegen (ResponseAppend_P) |
void responseAppend("literal") |
Zeichenketten-Literal an JSON anfuegen (kein Puffer noetig) |
void webSend(char buf[]) |
Zeichenkette an Webseiten-HTML senden (WSContentSend) |
void webSend("literal") |
Zeichenketten-Literal an Webseite senden (kein Puffer noetig) |
void webFlush() |
Web-Inhaltspuffer zum Client leeren (WSContentFlush) |
void addLog(char buf[]) |
Nachricht ins Tasmota-Log schreiben (AddLog auf INFO-Ebene) |
void addLog("literal") |
Zeichenketten-Literal ins Tasmota-Log schreiben |
webSendJsonArray(float arr[], int count) |
Float-Array als JSON-Integer-Array ausgeben |
Hinweise:
- addLog, webSend und responseAppend akzeptieren sowohl ein char-Array als auch ein Zeichenketten-Literal
- Zeichenketten-Literal-Varianten sind effizienter — keine Kopie durch einen Puffer, direkt aus dem Konstantenpool gesendet
- Verwenden Sie responseAppend() innerhalb von JsonCall() — fuegt an das MQTT-Telemetrie-JSON an
- Verwenden Sie webSend() innerhalb von WebPage() fuer einmaligen Seiteninhalt (Diagramme, Skripte, benutzerdefiniertes HTML)
- Verwenden Sie webSend() innerhalb von WebCall() fuer Sensor-aehnliche Zeilen, die periodisch aktualisiert werden
- Verwenden Sie das Format {s}Beschriftung{m}Wert{e} in webSend() fuer Sensor-aehnliche Tabellenzeilen
- Rufen Sie webFlush() periodisch auf, wenn Sie grosse HTML-Seiten erstellen, um den Chunked-Transfer-Puffer zu leeren (500 Bytes)
- Beginnen Sie JSON mit Komma: ",\"Key\":value" um korrekt an die Telemetrie anzufuegen
- In der Browser-IDE werden beide zur Ausgabekonsole geleitet; webFlush() ist eine Leeroperation
- Callback-Instruktionslimit: 200.000 (ESP32), 20.000 (ESP8266)
- Siehe Callback-Funktionen fuer vollstaendige Beispiele
HTTP-Anfragen¶
HTTP GET/POST-Anfragen an externe APIs stellen. URLs koennen Zeichenketten-Literale oder dynamisch in char-Arrays erstellt sein. Anfragen sind blockierend mit einem 5-Sekunden-Timeout.
| Funktion | Beschreibung |
|---|---|
int httpGet(char url[], char response[]) |
HTTP GET, gibt Antwortlaenge oder negativen Fehler zurueck |
int httpPost(char url[], char data[], char response[]) |
HTTP POST, gibt Antwortlaenge oder negativen Fehler zurueck |
void httpHeader(char name[], char value[]) |
Benutzerdefinierten Header fuer die naechste Anfrage setzen |
int webParse(char source[], "delim", int index, char result[]) |
Nicht-JSON Antworttext parsen (siehe unten) |
Rueckgabewerte: > 0 = Laenge des Antwortkoerpers, 0 = leere Antwort, negativ = HTTP-Fehlercode (z.B. -404).
Beispiel — Daikin-Klimaanlage Sensorabfrage:
char url[64];
char response[256];
char token[32];
int len;
int pos;
void main() {
strcpy(url, "http://192.168.188.43/aircon/get_sensor_info");
len = httpGet(url, response);
// response = "ret=OK,htemp=19.0,hhum=-,otemp=7.0,err=0,cmpfreq=0"
if (len > 0) {
// Innentemperatur extrahieren (htemp)
pos = strFind(response, token); // "htemp=" finden
strToken(token, response, ',', 3); // 3. Token = "htemp=19.0"
printString(token);
}
}
Beispiel — Tasmota-Befehl an ein anderes Geraet:
char url[128];
char response[512];
int len;
void EverySecond() {
strcpy(url, "http://192.168.1.100/cm?cmnd=Status%200");
len = httpGet(url, response);
if (len > 0) {
print(len);
// Antwort mit strFind/strToken parsen...
}
}
Beispiel — POST mit benutzerdefiniertem Header:
char url[128];
char data[128];
char hname[32];
char hval[64];
char response[512];
void main() {
strcpy(url, "http://192.168.1.100/api/data");
strcpy(data, "{\"value\":42}");
strcpy(hname, "Content-Type");
strcpy(hval, "application/json");
httpHeader(hname, hval); // Header vor Anfrage setzen
int len = httpPost(url, data, response);
}
webParse() — Nicht-JSON Web-Antworten parsen
Entspricht Scripters gwr(). Extrahiert Daten aus Klartext-HTTP-Antworten (Key=Value, CSV, zeilenbasierte Formate).
Zwei Modi:
- index > 0 — source an delim aufteilen, das N-te Segment zurueckgeben (1-basiert). Gibt Laenge zurueck.
- index < 0 — Muster delim=wert finden, Wert extrahieren (stoppt bei ,, : oder NUL). Gibt Laenge zurueck.
- index == 0 — Keine Aktion, gibt 0 zurueck.
Beispiel — Daikin-Klimaanlage mit webParse:
char url[64];
char response[256];
char value[32];
void main() {
strcpy(url, "http://192.168.188.43/aircon/get_sensor_info");
int len = httpGet(url, response);
// response = "ret=OK,htemp=19.0,hhum=-,otemp=7.0,err=0,cmpfreq=0"
if (len > 0) {
// Name=Wert Modus: Wert nach "htemp=" extrahieren
webParse(response, "htemp", -1, value); // value = "19.0"
float temp = atof(value);
print(temp); // 19.0
// Split Modus: 4. komma-getrenntes Feld holen
webParse(response, ",", 4, value); // value = "otemp=7.0"
printString(value);
}
}
TCP-Server¶
Einen TCP-Stream-Server starten, um eingehende Verbindungen anzunehmen. Es wird nur ein Client gleichzeitig bedient.
| Funktion | Beschreibung |
|---|---|
int tcpServer(int port) |
TCP-Server auf port starten. Gibt 0=ok, -1=Fehler, -2=kein Netzwerk zurueck |
tcpClose() |
TCP-Server schliessen und Client trennen |
int tcpAvailable() |
Wartenden Client annehmen und verfuegbare Bytes zurueckgeben |
int tcpRead(char buf[]) |
String vom TCP-Client in buf lesen. Gibt gelesene Bytes zurueck |
tcpWrite(char str[]) |
String an TCP-Client senden |
int tcpReadArray(int arr[]) |
Verfuegbare Bytes in Int-Array lesen (ein Byte pro Element). Gibt Anzahl zurueck |
tcpWriteArray(int arr[], int num) |
num Array-Elemente als uint8-Bytes an TCP-Client senden |
tcpWriteArray(int arr[], int num, int type) |
Mit Typ senden: 0=uint8, 1=uint16 BE, 2=sint16 BE, 3=float BE |
Beispiel — Einfacher TCP-Echo-Server:
char buf[128];
void main() {
tcpServer(8888); // auf Port 8888 horchen
}
void Every50ms() {
int n = tcpAvailable(); // Client annehmen + pruefen
if (n > 0) {
tcpRead(buf); // eingehenden String lesen
tcpWrite(buf); // zuruecksenden
}
}
Beispiel — Binaere Datenuebertragung:
int data[100];
void main() {
tcpServer(9000);
}
void EverySecond() {
int n = tcpAvailable();
if (n > 0) {
// Rohdaten in Array lesen
int count = tcpReadArray(data);
print(count);
// als uint16 Big-Endian zuruecksenden
tcpWriteArray(data, count, 1);
}
}
TCP-Client¶
Ausgehende TCP-Verbindungen zu entfernten Hosts oeffnen. Bis zu 4 parallele Client-Slots werden unterstuetzt; ein Selektor waehlt den aktiven Slot, alle Lese/Schreib-Aufrufe arbeiten auf diesem Slot. Slot 0 faellt zusaetzlich auf den vom tcpServer() angenommenen Client zurueck, sodass dieselben tcpRead/tcpWrite/tcpAvailable-Aufrufe fuer beide Rollen funktionieren.
| Funktion | Beschreibung |
|---|---|
int tcpConnect("host", port) |
TCP-Verbindung vom aktiven Slot zu host:port oeffnen. Gibt 0=verbunden, -1=Fehler, -2=kein Netzwerk zurueck |
int tcpConnect(char host[], port) |
Dasselbe mit char-Array als Host (IP oder DNS-Name) statt Literal |
int tcpConnected() |
Gibt 1 zurueck, wenn der aktive Slot eine offene Verbindung hat, sonst 0 |
tcpDisconnect() |
Client-Verbindung des aktiven Slots schliessen |
tcpSelect(int slot) |
Aktiven Client-Slot waehlen (0–3). Alle nachfolgenden Client-Aufrufe zielen auf diesen Slot |
Hinweise:
- tcpRead(buf), tcpWrite(buf), tcpAvailable(), tcpReadArray(), tcpWriteArray() arbeiten alle auf dem aktiven Slot. Mit tcpSelect(n) umschalten.
- tcpWrite() benoetigt weiterhin ein char[] — String-Literale werden nicht akzeptiert (char msg[] = "hello\n"; tcpWrite(msg);).
- Slot 0 ist speziell: Wenn kein ausgehender Client auf Slot 0 offen ist, faellt er transparent auf den Server-Client von tcpServer() zurueck. Bestehende Nur-Server-Skripte laufen unveraendert weiter.
- Verbindungen sind im Wesentlichen nicht-blockierend, haben aber einen kurzen Socket-Timeout — ein fehlgeschlagenes tcpConnect() kehrt schnell mit -1 zurueck.
Beispiel — Periodischer TCP-Client mit Heartbeat:
char rxbuf[128];
char msg[] = "ping\n";
void EverySecond() {
tcpSelect(0); // aktiver Slot = 0
if (!tcpConnected()) {
int r = tcpConnect("192.168.1.50", 1234);
if (r != 0) { return; } // naechsten Tick erneut versuchen
}
tcpWrite(msg);
delay(150); // dem Server Zeit zum Antworten geben
if (tcpAvailable() > 0) {
int n = tcpRead(rxbuf);
print(n); // z.B. 24 Bytes echo zurueck
}
}
void OnExit() {
tcpDisconnect(); // beim Skript-Stopp sauber aufraeumen
}
Beispiel — Zwei unabhaengige TCP-Clients parallel:
char buf[128];
char hello[] = "hello\n";
void main() {
tcpSelect(0);
tcpConnect("10.0.0.10", 9000); // Slot 0 → Metrik-Server
tcpSelect(1);
tcpConnect("10.0.0.11", 9001); // Slot 1 → Befehls-Server
}
void EverySecond() {
// Heartbeat auf Slot 0 senden
tcpSelect(0);
if (tcpConnected()) { tcpWrite(hello); }
// Antworten auf Slot 1 abfragen
tcpSelect(1);
if (tcpConnected() && tcpAvailable() > 0) {
tcpRead(buf);
// Befehl in buf verarbeiten...
}
}
MQTT Abonnieren / Publizieren¶
MQTT-Topics abonnieren und auf eingehende Nachrichten reagieren oder beliebige Payloads publizieren. Benoetigt USE_MQTT in der Firmware (standardmaessig aktiviert).
| Funktion | Beschreibung |
|---|---|
int mqttSubscribe("topic") |
topic abonnieren. Gibt Abo-Slot (0–9) bei Erfolg zurueck, -1 bei Fehler (kein freier Slot, Broker offline) |
int mqttSubscribe(char topic[]) |
Dasselbe mit char-Array als Topic (zur Laufzeit gebaut) |
int mqttUnsubscribe("topic") |
Zuvor abonniertes Topic abbestellen. Gibt 0=ok, -1=nicht gefunden zurueck |
mqttPublish("topic", "payload") |
payload auf topic publizieren (Literale oder char-Arrays akzeptiert) |
Hinweise:
- Bis zu 10 Abonnements pro VM, Topic maximal 128 Zeichen.
- Wildcard '#' wird ausschliesslich als Praefix-Match am Ende unterstuetzt ("sensors/#" matcht sensors/temp, sensors/humi/1 usw.). MQTTs +-Single-Level-Wildcard wird nicht unterstuetzt.
- Passende Topics loesen den OnMqttData(char topic[], char payload[]) Callback aus. Beide Strings werden fuer die Dauer des Callbacks in den VM-Heap kopiert.
- Abonnements bleiben ueber TinyCRun-Neuladen desselben Slots erhalten. mqttUnsubscribe() in OnExit() aufrufen, wenn bei Neustart eine saubere Basis gewuenscht ist.
- Abonnements werden bei Reconnect automatisch neu an den Broker gesendet (ueber FUNC_MQTT_INIT).
Beispiel — Fernsteuerung via MQTT:
char reply[64];
void main() {
mqttSubscribe("cmnd/room1/#"); // Wildcard-Praefix
mqttSubscribe("home/heartbeat"); // exakter Match
}
void OnMqttData(char topic[], char payload[]) {
if (strcmp(topic, "home/heartbeat") == 0) {
mqttPublish("stat/room1/alive", "ok");
return;
}
// cmnd/room1/light → GPIO umschalten etc.
sprintf(reply, "got %s = %s", topic, payload);
addLogLevel(2, reply);
}
void OnExit() {
mqttUnsubscribe("cmnd/room1/#");
mqttUnsubscribe("home/heartbeat");
}
mDNS-Dienstankuendigung¶
Das Geraet als mDNS-Dienst im lokalen Netzwerk registrieren, um Geraeteemulation zu ermoeglichen (Everhome ecotracker, Shelly oder benutzerdefinierte Dienste).
| Funktion | Beschreibung |
|---|---|
int mdnsRegister("name", "mac", "type") |
mDNS-Responder starten und Dienst ankuendigen. Gibt 0 bei Erfolg zurueck |
Parameter (alle Zeichenketten-Literale):
- name — Hostname-Praefix. Verwenden Sie "-" fuer Tasmota's Standard-Hostname, oder ein benutzerdefiniertes Praefix (MAC wird automatisch angefuegt)
- mac — MAC-Adresse. Verwenden Sie "-" fuer die eigene MAC des Geraets (Kleinbuchstaben, ohne Doppelpunkte), oder geben Sie eine benutzerdefinierte Zeichenkette an
- type — Diensttyp: "everhome" (ecotracker), "shelly" oder ein beliebiger benutzerdefinierter Dienstname
Eingebaute Emulationstypen:
- "everhome" — registriert _everhome._tcp mit IP-, Serial-, Productid-TXT-Eintraegen
- "shelly" — registriert _http._tcp und _shelly._tcp mit Firmware-Metadaten-TXT-Eintraegen
- Jede andere Zeichenkette — registriert _<type>._tcp mit IP- und Serial-TXT-Eintraegen
Beispiel — Everhome-Ecotracker-Emulation:
Dies entspricht Scripter's mdnsRegister("ecotracker-", "-", "everhome").
WebUI-Widgets¶
Interaktive Dashboards mit Widget-Funktionen erstellen. Widgets koennen an zwei Stellen erscheinen:
- Dedizierte
/tc_ui-Seite — verwenden Sie denWebUI()-Callback - Tasmota-Hauptseite (Sensorbereich) — verwenden Sie den
WebCall()-Callback
Beide Callbacks verwenden die gleichen Widget-Funktionen.
| Funktion | Beschreibung |
|---|---|
webButton(var, "label") |
Umschalt-Schaltflaeche (0/1) — zeigt EIN/AUS, Klick schaltet um |
webSlider(var, min, max, "label") |
Bereichsregler — ziehen zum Einstellen des Werts |
webCheckbox(var, "label") |
Kontrollkaestchen (0/1) — Aktivieren/Deaktivieren schaltet um |
webText(chararray, maxlen, "label") |
Texteingabe — Zeichenkettenvariable bearbeiten |
webNumber(var, min, max, "label") |
Zahleneingabe mit Min/Max-Grenzen |
webPulldown(var, "label", "opt0\|opt1\|opt2") |
Dropdown-Auswahl mit Beschriftung — Pipe-getrennte Optionen, 0-basierter Index. "@getfreepins" als Optionen zeigt verfuegbare GPIO-Pins |
webRadio(var, "opt0\|opt1\|opt2") |
Optionsschaltflaechengruppe — Pipe-getrennte Optionen, 0-basierter Index |
webTime(var, "label") |
Zeitauswahl (HH:MM) — gespeichert als HHMM-Ganzzahl (z.B. 1430 = 14:30) |
webPageLabel(page, "label") |
Seite 0–5 mit einer Schaltflaechenbeschriftung auf der Hauptseite registrieren |
int webPage() |
Gibt die aktuelle Seitennummer zurueck, die gerendert wird (in WebUI() zur Verzweigung verwenden) |
webConsoleButton("/url", "label") |
Schaltflaeche im Tasmota-Utilities-Menue registrieren (max 4). Navigiert zu URL bei Klick |
Das erste Argument der Widget-Funktionen ist immer eine globale Variable, die das Widget liest und in die es schreibt. Der Compiler uebergibt automatisch die Adresse der Variable an den Syscall.
Beispiel — Widgets auf der Hauptseite:
int relay;
int brightness;
void WebCall() {
webButton(relay, "Power");
webSlider(brightness, 0, 100, "Brightness");
}
Beispiel — Mehrere Seiten mit benutzerdefinierten Schaltflaechen:
Bis zu 6 Seiten koennen mit webPageLabel() registriert werden. Jede erstellt eine Schaltflaeche auf der Tasmota-Hauptseite. Verwenden Sie webPage() innerhalb von WebUI(), um verschiedene Widgets pro Seite zu rendern.
int power;
int brightness;
int mode;
int alarm_time;
char devname[32];
void WebUI() {
int page = webPage();
if (page == 0) {
webButton(power, "Power");
webSlider(brightness, 0, 100, "Brightness");
webPulldown(mode, "Mode", "Off|Auto|Manual");
}
if (page == 1) {
webTime(alarm_time, "Wake-up Time");
webText(devname, 32, "Device Name");
}
}
int main() {
webPageLabel(0, "Controls"); // erste Schaltflaeche auf der Hauptseite
webPageLabel(1, "Settings"); // zweite Schaltflaeche auf der Hauptseite
return 0;
}
Wenn kein webPageLabel() aufgerufen wird, aber WebUI() existiert, erscheint eine einzelne "TinyC UI"-Schaltflaeche.
Funktionsweise:
1. WebCall() rendert Widgets im Sensorbereich der Tasmota-Hauptseite
2. WebUI() rendert Widgets auf dedizierten Seiten unter http://<device>/tc_ui?p=N
3. webPageLabel(N, "text") registriert Seite N (0–5) mit einer Schaltflaeche auf der Hauptseite
4. webPage() gibt die aktuelle Seitennummer zurueck, damit WebUI() verschiedene Widgets anzeigen kann
5. Wenn Sie einen Regler bewegen / eine Schaltflaeche klicken, sendet JavaScript den neuen Wert per AJAX
6. Der Server schreibt den Wert direkt in die TinyC-globale Variable
7. Die Seite aktualisiert sich automatisch, um den aktualisierten Zustand anzuzeigen
8. Text- und Zahleneingaben pausieren die automatische Aktualisierung waehrend der Bearbeitung (wird bei Fokusverlust fortgesetzt)
WebChart — Automatische Google Charts¶
WebChart() rendert Google Charts auf der Tasmota-Hauptseite mit einem einzigen Funktionsaufruf pro Datenserie. Die Google Charts-Bibliothek und das gesamte JavaScript werden automatisch generiert.
void WebChart(int type, "title", "unit", int color, int pos, int count,
float array[], int decimals, int interval, float ymin, float ymax)
| Parameter | Beschreibung |
|---|---|
type |
Diagrammtyp: 0 = Liniendiagramm, 1 = Saeulendiagramm |
"title" |
Diagrammtitel (String-Literal). Leer "" = Serie zum vorherigen Diagramm hinzufuegen |
"unit" |
Y-Achsen-Einheit (String-Literal, z.B. "°C", "%", "m/s") |
color |
Linien-/Balkenfarbe als Hex-RGB (z.B. 0xe74c3c fuer Rot) |
pos |
Aktuelle Schreibposition im Ringpuffer |
count |
Anzahl gueltiger Datenpunkte (≤ Array-Groesse) |
array |
Float-Array mit den Daten (Ringpuffer) |
decimals |
Anzahl Dezimalstellen fuer Datenwerte (0–6) |
interval |
Minuten zwischen Datenpunkten (fuer X-Achsen-Zeitbeschriftung) |
ymin |
Y-Achsen-Minimum. Wenn ymin >= ymax, automatische Skalierung |
ymax |
Y-Achsen-Maximum. Wenn ymin >= ymax, automatische Skalierung |
Beispiel — 24h Wetterdaten:
#define NPTS 288 // 24h bei 5-Minuten-Intervallen
persist float h_temp[NPTS];
persist float h_hum[NPTS];
persist int h_pos = 0;
persist int h_count = 0;
void WebPage() {
if (h_count < 1) return;
WebChart(0, "Temperatur", "\u00b0C", 0xe74c3c, h_pos, h_count, h_temp, 1, 5, -20, 50);
WebChart(0, "Luftfeuchte", "%", 0x3498db, h_pos, h_count, h_hum, 1, 5, 0, 100);
}
- Fester Bereich fuer Daten mit bekannten Grenzen (Luftfeuchte 0–100, UV-Index 0–12)
- Auto-Skalierung (
0, 0) fuer Daten mit variablem Bereich (Helligkeit, Wind, Regen) - Aufruf aus
WebPage()-Callback — jeder Aufruf erzeugt eine Datenserie - Mehrere Serien in einem Diagramm: erster Aufruf hat Titel, weitere verwenden
""als Titel - Benutzerdefinierte Diagrammgroesse:
webChartSize(width, height)vor dem erstenWebChart()-Aufruf aufrufen, um benutzerdefinierte Diagrammabmessungen in Pixeln festzulegen
HTML aus Dateien einbinden:
Verwenden Sie webSendFile("filename"), um den Inhalt einer Datei vom Geraetedateisystem direkt an die Webseite zu senden. Dies ist nuetzlich fuer grosses HTML, CSS oder JavaScript, das zu gross waere, um in Bytecode-Konstanten kompiliert zu werden.
Die Datei wird in 256-Byte-Stuecken gelesen und per WSContentSend gesendet. Der Dateiname kann mit oder ohne fuehrendes / angegeben werden.
Benutzerdefinierte Web-Handler¶
Benutzerdefinierte HTTP-Endpunkte auf dem Tasmota-Webserver registrieren. Wenn eine Anfrage eintrifft, wird der WebOn()-Callback aufgerufen, wobei die Handler-Nummer ueber webHandler() zugaenglich ist.
| Funktion | Beschreibung |
|---|---|
webOn(int num, "url") |
Handler 1–4 fuer den angegebenen URL-Pfad registrieren |
int webHandler() |
Gibt die Handler-Nummer (1–4) innerhalb des WebOn()-Callbacks zurueck |
int webArg("name", buf) |
HTTP-Anfrageparameter in char-Puffer lesen, gibt Laenge zurueck (0 wenn fehlend) |
Verwenden Sie webSend(buf), um den Antwortkoerper auszugeben. Der Standard-Inhaltstyp der Antwort ist text/plain.
Beispiel — JSON-API-Endpunkt:
char buf[128];
void WebOn() {
int h = webHandler();
if (h == 1) {
// GET /v1/json?id=xxx
char id[32];
int len = webArg("id", id);
sprintf(buf, "{\"handler\":1,\"id\":\"%s\",\"value\":42}", id);
webSend(buf);
}
}
int main() {
webOn(1, "/v1/json");
return 0;
}
Beispiel — Mehrere Endpunkte:
void WebOn() {
int h = webHandler();
char buf[64];
if (h == 1) {
sprintf(buf, "{\"temp\":%.1f}", smlGet(1));
webSend(buf);
}
if (h == 2) {
webSend("OK");
}
}
int main() {
webOn(1, "/api/sensor");
webOn(2, "/api/ping");
return 0;
}
Hinweise:
- Bis zu 4 Handler koennen registriert werden (1–4)
- URLs muessen mit / beginnen (z.B. /v1/json, /api/data)
- webOn() wird in main() aufgerufen — Handler werden beim Programmstart registriert
- Der WebOn()-Callback laeuft nachdem main() zurueckgekehrt ist (wie andere Callbacks)
- webArg() liest sowohl GET-Abfrageparameter als auch POST-Formularfelder
- Aequivalent zu Scripter's won(N, "/url") + >onN-Abschnitt
- CORS ist aktiviert, sodass Endpunkte von externen Anwendungen zugaenglich sind
UDP-Multicast (Scripter-kompatibel)¶
Float-Variablen zwischen Tasmota-Geraeten ueber UDP-Multicast auf 239.255.255.250:1999 teilen. Kompatibel mit Tasmota Scripter's globalem Variablenprotokoll.
| Funktion | Beschreibung |
|---|---|
float udpRecv("name") |
Letzten empfangenen Wert fuer benannte Variable abrufen (0 wenn keiner) |
int udpReady("name") |
Gibt 1 zurueck wenn neuer Wert seit letzter Pruefung empfangen |
void udpSendArray("name", float_arr, count) |
Float-Array per binaeren Multicast senden |
int udpRecvArray("name", float_arr, maxcount) |
Float-Array empfangen, gibt tatsaechliche Anzahl zurueck |
udpSendStr("name", char str[]) |
String über UDP-Multicast senden |
Protokoll:
- Einzelner Float: sende =>name:[4 Bytes IEEE-754 Float]
- Float-Array: sende =>name:[2-Byte LE Anzahl][N x 4-Byte Float]
- Empfang: sowohl ASCII (=>name=value) als auch binaer (einzeln oder Array)
- Multicast-Gruppe: 239.255.255.250, Port 1999
- Maximal 8 ueberwachte Variablennamen, je 16 Zeichen
- Maximal 64 Floats pro Array
Callback: Definieren Sie void UdpCall(), um bei jeder empfangenen Variable benachrichtigt zu werden.
Der UDP-Socket wird beim ersten Schreibzugriff auf eine globale Variable, udpRecv()- oder udpReady()-Aufruf automatisch initialisiert.
Skalare global Float-Variablen werden bei Zuweisung automatisch per UDP gesendet (kein expliziter Aufruf noetig).
Socket-Watchdog: Der Multicast-Socket hat einen eingebauten Inaktivitaets-Watchdog (Standard: 60 Sekunden). Wenn innerhalb der Timeout-Periode kein Paket empfangen wird, wird der Socket automatisch geschlossen und neu geoeffnet. Dies behebt das bekannte ESP32-Problem, bei dem der UDP-Empfangspfad nach variabler Zeit stillschweigend aufhoert zu funktionieren. Mit udp(8, 0, sekunden) kann der Timeout geaendert werden (0 = deaktiviert).
Beispiel (Skalar — automatischer Broadcast):
global float temperature = 0.0; // als 'global' deklariert → sendet automatisch bei Zuweisung
void EverySecond() {
temperature = 20.0 + sin(counter) * 5.0;
// Kein udpSend() noetig — Zuweisung an 'global' Variable sendet automatisch
}
void UdpCall() {
float remote = udpRecv("temperature");
// Fernwert verarbeiten...
}
Beispiel (Array):
float sensors[8];
void EverySecond() {
// 8 Sensorwerte als Array senden
udpSendArray("sensors", sensors, 8);
}
void UdpCall() {
float remote[8];
int n = udpRecvArray("sensors", remote, 8);
// n = Anzahl der tatsaechlich empfangenen Floats
}
Allgemeine UDP-Funktion¶
Scripter-kompatible udp()-Funktion fuer beliebige UDP-Kommunikation. Verwendet einen separaten Socket von der Multicast-Variablenfreigabe oben.
| Funktion | Beschreibung |
|---|---|
int udp(0, int port) |
UDP-Port oeffnen. Gibt 1 bei Erfolg zurueck |
int udp(1, char buf[]) |
Empfangenen String in buf lesen. Gibt Byteanzahl zurueck (0 = nichts) |
void udp(2, char str[]) |
Antwort an Absender-IP und -Port senden |
void udp(3, char url[], char str[]) |
String an url mit dem Port von udp(0) senden |
int udp(4, char buf[]) |
Remote-Absender-IP als String. Gibt Laenge zurueck |
int udp(5) |
Remote-Absender-Port zurueckgeben |
int udp(6, char url[], int port, char str[]) |
String an beliebige url:port senden |
int udp(7, char url[], int port, int arr[], int count) |
Array als Rohbytes an url:port senden |
int udp(8, int welcher, int sekunden) |
Socket-Inaktivitaets-Timeout setzen (welcher: 0=Multicast, 1=Allgemeiner Port; 0=deaktiviert) |
int udp(9, char mcast_ip[], int port) |
Beliebiger UDP-Multicast-Gruppe beitreten und an Port binden. Gibt 1 bei Erfolg zurueck |
Hinweise:
- Das erste Argument (Modus) muss ein ganzzahliges Literal (0-9) sein
- Modi 6 und 7 erstellen einen temporaeren Socket (kein vorheriges udp(0) noetig)
- Modus 1 ist nicht-blockierend: gibt sofort 0 zurueck wenn kein Paket verfuegbar
- Modus 7 sendet das untere Byte jedes Array-Elements
- Modus 8 konfiguriert den Socket-Watchdog: wenn innerhalb von sekunden kein Paket empfangen wird, wird der Socket automatisch zurueckgesetzt. Standard ist 60 Sekunden. 0 zum Deaktivieren.
- Modus 9 tritt einer benutzerdefinierten Multicast-Gruppe bei (z.B. SMA Speedwire 239.12.255.254:9522). Empfang ueber udp(1, buf). Ersetzt eine bestehende Unicast-Bindung auf demselben Socket; udp(0, port) erneut aufrufen, um zurueck zu Unicast zu wechseln.
I2C-Bus¶
Direkter I2C-Bus-Zugriff fuer Sensortreiber (erfordert USE_I2C). Alle Funktionen nehmen bus als letzten Parameter (0 oder 1).
| Funktion | Beschreibung |
|---|---|
int i2cExists(int addr, int bus) |
Pruefen ob Geraet an Adresse antwortet. Gibt 1 zurueck wenn gefunden |
int i2cRead8(int addr, int reg, int bus) |
Einzelnes Byte aus Register lesen. Gibt Bytewert (0–255) zurueck |
int i2cWrite8(int addr, int reg, int val, int bus) |
Einzelnes Byte in Register schreiben. Gibt 1=ok, 0=Fehler zurueck |
int i2cRead(int addr, int reg, char buf[], int len, int bus) |
len Bytes in char-Array lesen. Gibt 1=ok zurueck |
int i2cWrite(int addr, int reg, char buf[], int len, int bus) |
len Bytes aus char-Array schreiben. Gibt 1=ok zurueck |
int i2cRead0(int addr, char buf[], int len, int bus) |
len Bytes ohne Register lesen. Gibt 1=ok zurueck |
int i2cWrite0(int addr, int reg, int bus) |
Nur Register-Byte schreiben (keine Daten). Gibt 1=ok zurueck |
int i2cSetDevice(int addr, int bus) |
Pruefen ob Adresse nicht belegt und ansprechbar. Gibt 1=verfuegbar zurueck |
i2cSetActiveFound(int addr, "type", int bus) |
Adresse als belegt registrieren. Loggt Erkennung |
int i2cReadRS(int addr, int reg, char buf[], int len, int bus) |
I2C-Lesen mit Repeated-Start (SMBus) |
I2cResetActive(int addr, int bus) |
Beanspruchte I2C-Adresse freigeben |
Hinweise:
- bus = 0 oder 1 — waehlt welcher I2C-Bus verwendet wird
- Adresse ist 7-Bit (0x00–0x7F), z.B. 0x48 fuer TMP102
- Register ist 8-Bit (0x00–0xFF)
- Pufferfunktionen verwenden char[]-Arrays — jedes Element enthaelt ein Byte (0–255)
- Maximale Pufferlaenge ist 255 Bytes
- Gibt 0 zurueck wenn I2C nicht einkompiliert ist oder die Operation fehlschlaegt
- i2cSetDevice + i2cSetActiveFound verwenden, um I2C-Adressen korrekt zu beanspruchen und Konflikte mit Tasmota-Treibern zu vermeiden
Beispiel — TMP102-Temperatursensor auf Bus 0 lesen:
#define TMP102_ADDR 0x48
#define TMP102_TEMP 0x00
#define I2C_BUS 0
void EverySecond() {
if (!i2cExists(TMP102_ADDR, I2C_BUS)) return;
char buf[2];
if (i2cRead(TMP102_ADDR, TMP102_TEMP, buf, 2, I2C_BUS)) {
// TMP102: 12-Bit Temperatur in oberen Bits von 2 Bytes
int raw = (buf[0] << 4) | (buf[1] >> 4);
if (raw > 2047) raw = raw - 4096; // Vorzeichenerweiterung
float temp = (float)raw * 0.0625;
char out[64];
sprintf(out, "TMP102: %.2f °C\n", temp);
printString(out);
}
}
Smart Meter (SML)¶
Zaehlerstaende auslesen und Zaehler ueber Tasmota's SML-Treiber steuern (erfordert USE_SML oder USE_SML_M).
SML kann ohne Scripter laufen — nur USE_UFILESYS wird fuer dateibasierte Zaehlerbeschreibungen benoetigt.
Der SML-Deskriptor-Tab der IDE verwaltet die Zaehlerdefinitionsdatei (/sml_meter.def) auf dem Geraet.
Zaehlerstaende lesen¶
| Funktion | Beschreibung |
|---|---|
float smlGet(int index) |
Zaehlerwert abrufen. Index 0 gibt Anzahl zurueck, 1..N gibt Werte zurueck |
int smlGetStr(int index, char buf[]) |
Positiver Index: Zaehler-ID/OBIS-Zeichenkette. Negativer Index: numerischer Wert mit voller Praezision als Zeichenkette (4 Nachkommastellen) — entspricht Scripter's smls[-x] |
Hinweise:
- Index ist 1-basiert: smlGet(1) gibt den ersten Zaehlerwert zurueck
- smlGet(0) gibt die Gesamtzahl der Zaehlervariablen zurueck
- Gibt 0 zurueck wenn SML nicht einkompiliert ist oder der Index ausserhalb des Bereichs liegt
- smlGet()-Werte entsprechen Scripter's sml[x]-Syntax (einfache Float-Praezision)
- smlGetStr(-i, buf) formatiert den zugrundeliegenden double-SML-Wert mit 4 Nachkommastellen — nuetzlich bei kumulativen Energiezaehlern, die float's ~7-stellige Praezision ueberschreiten
Beispiel:
void WebCall() {
char buf[64];
int n = smlGet(0); // Gesamtzaehler
int i = 1;
while (i <= n) {
float val = smlGet(i);
sprintf(buf, "{s}Meter %d{m}%.2f{e}", val);
webSend(buf);
i++;
}
}
Erweiterte Zaehlersteuerung¶
Diese Funktionen erfordern, dass USE_SML_SCRIPT_CMD in der Firmware aktiviert ist.
| Funktion | Beschreibung |
|---|---|
int smlWrite(int meter, char buf[]) |
Hex-Sequenz an Zaehler senden (z.B. Aufweck- oder Anfragebefehle) |
int smlWrite(int meter, "hex") |
Dasselbe, mit Zeichenketten-Literal (kein temporaerer Puffer noetig) |
int smlRead(int meter, char buf[]) |
Rohen Zaehlerpuffer in char-Array lesen, gibt gelesene Bytes zurueck |
int smlSetBaud(int meter, int baud) |
Baudrate des seriellen Ports eines Zaehlers aendern |
int smlSetWStr(int meter, char buf[]) |
Asynchrone Schreibzeichenkette fuer naechsten geplanten Sendevorgang setzen |
int smlSetWStr(int meter, "hex") |
Dasselbe, mit Zeichenketten-Literal |
int smlSetOptions(int options) |
Globale SML-Optionen-Bitmaske setzen |
int smlGetV(int sel) |
Daten-Gueltigkeitsflags abrufen/zuruecksetzen (0=abrufen, 1=zuruecksetzen) |
Hinweise:
- meter ist der 1-basierte Zaehlerindex aus dem SML-Deskriptor
- smlWrite und smlSetWStr akzeptieren entweder ein char[]-Array oder ein Zeichenketten-Literal — der Compiler erkennt automatisch welche Variante verwendet wird
- smlWrite sendet eine hex-kodierte Bytesequenz (z.B. "AA0100") an den seriellen Port des Zaehlers
- smlRead kopiert den rohen Empfangspuffer in ein char-Array fuer benutzerdefiniertes Parsen
- smlSetBaud aendert dynamisch die Baudrate des Zaehlers (nuetzlich fuer Zaehler, die Geschwindigkeitsverhandlung erfordern)
- smlSetWStr setzt eine Hex-Zeichenkette, die beim naechsten geplanten Zaehlerabfragezyklus gesendet wird
- Diese Funktionen ersetzen Scripter's >F/>S-Abschnitt-Zaehlersteuerungsbefehle
Beispiel — OBIS-Zaehler-Aufwecksequenz:
void EverySecond() {
// Zeichenketten-Literal — kein temporaerer Puffer noetig
smlWrite(1, "2F3F210D0A"); // "/?!\r\n" in Hex
}
Beispiel — Dynamische Baudratenverhandlung:
void EverySecond() {
// Zaehlerantwort lesen
char buf[64];
int n = smlRead(1, buf);
if (n > 0 && buf[0] == 0x06) {
// ACK empfangen, auf hohe Geschwindigkeit umschalten
smlSetBaud(1, 9600);
}
}
SML-Deskriptor-Editor (IDE)¶
Die IDE enthaelt einen SML-Deskriptor-Tab im linken Bereich zur Verwaltung von Zaehlerdefinitionen:
- Zaehler-Datenbank: Ein Dropdown laedt
.tas-Zaehlerdefinitionen aus der Community-Datenbank - Benutzerdefinierte Zaehler-URL: Die Datenbank-URL wird aus
/sml_meter_url.txtauf dem Geraetedateisystem gelesen. Um ein anderes Zaehler-Repository zu verwenden, bearbeiten Sie diese Datei mit einer URL, die auf ein Verzeichnis mit einersmartmeter.json-Indexdatei zeigt. Die Standard-URL verweist auf das Community-GitHub-Repository. - RX/TX-Pin-Auswahl: Dropdowns werden aus den freien GPIOs des Geraets befuellt (ueber
freegpio-API) - Pin-Platzhalter:
%0rxpin%und%0txpin%in Deskriptoren werden beim Speichern durch die ausgewaehlten Pins ersetzt - Auf Geraet speichern: Extrahiert nur den
>M-Abschnitt und speichert ihn als/sml_meter.def - Von Geraet laden: Liest die aktuelle
/sml_meter.defvom Geraet
Callback-Zusammenfuehrung¶
Viele .tas-Zaehlerdateien erfordern periodischen Code (Scripter's >S- und >F-Abschnitte) fuer Zaehlerkommunikation, Aufwecksequenzen oder Baudratenverhandlung. In TinyC schreiben Sie diese direkt als Callback-Funktionen im SML-Editor:
void EverySecond() {
smlWrite(1, "2F3F210D0A");
}
>M 1
+1,3,s,16,9600,SML,1
1,1-0:1.8.0*255(@1,Energy In,kWh,E_in,3
#
Funktionsweise:
1. Schreiben Sie TinyC-Callback-Funktionen (EverySecond(), Every100ms() usw.) an beliebiger Stelle im SML-Editor — vor oder nach dem >M-Abschnitt
2. Beim Speichern geht nur der >M-Abschnitt als /sml_meter.def auf das Geraet
3. Beim Kompilieren fuehrt die IDE automatisch SML-Callbacks in das Hauptprogramm zusammen:
- Wenn der Haupteditor bereits denselben Callback hat — wird der SML-Code an den bestehenden Funktionsrumpf angefuegt
- Wenn der Haupteditor ihn nicht hat — wird eine neue Callback-Funktion erstellt
4. Der zusammengefuehrte Quellcode wird als ein Programm kompiliert — SML-Code und Hauptcode teilen sich dieselben Globalen und Funktionen
SPI-Bus¶
Direkter SPI-Bus-Zugriff fuer Sensoren und Displays. Unterstuetzt sowohl Hardware-SPI (unter Verwendung der von Tasmota konfigurierten Pins) als auch Software-Bitbang auf beliebigen GPIO-Pins.
| Funktion | Beschreibung |
|---|---|
int spiInit(int sclk, int mosi, int miso, int speed_mhz) |
SPI-Bus initialisieren. Gibt 1=ok zurueck |
spiSetCS(int index, int pin) |
Chip-Select-Pin fuer Slot-Index (1–4) setzen |
int spiTransfer(int cs, char buf[], int len, int mode) |
Bytes uebertragen. Gibt uebertragene Bytes zurueck |
spiInit-Pin-Modi:
- sclk = -1 — Tasmota's primaeren Hardware-SPI-Bus verwenden (GPIO in Tasmota konfiguriert)
- sclk = -2 — HSPI sekundaeren Hardware-SPI-Bus verwenden (nur ESP32)
- sclk >= 0 — Bitbang-Modus mit GPIO-Pins (sclk, mosi, miso)
- Setzen Sie mosi oder miso auf -1, wenn nicht benoetigt (z.B. Nur-Lesen- oder Nur-Schreiben-Geraet)
- speed_mhz setzt die Taktfrequenz fuer Hardware-SPI (wird fuer Bitbang ignoriert)
spiTransfer-Modi:
| Modus | Beschreibung |
|-------|-------------|
| 1 | 8-Bit pro Element — jedes buf[]-Element = 1 uebertragenes Byte |
| 2 | 16-Bit pro Element — jedes buf[]-Element = 2 Bytes (MSB zuerst) |
| 3 | 24-Bit pro Element — jedes buf[]-Element = 3 Bytes (MSB zuerst) |
| 4 | 8-Bit mit CS-Umschaltung pro Byte — CS geht fuer jedes Byte Low/High |
Hinweise:
- Der cs-Parameter ist ein 1-basierter CS-Slot-Index (entsprechend spiSetCS). Verwenden Sie 0 fuer keine automatische CS-Verwaltung
- Die Uebertragung ist Vollduplex: buf[] wird geschrieben (MOSI) und gelesene Werte (MISO) ersetzen jedes Element
- Die maximale praktische Uebertragungslaenge ist durch Ihre char-Array-Groesse begrenzt
- SPI-Ressourcen werden automatisch bereinigt, wenn die VM stoppt
- Hardware-SPI erfordert in Tasmota konfigurierte SPI-Pins (Template- oder Modul-Einstellungen)
Beispiel — MAX31855-Thermoelement lesen (SPI, 32-Bit-Lesung):
#define CS_PIN 5
int main() {
spiInit(-1, -1, -1, 4); // HW-SPI bei 4 MHz
spiSetCS(1, CS_PIN); // CS-Slot 1 = Pin 5
char buf[4];
buf[0] = 0; buf[1] = 0; buf[2] = 0; buf[3] = 0;
spiTransfer(1, buf, 4, 1); // 4 Bytes lesen
// MAX31855: Bits 31..18 = 14-Bit Thermoelement-Temperatur
int raw = ((buf[0] << 8) | buf[1]) >> 2;
if (raw & 0x2000) raw = raw - 16384; // Vorzeichenerweiterung
float temp = (float)raw * 0.25;
char out[64];
sprintf(out, "Thermocouple: %.2f °C\n", temp);
printString(out);
return 0;
}
Display-Zeichnung¶
Erfordert einen Tasmota-Build mit aktiviertem USE_DISPLAY und einem konfigurierten Display-Treiber. Alle Zeichenfunktionen arbeiten direkt auf dem Tasmota-Display-Renderer — viel effizienter als das Erstellen von DisplayText-Befehlszeichenketten.
Einrichtung & Steuerung¶
| Funktion | Beschreibung |
|---|---|
dspClear() |
Display loeschen, Position auf (0,0) zuruecksetzen |
dspPos(x, y) |
Aktuelle Zeichenposition setzen (Pixel) |
dspFont(f) |
Schriftart setzen (0-7), setzt Textgroesse auf 1 fuer Nicht-GFX-Schriften zurueck |
dspSize(s) |
Textgroessen-Multiplikator setzen |
dspColor(fg, bg) |
Vordergrund- und Hintergrundfarbe setzen (16-Bit RGB565) |
dspPad(n) |
Text-Auffuellung fuer dspDraw() setzen: positiv = linksbuendig aufgefuellt auf n Zeichen, negativ = rechtsbuendig aufgefuellt auf n Zeichen, 0 = aus |
dspDim(val) |
Display-Helligkeit setzen (0-15) |
dspOnOff(on) |
Display ein- (1) oder ausschalten (0) |
dspUpdate() |
Display-Aktualisierung erzwingen (erforderlich fuer E-Paper-Displays) |
dspWidth() |
Gibt Display-Breite in Pixeln zurueck |
dspHeight() |
Gibt Display-Hoehe in Pixeln zurueck |
Zeichenprimitiven¶
Alle Primitiven verwenden die aktuelle Position, die durch dspPos() gesetzt wurde, und die aktuelle Vordergrundfarbe, die durch dspColor() gesetzt wurde.
| Funktion | Beschreibung |
|---|---|
dspDraw(buf) |
Textzeichenkette an aktueller Position zeichnen |
dspPixel(x, y) |
Einzelnen Pixel an (x,y) zeichnen |
dspLine(x1, y1) |
Linie von aktueller Position zu (x1,y1) zeichnen, aktualisiert Position |
dspHLine(w) |
Horizontale Linie von aktueller Position, Breite w, aktualisiert Position |
dspVLine(h) |
Vertikale Linie von aktueller Position, Hoehe h, aktualisiert Position |
dspRect(w, h) |
Rechteckumriss an aktueller Position zeichnen |
dspFillRect(w, h) |
Gefuelltes Rechteck an aktueller Position zeichnen |
dspCircle(r) |
Kreisumriss an aktueller Position mit Radius r zeichnen |
dspFillCircle(r) |
Gefuellten Kreis an aktueller Position zeichnen |
dspRoundRect(w, h, r) |
Abgerundetes Rechteck an aktueller Position mit Eckenradius r |
dspFillRoundRect(w, h, r) |
Gefuelltes abgerundetes Rechteck |
dspTriangle(x1, y1, x2, y2) |
Dreieck von aktueller Position zu (x1,y1) und (x2,y2) |
dspFillTriangle(x1, y1, x2, y2) |
Gefuelltes Dreieck |
Bild & Rohbefehle¶
| Funktion | Beschreibung |
|---|---|
dspPicture("file.jpg", scale) |
Bilddatei vom Dateisystem an aktueller Position zeichnen (scale: 0=Original) |
int dspLoadImage("file.jpg") |
JPG in PSRAM als RGB565-Pixelspeicher laden, gibt Slot 0-3 zurueck (-1 bei Fehler). Bleibt im Speicher bis VM stoppt. Nur ESP32+JPEG_PICTS |
dspPushImageRect(slot, sx, sy, dx, dy, w, h) |
Teilrechteck aus geladenem Bild auf Bildschirm zeichnen. Liest aus Bild bei (sx,sy), schreibt auf Bildschirm bei (dx,dy), Groesse w×h. Fuer Hintergrund-Wiederherstellung (z.B. Uhrzeiger ueber Zifferblatt) |
int dspImageWidth(slot) |
Breite des geladenen Bildes im Slot abfragen (0 bei ungueltigem Slot) |
int dspImageHeight(slot) |
Hoehe des geladenen Bildes im Slot abfragen (0 bei ungueltigem Slot) |
int dspTextWidth(len) |
Pixelbreite fuer len Zeichen im aktuellen Font und Textgroesse. Fuer transparenten Text auf Bildhintergrund: Text messen, zeichnen, spaeter Hintergrund mit dspPushImageRect wiederherstellen |
int dspTextHeight() |
Pixelhoehe fuer aktuellen Font und Textgroesse |
dspImgText(slot, x, y, color, fieldWidth, align, text) |
Text auf ein Bild-Teilrechteck im RAM zusammensetzen und das Ergebnis in einer einzigen SPI-Transaktion uebertragen (flimmerfrei). Der Bildpuffer liefert die Hintergrundpixel; nur Vordergrund-Fontpixel werden ueberschrieben. slot: Bild-Slot von dspLoadImage(). x, y: Pixelposition auf dem Bild (und Bildschirm). color: RGB565-Textfarbe. fieldWidth: Gesamtfeldbreite in Zeichen — wenn groesser als Textlaenge, zeigt der Rest den Bildhintergrund; 0 fuer automatisch (passt genau zum Text). align: 0=links, 1=rechts, 2=zentriert (Ausrichtung innerhalb des Feldes). text: der darzustellende String. Funktioniert mit EPD-Fonts 1-4 (gesetzt via dspText("[f1]")..dspText("[f4]")) bei jeder Textgroesse. Beispiel: dspText("[f2s1]"); dspImgText(img, 10, 10, 0, 28, 0, buf); |
int dspLoadImageFromCam(cam_slot) |
JPEG aus einem PSRAM Cam-Slot (1-4, gefuellt ueber camControl(10, ...)) in einen freien RGB565 Bild-Slot (0-3) dekodieren. Gibt den neuen Bild-Slot zurueck, -1 bei Fehler. Der Quell-Cam-Slot bleibt unveraendert. Nur ESP32+JPEG_PICTS+Kamera |
dspImgTextBurn(slot, x, y, color, fieldWidth, align, text) |
Glyphen-Pixel direkt in den Bildpuffer schreiben — im Gegensatz zu dspImgText wird das Display NICHT angefasst. Fuer kopflose Cam-Boards (kein TFT angeschlossen), um Zeitstempel/Label in ein Frame zu brennen, bevor es neu kodiert wird. Faellt auf Font12 Groesse 1 zurueck, wenn kein Renderer aktiv ist; bei vorhandenem Display werden aktueller Font und Groesse respektiert (dspText("[f2s1]") etc.). Parameter identisch zu dspImgText |
int dspImageToCam(img_slot, cam_slot, quality) |
RGB565-Bild-Slot zurueck in einen Cam-Slot als JPEG kodieren (ueber esp32-camera fmt2jpg). quality 1..63 (esp_camera-Konvention, niedriger=besser; 12 ≈ JPEG Q=85). Gibt kodierte Byte-Groesse zurueck, -1 bei Fehler. Das Ergebnis ist sofort einsatzbereit fuer camControl(11, cam_slot, fh) (Datei speichern), Mail-Anhang oder jeden anderen Cam-Slot-Konsument |
dspText(buf) |
Rohen DisplayText-Befehl ausfuehren (z.B. "[z][x50][y20]Hello") |
Vordefinierte Farbkonstanten (RGB565)¶
Die folgenden Farbkonstanten sind vordefiniert — kein #define noetig:
| Konstante | Wert | Konstante | Wert |
|---|---|---|---|
BLACK |
0 | WHITE |
65535 |
RED |
63488 | GREEN |
2016 |
BLUE |
31 | YELLOW |
65504 |
CYAN |
2047 | MAGENTA |
63519 |
ORANGE |
64800 | PURPLE |
30735 |
GREY |
33808 | DARKGREY |
21130 |
LIGHTGREY |
50712 | DARKGREEN |
992 |
NAVY |
16 | MAROON |
32768 |
OLIVE |
33792 |
Benutzerdefinierte #define-Ueberschreibungen haben Vorrang vor vordefinierten Farben.
Beispiel¶
int counter;
char buf[32];
void EverySecond() {
counter++;
dspClear();
dspColor(WHITE, BLACK); // Weiss auf Schwarz
// Titel
dspFont(2);
dspSize(2);
dspPos(10, 10);
dspDraw("TinyC Display");
// Zaehler
dspFont(1);
dspSize(1);
sprintf(buf, "Count: %d", counter);
dspPos(10, 60);
dspDraw(buf);
// Roten Rahmen um den Zaehler zeichnen
dspColor(RED, BLACK);
dspPos(5, 55);
dspRect(150, 25);
// Blauen gefuellten Kreis zeichnen
dspColor(BLUE, BLACK);
dspPos(200, 80);
dspFillCircle(20);
dspUpdate(); // noetig fuer E-Paper
}
int main() {
counter = 0;
dspClear();
return 0;
}
Touch-Buttons & Slider¶
GFX-Touch-Buttons und Slider auf dem Display erstellen. Diese Komfortfunktionen formatieren intern [b...] und [bs...] DisplayText-Befehle.
Button-Erstellung¶
| Funktion | Beschreibung |
|---|---|
dspButton(num, x, y, w, h, oc, fc, tc, ts, "text") |
Power-Button erstellen (steuert Relais num) |
dspTButton(num, x, y, w, h, oc, fc, tc, ts, "text") |
Virtuellen Toggle-Button erstellen (MQTT TBT) |
dspPButton(num, x, y, w, h, oc, fc, tc, ts, "text") |
Virtuellen Push-Button erstellen (MQTT PBT) |
dspSlider(num, x, y, w, h, nelem, bg, fc, bc) |
Slider erstellen |
Parameter: num = Button-Index (0-15), x,y = Position, w,h = Groesse, oc = Umrissfarbe, fc = Fuellfarbe, tc = Textfarbe, ts = Textgroesse, nelem = Slider-Segmente, bg = Hintergrundfarbe, bc = Balkenfarbe.
Zustand setzen & lesen¶
| Funktion | Beschreibung |
|---|---|
dspButtonState(num, val) |
Button-Zustand setzen (0/1) oder Slider-Wert (0-100) |
int touchButton(num) |
Button-Zustand lesen: 0/1 fuer Buttons, -1 wenn undefiniert |
dspButtonDel(num) |
Button/Slider num loeschen, oder alle wenn num = -1 |
Touch-Callback¶
Der TouchButton-Callback wird bei Touch-Ereignissen mit Button-Index und Wert aufgerufen:
void TouchButton(int btn, int val) {
if (btn == 0) {
// Toggle-Button gedrueckt, val = 0 oder 1
char buf[16];
sprintf(buf, "%d", val);
tasmCmd("Power1", buf);
}
if (btn == 1) {
// Slider bewegt, val = 0-100
char buf[16];
sprintf(buf, "%d", val);
tasmCmd("Dimmer", buf);
}
}
int main() {
dspTButton(0, 10, 10, 100, 50, WHITE, BLUE, WHITE, 2, "Light");
dspSlider(1, 10, 80, 200, 40, 10, DARKGREY, WHITE, CYAN);
return 0;
}
TinyUI — Retained-Mode-Widget-Schicht¶
Eine duenne Retained-Mode-UI-Schicht auf den primitiven dsp*-Aufrufen. Bietet:
- Bildschirme — bis zu 256 logische Screens; Umschalten loescht die Zeichenflaeche, entfernt interaktive Widgets und zeichnet passive Widgets des neuen Screens neu.
- Theme — eine einzige globale Farb-/Padding-Palette, die auf alle Widgets angewendet wird.
- Passive Widgets (
uiLabel,uiProgress,uiGauge) — in einem separaten Pool (tc_ui_widgets[TC_UI_MAX_WIDGETS], 16 Eintraege per Default). Sie ueberdauernuiScreen()-Wechsel und werden automatisch neu gezeichnet. - Interaktive Widgets (
uiCheckbox,uiIcon) — auf dem bestehenden VButton-Pool (MAX_TOUCH_BUTTONSEintraege). Ereignisse laufen ueber den normalenTouchButton(num, state)-Callback. Eigener Indexraum, getrennt von passiven Widgets.
TinyUI ist wirklich klein: ~400 Zeilen C, kein zusaetzlicher RAM im unbenutzten Zustand, keine zusaetzlichen Abhaengigkeiten, und verwendet den bestehenden Display-Renderer. Im Vergleich zu LVGL (~150–500 KB Flash, 10–30 KB RAM).
API¶
| Funktion | Beschreibung |
|---|---|
uiScreen(int id) |
Zu Screen id (0..255) wechseln. Fuellt Canvas mit theme.bg, loescht alle VButtons, zeichnet passive Widgets des neuen Screens neu. Danach eigene build_screenN() aufrufen, um interaktive Widgets wiederherzustellen. |
uiTheme(bg, accent, text, border) |
Globale Palette setzen (RGB565). Wird von danach erstellten Widgets verwendet. |
uiClearScreen() |
Canvas mit theme.bg fuellen. |
uiLabel(num, x, y, w, h, "text", align) |
Passives Textlabel im Widget-Pool-Slot num (0..15). align: -1=rechts, 0=zentriert, 1=links. |
uiLabelSet(num, "text") oder uiLabelSet(num, buf) |
Label-Text aktualisieren und neu zeichnen. Akzeptiert Stringliteral oder char[]-Puffer. |
uiProgress(num, x, y, w, h, value, max) |
Horizontaler Fortschrittsbalken. Bereich 0..max. |
uiProgressSet(num, value) |
Balkenwert aktualisieren + neu zeichnen. |
uiGauge(num, x, y, r, value, vmin, vmax) |
240°-Skala zentriert bei x,y, Radius r. Erneuter Aufruf mit gleichem num bewegt die Nadel. |
uiCheckbox(num, x, y, w, h, "label") |
Interaktive rastende Toggle-Checkbox ueber VButton-Slot num, Trefferflaeche vom Aufrufer bestimmt (w × h, Minimum 8×8). Ein TouchButton(num, state) pro Tap, state = neuer rastender Wert (0/1). |
uiButton(num, x, y, w, h, "label") |
Tastender Momentantaster im VButton-Slot num (gleiche Trefferflaechen-Regel wie uiCheckbox). Feuert TouchButton(num, 1) beim Druecken und TouchButton(num, 0) beim Loslassen — fuer Ausloeseaktionen (Puls, Klingel, Weiter). |
uiIcon(num, x, y, img_slot) |
(reserviert) bildbasiertes Icon — Verdrahtung zum Image-Slot-System folgt. |
Passive Widgets (Label/Progress/Gauge) verwenden einen Indexraum (0..15). Checkboxes / Pushbuttons / Icons teilen sich den VButton-Indexraum (0..MAX_TOUCH_BUTTONS-1). Die beiden Indexraeume kollidieren nicht miteinander.
Beispiel¶
int current = 1;
float power = 0;
void build_screen1() {
uiLabel(0, 0, 0, 320, 30, "Dashboard", 0);
uiLabel(1, 10, 50, 150, 20, "Power: 0 W", 1);
uiProgress(3, 10, 80, 300, 18, 0, 1000);
}
void main() {
uiTheme(0x0000, 0x07FF, 0xFFFF, 0x39E7); // bg, accent, text, border
uiScreen(1);
build_screen1();
}
void EverySecond() {
power = power + 50; if (power > 1000) power = 0;
char buf[32];
sprintfFloat(buf, "Power: %.0f W", power);
uiLabelSet(1, buf);
uiProgressSet(3, power);
}
Siehe examples/tinyui_demo.tc fuer ein 3-Screen-Demo mit Live-Werten, Arc-Gauge und interaktiven Checkboxes.
Compile-time-Limits¶
| Konstante | Default | Zweck |
|---|---|---|
TC_UI_MAX_WIDGETS |
16 | Passiver Widget-Pool (tc_ui_widgets[]) |
MAX_TOUCH_BUTTONS |
16 | Interaktiver VButton-Pool (geteilt mit dspButton/dspTButton/…) |
Audio¶
| Funktion | Beschreibung |
|---|---|
audioVol(int vol) |
Audiolautstaerke setzen (0-100) |
audioPlay("file.mp3") |
MP3-Datei vom Dateisystem abspielen |
audioSay("hello") |
Text-zu-Sprache-Ausgabe |
Erfordert einen auf dem Geraet konfigurierten I2S-Audiotreiber.
audioVol(50); // Lautstaerke auf 50% setzen
audioPlay("/alarm.mp3"); // MP3-Datei abspielen
audioSay("sensor alert"); // Text sprechen
Persistente Variablen¶
| Funktion | Beschreibung |
|---|---|
saveVars() |
Alle persist Globals in die .pvs-Datei des Programms speichern |
Persist-Variablen werden automatisch beim Programmstart geladen und beim TinyCStop gespeichert. Verwenden Sie saveVars() um an kritischen Stellen zu speichern (z.B. nach Mitternachts-Zaehleraktualisierung).
Watch-Variablen (Aenderungserkennung)¶
| Funktion | Beschreibung |
|---|---|
changed(var) |
Gibt 1 zurueck wenn Watch-Variable vom Schattenwert abweicht |
delta(var) |
Gibt aktuell - Schatten zurueck (int oder float je nach Variablentyp) |
written(var) |
Gibt 1 zurueck wenn Variable seit letztem snapshot() zugewiesen wurde |
snapshot(var) |
Schattenwert aktualisieren und Written-Flag loeschen |
Watch-Variablen sind Compiler-Intrinsics — sie erzeugen Inline-Vergleichscode ohne Laufzeit-Overhead (kein Syscall).
Deep Sleep (ESP32)¶
| Funktion | Beschreibung |
|---|---|
deepSleep(int sekunden) |
Tiefschlaf mit Timer-Aufwachen nach sekunden |
deepSleepGpio(int sekunden, int pin, int level) |
Tiefschlaf mit Timer + GPIO-Aufwachen (0=low, 1=high) |
int wakeupCause() |
Gibt ESP32-Aufwachgrund zurueck (0=Reset, 2=EXT0, 3=EXT1, 4=Timer, ...) |
Persistente Variablen und Einstellungen werden vor dem Tiefschlaf automatisch gespeichert.
// Alle 5 Minuten aufwachen zum Sensor-Ablesen
int cause = wakeupCause();
if (cause == 4) {
// Timer-Aufwachen — Sensor lesen, Daten senden
}
deepSleep(300); // 300 Sekunden schlafen
// Schlafen bis GPIO12 HIGH wird (oder max 1 Stunde)
deepSleepGpio(3600, 12, 1);
Hardware-Register¶
Direkt auf Peripherie-Register des ESP32 zugreifen (fuer Low-Level-Treiber und Debugging).
| Funktion | Beschreibung |
|---|---|
int peekReg(int addr) |
32-Bit-Wert aus Peripherie-Register lesen |
pokeReg(int addr, int val) |
32-Bit-Wert in Peripherie-Register schreiben |
Email (ESP32 — benoetigt USE_SENDMAIL)¶
| Funktion | Beschreibung |
|---|---|
mailBody(body) |
E-Mail-Text setzen (HTML). body ist ein char[]-Array |
mailAttach("/pfad") |
Dateianhang vom Dateisystem hinzufuegen (String-Literal, bis zu 8) |
int mailSend(params) |
E-Mail senden. params ist char[] mit [server:port:user:passwd:von:an:betreff]. Gibt 0=ok zurueck |
Fuer einfache E-Mails ohne Anhaenge den Text nach ] in params setzen:
char cmd[200];
strcpy(cmd, "[smtp.gmail.com:465:user:pass:von@x.com:an@y.com:Alarm] Sensor ausgeloest!");
int result = mailSend(cmd);
Fuer E-Mails mit Dateianhaengen mailBody() und mailAttach() vor mailSend() verwenden:
// Text aufbauen
char body[200];
sprintf(body, "<h1>Tagesbericht</h1><p>Temperatur: %d C</p>", "%.1f");
// Text und Anhaenge registrieren
mailBody(body);
mailAttach("/daten.csv");
mailAttach("/log.txt");
// Senden — params nur [server:port:user:passwd:von:an:betreff]
char params[200];
strcpy(params, "[*:*:*:*:*:an@example.com:Tagesbericht]");
int result = mailSend(params);
// result: 0=ok, 1=Parse-Fehler, 4=Speicherfehler
* fuer Server/Port/User/Passwort/Von-Felder verwenden um #define-Standardwerte aus user_config_override.h zu nutzen.
Tesla Powerwall (ESP32 — benoetigt TESLA_POWERWALL)¶
Zugriff auf die lokale Tesla Powerwall API ueber HTTPS. Verwendet die SSL-Implementierung der E-Mail-Bibliothek (Standard-Arduino-SSL funktioniert nicht mit Powerwall).
Benoetigt: #define TESLA_POWERWALL in user_config_override.h und die ESP-Mail-Client-Bibliothek.
| Funktion | Beschreibung |
|---|---|
int pwlRequest(url) |
Konfigurationsbefehl oder API-Anfrage. Gibt 0=ok, -1=Fehler zurueck |
pwlBind(&var, pfad) |
Globale Float-Variable fuer Auto-Fill registrieren. Pfad mit #-Trenner (max 24 Bindings) |
float pwlGet(pfad) |
Float-Wert aus letzter Antwort. Unterstuetzt [N]-Suffix fuer N-tes Vorkommen |
int pwlStr(pfad, buf) |
String aus letzter Antwort in char[]-Puffer extrahieren. Gibt Laenge zurueck |
Empfohlener Ansatz — pwlBind (einmal parsen, alle fuellen):
Globale Variablen mit JSON-Pfaden in Setup() registrieren. Wenn pwlRequest() eine Antwort erhaelt, wird JSON einmal geparst und alle passenden gebundenen Variablen direkt gefuellt. Keine String-Ersetzungen, kein wiederholtes Parsen.
float sip, sop, bip, hip, pwl, rper;
void Setup() {
pwlRequest("@D192.168.188.60,email@example.com,meinpasswort");
pwlRequest("@C0x000004714B006CCD,0x000004714B007969");
// Bindings registrieren — originale JSON-Schluesselnamen verwenden
pwlBind(&sip, "site#instant_power");
pwlBind(&sop, "solar#instant_power");
pwlBind(&bip, "battery#instant_power");
pwlBind(&hip, "load#instant_power");
pwlBind(&pwl, "percentage");
pwlBind(&rper, "backup_reserve_percent");
}
void Loop() {
// Alle passenden Bindings werden automatisch gefuellt:
pwlRequest("/api/meters/aggregates");
// sip, sop, bip, hip sind jetzt gesetzt
pwlRequest("/api/system_status/soe");
// pwl ist jetzt gesetzt
pwlRequest("/api/operation");
// rper ist jetzt gesetzt
}
Konfigurations-Praefixe:
| Praefix | Beschreibung |
|---------|-------------|
| @Dip,email,passwort | IP und Zugangsdaten konfigurieren |
| @Ccts1,cts2 | CTS-Seriennummern konfigurieren (werden in Antworten maskiert) |
| @N | Auth-Cookie loeschen (erneute Authentifizierung erzwingen) |
Haeufige API-Endpunkte:
| Endpunkt | Daten |
|----------|-------|
| /api/meters/aggregates | Netz-, Batterie-, Haus-, Solarleistung (W) |
| /api/system_status/soe | Ladestand / Batterieprozent |
| /api/system_status | Systemstatus-Informationen |
| /api/operation | Betriebsmodus, Reserveprozent |
| /api/meters/readings | Detaillierte Zaehlerablesung pro CTS |
N-tes Vorkommen: pwlGet("key[N]") extrahiert das N-te Vorkommen eines wiederholten Schluessels aus der JSON-Antwort. Nuetzlich fuer /api/meters/readings mit mehreren CTS-Objekten mit gleichen Schluesselnamen:
// Netzphasen — CTS2 Grid-Phasen sind Vorkommen 6,7,8 von "p_W"
phs1 = pwlGet("p_W[6]");
phs2 = pwlGet("p_W[7]");
phs3 = pwlGet("p_W[8]");
Ad-hoc-Zugriff: pwlGet() und pwlStr() stehen fuer einmalige Wertextraktion zur Verfuegung, aber pwlBind() wird fuer wiederholtes Polling bevorzugt, da es erneutes Parsen vermeidet.
Adressierbare LED-Streifen (WS2812 — benoetigt USE_WS2812)¶
WS2812 / NeoPixel adressierbare LED-Streifen direkt aus TinyC steuern.
Benoetigt: #define USE_WS2812 in user_config_override.h.
| Funktion | Beschreibung |
|---|---|
setPixels(array, len, offset) |
Setzt len Pixel aus array, ab Strip-Position offset & 0x7FF. Aktualisiert den Strip sofort. |
Farbformat: Jedes Array-Element ist 0xRRGGBB (24-Bit RGB als Integer gepackt).
RGBW-Modus: Bit 12 von offset setzen (offset | 0x1000) fuer RGBW-Modus. Im RGBW-Modus kodieren zwei aufeinanderfolgende Array-Elemente ein Pixel (High-Word = 0x00RG, Low-Word = 0xBW00).
Beispiel — Regenbogen-Effekt:
int leds[60];
void setup() {
for (int i = 0; i < 60; i++) {
int hue = (i * 256) / 60;
leds[i] = hueToRGB(hue);
}
setPixels(leds, 60, 0);
}
int hueToRGB(int h) {
int r, g, b;
int region = h / 43;
int remainder = (h - region * 43) * 6;
switch (region) {
case 0: r = 255; g = remainder; b = 0; break;
case 1: r = 255 - remainder; g = 255; b = 0; break;
case 2: r = 0; g = 255; b = remainder; break;
case 3: r = 0; g = 255 - remainder; b = 255; break;
case 4: r = remainder; g = 0; b = 255; break;
default: r = 255; g = 0; b = 255 - remainder; break;
}
return (r << 16) | (g << 8) | b;
}
ESP Kamera (ESP32)¶
Kamera-Unterstuetzung fuer ESP32-Boards mit OV2640/OV3660/OV5640-Sensoren. Zwei Modi verfuegbar:
- Tasmota Webcam-Treiber (sel 0-7): Verwendet den Standard
USE_WEBCAMTreiber.USE_WEBCAMinuser_config_override.hdefinieren. - TinyC integrierte Kamera (sel 8-18): Direkter esp_camera-Treiber mit boardspezifischen Pins, MJPEG-Streaming auf Port 81 und PSRAM-Slot-Verwaltung.
USE_TINYC_CAMERAdefinieren (via-DTINYC_CAMERABuild-Flag). KeineUSE_WEBCAMAbhaengigkeit.
Beide Modi unterstuetzen mailAttachPic() fuer E-Mail-Bildanhaenge (bis zu 4 Bilder pro E-Mail).
Kamera-Init mit eigenen Pins (TinyC integrierter Modus)¶
// Pin-Reihenfolge: pwdn, reset, xclk, sda, scl, d7..d0, vsync, href, pclk
int campins[] = {-1, -1, 15, 4, 5, 16, 17, 18, 12, 10, 8, 9, 11, 6, 7, 13};
int ok = cameraInit(campins, PIXFORMAT_JPEG, FRAMESIZE_VGA, 12, 0, 0, -1);
| Funktion | Beschreibung |
|---|---|
cameraInit(pins[], format, framesize, quality, fb_count, grab_mode, xclk_freq) |
Kamera mit Pin-Array initialisieren. Gibt 0=ok, sonst Fehler zurueck. fb_count=0 auto, grab_mode=0 auto, xclk_freq=-1 Standard 20MHz. |
Kamerasteuerung (camControl)¶
Alle Kamera-Operationen nutzen camControl(sel, p1, p2):
Tasmota Webcam-Treiber (sel 0-7, benoetigt USE_WEBCAM):
| sel | Funktion | Beschreibung |
|---|---|---|
| 0 | camControl(0, resolution, 0) |
Init ueber Tasmota-Treiber (WcSetup) |
| 1 | camControl(1, bufnum, 0) |
Bild in Tasmota Pic-Buffer aufnehmen (1-4) |
| 2 | camControl(2, option, wert) |
Optionen setzen (WcSetOptions) |
| 3 | camControl(3, 0, 0) |
Breite abfragen |
| 4 | camControl(4, 0, 0) |
Hoehe abfragen |
| 5 | camControl(5, on_off, 0) |
Tasmota Stream-Server starten/stoppen |
| 6 | camControl(6, param, 0) |
Bewegungserkennung (-1=Bewegung lesen, -2=Helligkeit lesen, ms=Intervall) |
TinyC integrierte Kamera (sel 7-18, benoetigt USE_WEBCAM oder USE_TINYC_CAMERA):
| sel | Funktion | Beschreibung |
|---|---|---|
| 7 | camControl(7, bufnum, dateiHandle) |
Bild-Buffer in Datei speichern, gibt Bytes zurueck |
| 8 | camControl(8, 0, 0) |
Sensor-PID abfragen (z.B. 0x2642 = OV2640, 0x3660 = OV3660) |
| 9 | camControl(9, param, wert) |
Sensor-Parameter setzen (siehe Tabelle) |
| 10 | camControl(10, slot, 0) |
Bild in PSRAM-Slot aufnehmen (1-4), gibt JPEG-Groesse zurueck |
| 11 | camControl(11, slot, dateiHandle) |
PSRAM-Slot in Datei speichern, gibt Bytes zurueck |
| 12 | camControl(12, slot, 0) |
PSRAM-Slot freigeben (0 = alle Slots) |
| 13 | camControl(13, 0, 0) |
Kamera deinitialisieren + alle Slots + Stream stoppen |
| 14 | camControl(14, slot, 0) |
Slot-Groesse in Bytes abfragen (0 wenn leer) |
| 15 | camControl(15, on_off, 0) |
MJPEG Stream-Server auf Port 81 starten/stoppen |
| 16 | camControl(16, intervall_ms, schwelle) |
Bewegungserkennung aktivieren (0=deaktivieren) |
| 17 | camControl(17, sel, 0) |
Bewegungswert: 0=Trigger, 1=Helligkeit, 2=ausgeloest, 3=Intervall |
| 18 | camControl(18, 0, 0) |
Bewegungs-Referenzbuffer freigeben |
| 19 | camControl(19, addr, mask) |
Rohes Sensorregister lesen |
| 20 | camControl(20, addr, val) |
Rohes Sensorregister schreiben |
Aufnahme (sel 10) kopiert das JPEG vom Kamera-Framebuffer in einen PSRAM-Slot und gibt den Framebuffer sofort zurueck, was schnelle aufeinanderfolgende Aufnahmen ermoeglicht. Bis zu 4 Slots koennen gleichzeitig Bilder halten.
Wichtig: Kamera-Aufnahme (camControl(10, ...)) muss in TaskLoop() (VM-Task-Thread) laufen. Aufruf aus EverySecond() (Haupt-Thread) friert das Geraet ein.
Stream-Server (sel 15): Startet einen MJPEG-Server auf Port 81 mit /stream, /cam.mjpeg und /cam.jpg Endpunkten. Wird automatisch verzoegert, wenn WiFi noch nicht bereit ist (sicher fuer Autoexec). Der Stream wird auf der Tasmota-Hauptseite eingebettet.
Sensor-Parameter (sel=9)¶
| param | Einstellung | Bereich |
|---|---|---|
| 0 | vflip | 0/1 |
| 1 | Helligkeit | -2..2 |
| 2 | Saettigung | -2..2 |
| 3 | hmirror | 0/1 |
| 4 | Kontrast | -2..2 |
| 5 | Bildgroesse | FRAMESIZE_* |
| 6 | Qualitaet | 10..63 |
| 7 | Schaerfe | -2..2 |
E-Mail Bildanhaenge¶
In PSRAM-Slots aufgenommene Bilder koennen per mailAttachPic() an E-Mails angehaengt werden. Bis zu 4 Bilder pro E-Mail:
// 2 Bilder in Slot 1 und 2 aufnehmen
camControl(10, 1, 0);
camControl(10, 2, 0);
// E-Mail mit beiden Bildern senden
mailBody("Bewegungsalarm");
mailAttachPic(1);
mailAttachPic(2);
mailSend("[*:*:*:*:*:user@example.com:Alarm]");
Aufnahme und Speichern Beispiel¶
// Bild in PSRAM-Slot 1 aufnehmen
int size = camControl(10, 1, 0);
// Slot 1 in Datei speichern
int fh = fileOpen(path, 1); // zum Schreiben oeffnen
int written = camControl(11, 1, fh);
fileClose(fh);
// MJPEG Stream auf Port 81 starten
camControl(15, 1, 0);
Zeitstempel- / Text-Overlay auf JPEG-Frames (Cam ↔ Bild Bruecke)¶
Drei Hilfs-Syscalls verbinden Cam-Slots (JPEG im PSRAM, geschrieben von camControl(10)) und Bild-Slots (RGB565 im PSRAM, vom Display-Subsystem genutzt). Gemeinsam bilden sie eine Pipeline Aufnahme → Dekodierung → Text-Overlay → Neu-Enkodierung → Speichern/Streamen auf jedem Cam-Board, mit oder ohne physisch angeschlossenem Display.
| Funktion | Beschreibung |
|---|---|
int dspLoadImageFromCam(cam_slot) |
JPEG aus Cam-Slot in einen freien RGB565-Bild-Slot dekodieren. Gibt Bild-Slot zurueck, -1 bei Fehler. Quell-Cam-Slot bleibt unveraendert |
dspImgTextBurn(slot, x, y, color, fieldw, align, text) |
Glyphen-Pixel direkt in den Bildpuffer schreiben (kein Display-Push). Parameter wie dspImgText. Funktioniert kopflos |
int dspImageToCam(img_slot, cam_slot, quality) |
Bild-Slot zurueck in einen Cam-Slot als JPEG kodieren. Gibt Bytes zurueck, -1 bei Fehler. quality 1..63 (12 ≈ Q=85) |
Build-Voraussetzungen: USE_WEBCAM oder USE_TINYC_CAMERA, zusaetzlich USE_DISPLAY (fuer die Font-Tabellen — das TFT muss nicht verdrahtet sein), auf ESP32 mit JPEG_PICTS / PSRAM.
Beispiel — Zeitstempel in VGA-Aufnahme brennen, auf Dateisystem speichern:
#define CAM_IN 1 // Cam-Slot mit der Rohaufnahme
#define CAM_OUT 2 // Cam-Slot der das gestempelte JPEG haelt
int counter;
char path[40];
char line[48];
void main() {
camControl(0, 8); // Kamera init, FRAMESIZE_VGA (8 = VGA)
}
void TaskLoop() {
if (camControl(10, CAM_IN, 0) <= 0) { return; } // JPEG aufnehmen
int img = dspLoadImageFromCam(CAM_IN); // → RGB565
if (img < 0) { return; }
sprintf(line, "%04d-%02d-%02d %02d:%02d:%02d",
tasm_year, tasm_month, tasm_day,
tasm_hour, tasm_minute, tasm_second);
dspImgTextBurn(img, 10, 10, YELLOW, 0, 0, line); // Overlay
int jlen = dspImageToCam(img, CAM_OUT, 12); // Neu-Enkodierung
if (jlen <= 0) { return; }
counter = counter + 1;
sprintf(path, "/snap_%04d.jpg", counter);
int fh = fileOpen(path, 1);
if (fh >= 0) {
camControl(11, CAM_OUT, fh); // auf FS speichern
fileClose(fh);
}
delay(60000); // eine Aufnahme pro Minute
}
Hinweise:
- Aufnahme + Dekodierung + Neu-Enkodierung muessen in TaskLoop() (VM-Task-Thread) laufen. Aufruf aus EverySecond() friert das Geraet ein (gleiche Regel wie bei reinem camControl(10)).
- Font-Auswahl folgt dem Display-Stack: dspText("[f2s1]") vor dspImgTextBurn aufrufen, um einen groesseren Font zu waehlen. Auf kopflosen Boards ohne geladenen Display-Treiber faellt es auf Font12 Groesse 1 zurueck.
- Das Ergebnis in CAM_OUT verhaelt sich genau wie eine frische Aufnahme — es kann via camControl(11) in eine Datei geschrieben, via mailAttachPic() angehaengt oder ueber den Stream-Server geroutet werden.
Siehe snap_with_timestamp.tc fuer die vollstaendige Pipeline oben.
Komplettes Kamera-Skript¶
Siehe webcam_tinyc.tc fuer ein vollstaendiges Sicherheitskamera-Beispiel mit MJPEG-Streaming, Bewegungserkennung, PIR-Alarm, E-Mail-Benachrichtigung, Zeitraffer und automatischem Aufraeumen. Siehe webcam.tc fuer die Variante mit dem Tasmota Webcam-Treiber.
HomeKit (ESP32 — benoetigt USE_HOMEKIT)¶
Apple HomeKit-Integration — Geraete direkt aus TinyC als HomeKit-Zubehoer bereitstellen. Sensoren, Lichter, Schalter und Steckdosen werden ueber Apple Home steuerbar. Alle HomeKit-gebundenen Variablen verwenden native Float-Werte — keine x10-Skalierung noetig.
Benoetigt: #define USE_HOMEKIT in user_config_override.h.
Vordefinierte HomeKit-Konstanten¶
| Konstante | Wert | HAP-Kategorie | Variablen |
|---|---|---|---|
HK_TEMPERATURE |
1 | Sensor (Temperatur) | 1: Temperatur in °C |
HK_HUMIDITY |
2 | Sensor (Feuchte) | 1: Feuchte in % |
HK_LIGHT_SENSOR |
3 | Sensor (Helligkeit) | 1: Lux-Wert |
HK_BATTERY |
4 | Sensor (Batterie) | 3: Ladezustand, Schwach-Flag, Ladestatus |
HK_CONTACT |
5 | Sensor (Kontakt) | 1: Offen/Geschlossen |
HK_SWITCH |
6 | Schalter | 1: Ein/Aus |
HK_OUTLET |
7 | Steckdose | 1: Ein/Aus |
HK_LIGHT |
8 | Licht (Farbe) | 4: Power, Hue, Saturation, Brightness |
HomeKit-Funktionen¶
| Funktion | Beschreibung |
|---|---|
hkSetCode(code) |
Kopplungscode festlegen (Format: "XXX-XX-XXX") |
hkAdd(name, typ) |
Geraet hinzufuegen — Name und Typ (z.B. HK_TEMPERATURE) |
hkVar(variable) |
Float-Variable an das aktuelle Geraet binden |
int hkReady(variable) |
Gibt 1 zurueck wenn HomeKit diese Variable geaendert hat (loescht Flag automatisch) |
int hkStart() |
Deskriptor fertigstellen und HomeKit starten. Gibt 0=ok zurueck |
int hkInit(char descriptor[]) |
HomeKit mit Raw-Deskriptor starten |
hkReset() |
Alle Kopplungsdaten loeschen (Werksreset). Nach Neustart erneut koppeln |
hkStop() |
HomeKit-Server beenden |
hkReady() — Aenderungsabfrage¶
hkReady(var) funktioniert wie udpReady() — gibt 1 zurueck wenn Apple Home diese Variable seit dem letzten Aufruf geaendert hat, und loescht das Flag automatisch. Die Firmware schreibt den Wert direkt in die globale Variable, daher ist keine manuelle Zuweisung noetig. Da global Variablen automatisch per UDP senden, ist kein expliziter Aufruf mehr noetig:
void EverySecond() {
// global Variablen senden automatisch bei Zuweisung — kein expliziter udpSend noetig
}
HomeKitWrite-Callback (Optional)¶
Wird aufgerufen wenn Apple Home einen Wert aendert. Der Wert ist bereits in der globalen Variable gespeichert bevor dieser Callback laeuft — nur fuer lokale Seiteneffekte wie Relais-Weiterleitung verwenden:
void HomeKitWrite(int dev, int var, float val) {
// dev = Geraeteindex (Reihenfolge der hkAdd-Aufrufe, ab 0)
// var = Variablenindex (Reihenfolge der hkVar-Aufrufe pro Geraet, ab 0)
// val = neuer Float-Wert von Apple Home (bereits in der Variablen gespeichert)
// Nur fuer Seiteneffekte wie tasm_power = 1 noetig
}
Builder-Pattern (hkAdd + hkVar)¶
Geraete werden schrittweise definiert. hkAdd() beginnt ein Geraet, hkVar() bindet Float-Variablen daran. Mehrere hkVar()-Aufrufe fuer Geraete mit mehreren Eigenschaften (z.B. Licht mit Farbe):
// Farbiges Licht — 4 Variablen: Power, Hue, Saturation, Brightness
float pwr, hue, sat, bri;
hkSetCode("111-22-333");
hkAdd("Lampe", HK_LIGHT);
hkVar(pwr); hkVar(hue); hkVar(sat); hkVar(bri);
// Einfacher Sensor — 1 Variable
float temp;
hkAdd("Temperatur", HK_TEMPERATURE);
hkVar(temp);
hkStart();
Vollstaendiges Beispiel — Buero mit Licht + Sensoren¶
// HomeKit-gebundene Variablen (native Float-Werte)
float mh_pwr, mh_hue, mh_sat, mh_bri; // Farblicht
float elamp; // Ecklicht ein/aus
float btemp; // Temperatur (z.B. 22.5)
float bhumi; // Feuchte (z.B. 55.0)
int last_pwr;
// Nur fuer Relais-Weiterleitung noetig — Wert ist bereits in der Variable
void HomeKitWrite(int dev, int var, float val) {
if (dev == 0 && var == 0) {
int pwr;
pwr = 0;
if (val > 0.0) { pwr = 1; }
if (pwr != last_pwr) { tasm_power = pwr; last_pwr = pwr; }
}
}
void EverySecond() {
// Sensorwerte via UDP empfangen
if (udpReady("btemp")) { btemp = udpRecv("btemp"); }
if (udpReady("bhumi")) { bhumi = udpRecv("bhumi"); }
// global Variablen senden automatisch bei Zuweisung — kein expliziter udpSend noetig
}
int main() {
mh_pwr = 0.0; mh_hue = 0.0; mh_sat = 0.0; mh_bri = 50.0;
elamp = 0.0; btemp = 22.0; bhumi = 50.0;
last_pwr = -1;
hkSetCode("111-11-111");
hkAdd("Licht", HK_LIGHT);
hkVar(mh_pwr); hkVar(mh_hue); hkVar(mh_sat); hkVar(mh_bri);
hkAdd("Ecklicht", HK_OUTLET); hkVar(elamp);
hkAdd("Temperatur", HK_TEMPERATURE); hkVar(btemp);
hkAdd("Feuchte", HK_HUMIDITY); hkVar(bhumi);
hkStart();
return 0;
}
Kopplung¶
- Firmware mit
USE_HOMEKITkompilieren und flashen - TinyC-Programm mit
hkSetCode()/hkAdd()/hkStart()kompilieren und hochladen - QR-Code unter
http://<Geraet>/hkmit iPhone scannen - Bei Konfigurationsaenderungen
hkReset()einmalig ausfuehren, dann erneut koppeln
Vordefinierte Datei-Konstanten¶
Fuer fileOpen() stehen folgende Kurzformen zur Verfuegung:
| Konstante | Wert | Beschreibung |
|---|---|---|
r |
0 | Lesen |
w |
1 | Schreiben |
a |
2 | Anhaengen |
int f = fileOpen("/daten.csv", r); // statt fileOpen("/daten.csv", 0)
f = fileOpen("/log.txt", a); // statt fileOpen("/log.txt", 2)
Plugin-Abfrage¶
| Funktion | Beschreibung |
|---|---|
int pluginQuery(char dst[], int index, int p1, int p2) |
Binäres Plugin abfragen. Gibt Ergebnis zurueck und schreibt optionale String-Antwort in dst |
Cross-VM Share-Tabelle (ESP32)¶
Treiber-globaler benannter Schluessel/Wert-Speicher mit Mutex-Schutz, ueber den zwei oder mehr TinyC-Slots Skalare und kurze Strings austauschen koennen. Sinnvoll, wenn ein Programm einen einzelnen Slot sprengt (TC_MAX_PROGRAM = 128 KB) und auf zwei Slots aufgeteilt wird, oder wenn mehrere kooperierende Programme Zustand austauschen sollen — ohne Umweg ueber MQTT oder Dateisystem.
Kapazitaet (per user_config_override.h aenderbar): TC_SHARE_MAX = 32 Eintraege · TC_SHARE_KEY_LEN = 16 Zeichen Schluessel · TC_SHARE_STR_LEN = 64 Zeichen Wert. Worst-Case-Speicher ≈ 2,6 KB DRAM. Mutex wird beim ersten Zugriff angelegt.
| Funktion | Beschreibung |
|---|---|
void shareSetInt(char key[], int v) |
Integer-Wert fuer key setzen (Eintrag wird angelegt, Typ ueberschrieben) |
void shareSetFloat(char key[], float v) |
Float-Wert fuer key setzen |
void shareSetStr(char key[], char v[]) |
String-Wert fuer key setzen (auf TC_SHARE_STR_LEN gekuerzt) |
int shareGetInt(char key[]) |
Integer lesen; 0 wenn Schluessel fehlt oder falscher Typ |
float shareGetFloat(char key[]) |
Float lesen; 0.0 wenn fehlend |
int shareGetStr(char key[], char dst[]) |
String nach dst lesen; gibt kopierte Zeichen zurueck, 0 + leerer dst wenn fehlend |
int shareHas(char key[]) |
1 wenn Schluessel existiert, sonst 0 |
int shareDelete(char key[]) |
Eintrag loeschen; 1 wenn vorhanden, 0 sonst |
Schluessel-Beschraenkung: jedes key-Argument muss ein String-Literal sein (zu einem Konstantenpool-Index zur Compilezeit aufgeloest). Variable Schluessel werden nicht unterstuetzt. Schluessel sind Gross-/Kleinschreibung-relevant.
Semantik fehlender Schluessel: Lesezugriffe loesen niemals einen Fehler aus. shareHas() unterscheidet "Schluessel fehlt" von "Schluessel existiert mit Wert 0". Erneutes shareSet* mit anderem Typ ueberschreibt den Eintrag stillschweigend.
Beispiel — Slot 0 Schreiber + Slot 1 Leser:
// Slot 0 (Schreiber)
int counter = 0;
void EverySecond() {
counter = counter + 1;
shareSetInt("counter", counter);
shareSetFloat("kwh", counter * 0.1);
char nm[32];
sprintf(nm, "tick=%d", counter);
shareSetStr("name", nm);
}
int main() { return 0; }
// Slot 1 (Leser)
void Command(char cmd[]) {
if (strcmp(cmd, "ALL") == 0) {
int c = shareGetInt("counter");
float f = shareGetFloat("kwh");
char n[32];
shareGetStr("name", n);
char r[160];
sprintf(r, "counter=%d kwh=%.1f name=%s", c, f, n);
responseCmnd(r);
} else {
responseCmnd("RDR: ALL");
}
}
int main() { addCommand("RDR"); return 0; }
Debug¶
| Funktion | Beschreibung |
|---|---|
dumpVM() |
VM-Zustand auf Konsole ausgeben |
Multi-VM Slots (ESP32)¶
Auf dem ESP32 koennen bis zu 6 unabhaengige TinyC-Programme gleichzeitig in separaten VM-Slots laufen. Jeder Slot hat eigenen Bytecode, Globals, Stack, Heap und Ausgabepuffer. Speicher wird dynamisch allokiert — leere Slots kosten null Bytes, nicht-autoexec Slots verwenden Lazy Loading (nur ~33 Bytes bis zum ersten Start). Der ESP8266 unterstuetzt nur 1 Slot.
Slot-Konfiguration¶
Slot-Zuweisungen und Autoexec-Flags werden in /tinyc.cfg auf dem Dateisystem gespeichert. Diese Datei wird automatisch erstellt und aktualisiert, wenn ein Programm geladen, hochgeladen oder das Autoexec-Flag umgeschaltet wird. Eine manuelle Bearbeitung ist nicht notwendig.
Beispiel /tinyc.cfg:
Jede Zeile entspricht einem Slot (0–3): Dateiname,Autoexec-Flag. Die letzte Zeile _info,<0|1> steuert, ob Debug-Statuszeilen auf der Tasmota-Hauptseite angezeigt werden.
Tasmota-Befehle¶
Alle Befehle verwenden standardmaessig Slot 0, wenn keine Slot-Nummer angegeben wird (abwaertskompatibel).
| Befehl | Beschreibung |
|---|---|
TinyC |
Status aller Slots anzeigen (JSON) |
TinyCRun [slot] [/datei.tcb] |
Slot starten (optional Datei vorher laden) |
TinyCStop [slot] |
Slot stoppen |
TinyCReset [slot] |
Slot stoppen und zuruecksetzen |
TinyCExec <n> |
Instruktionen pro Tick setzen (Standard 1000) |
TinyCInfo 0\|1 |
VM-Debug-Zeilen auf Hauptseite ein-/ausblenden |
TinyC ?<abfrage> |
Globale Variablen per Index abfragen (siehe unten) |
Beispiele:
TinyCRun → Slot 0 starten
TinyCRun /weather.tcb → Datei in Slot 0 laden und starten
TinyCRun 2 /logger.tcb → Datei in Slot 2 laden und starten
TinyCStop 1 → Slot 1 stoppen
TinyCReset 3 → Slot 3 zuruecksetzen
TinyCInfo 1 → Debug-Info auf Hauptseite anzeigen
Web-Konsole (/tc)¶
Die TinyC-Konsolenseite unter /tc zeigt eine kompakte Uebersicht aller Slots:
- Statusanzeige: gruener Punkt = aktiv (laeuft oder Callback-bereit), orange = geladen aber nicht gestartet, grau = leer
- Run / Stop Buttons: kontextabhaengig — Run ausgegraut wenn aktiv, Stop ausgegraut wenn inaktiv
- A-Button: schaltet Auto-Ausfuehrung beim Booten um (gruen = aktiviert). Wird sofort in
/tinyc.cfggespeichert - Programm laden: Dateiauswahl mit Slot-Dropdown um beliebige
.tcb-Datei in beliebigen Slot zu laden - Repository: wenn
/tinyc_repo.cfgauf dem Dateisystem existiert, wird ein entferntes Programm-Repository angezeigt (siehe unten) - Programm hochladen: Datei-Upload mit Slot-Dropdown um eine
.tcb-Datei direkt hochzuladen
Programm-Repository¶
TinyC unterstuetzt das Herunterladen vorkompilierter .tcb-Programme aus einem entfernten Repository direkt auf dem Geraet.
Einrichtung:
1. Erstelle eine Datei /tinyc_repo.cfg auf dem Geraete-Dateisystem mit der Basis-URL des Repositories (eine Zeile):
index.txt-Datei enthalten, die die verfuegbaren .tcb-Dateien auflistet (ein Dateiname pro Zeile):
3. Die .tcb-Dateien muessen unter <basis_url>/<dateiname> erreichbar sein
Verwendung:
Wenn /tinyc_repo.cfg vorhanden ist, zeigt die TinyC-Konsolenseite ein zusaetzliches Repository-Feld mit:
- Einem Dropdown mit allen .tcb-Dateien aus der entfernten index.txt
- Einem Slot-Waehler
- Einem Download & Load-Button, der die ausgewaehlte Datei auf das Geraete-Dateisystem herunterlaed und in den gewaehlten Slot laed
Das Standard-Repository unter https://raw.githubusercontent.com/gemu2015/Sonoff-Tasmota/universal/tasmota/tinyc/bytecode enthaelt Beispielprogramme fuer Sensoren, Displays, Charts und mehr. Lade die mitgelieferte tinyc_repo.cfg auf dein Geraet hoch um es zu aktivieren.
API-Endpunkte¶
Die JSON-API unter /tc_api unterstuetzt einen slot-Parameter:
GET /tc_api?cmd=run&slot=2 → Slot 2 starten
GET /tc_api?cmd=stop&slot=1 → Slot 1 stoppen
GET /tc_api?cmd=status → Status aller Slots
POST /tc_upload?slot=3&api=1 → .tcb in Slot 3 hochladen (JSON-Antwort)
Variablen-Abfrage — _Q() Makro (Google Charts)¶
Globale TinyC-Variablen koennen per HTTP als JSON abgefragt werden, um Live-Dashboards mit Google Charts oder anderen JavaScript-Charting-Bibliotheken zu erstellen.
Das _Q() Makro wird zur Kompilierzeit in String-Literalen expandiert. Der Compiler ersetzt Variablennamen durch ihre Indizes und Typen — das Binary enthaelt keine Variablennamen, nur kompakte Index-basierte Abfragen.
Syntax: _Q(var1, var2, ...)
Der Compiler ersetzt _Q(...) durch einen index-kodierten Abfrage-String:
- <index>i — int-Skalar
- <index>f — float-Skalar
- <index>s<n> — char[n] String
- <index>I<n> — int-Array mit n Elementen
- <index>F<n> — float-Array mit n Elementen
Beispiel: Bei den Globalen float temperature; int counter; wird der String:
Antwortformat: JSON-Array in der Reihenfolge der Abfrage:
Verwendung im WebPage-Callback:
float temperature = 23.5;
int counter = 0;
void WebPage() {
webSend("<script>fetch('/cm?cmnd=TinyC+%3F_Q(temperature,counter)')");
webSend(".then(r=>r.json()).then(d=>{var v=d.TinyC;");
webSend("// v[0]=temperature, v[1]=counter");
webSend("});</script>");
}
Fuer einen bestimmten Slot wird die Slot-Nummer vorangestellt:
Boot-Ablauf¶
Beim Booten liest TinyC /tinyc.cfg und:
1. Laedt jede konfigurierte .tcb-Datei in ihren Slot
2. Startet automatisch Slots mit gesetztem Autoexec-Flag (1)
Wenn keine /tinyc.cfg existiert (erster Start), werden keine Programme geladen.
Ressourcenverbrauch¶
Jeder VM-Slot verbraucht ca. 3,2 KB RAM (nur Struct, ohne Programm-Bytecode). Slots werden dynamisch allokiert — nur aktive Slots verbrauchen Speicher. Das Slot-Pointer-Array selbst benoetigt nur 24 Bytes. Nicht-autoexec Slots verwenden Lazy Loading: nur der Dateiname (~33 Bytes) wird gespeichert bis zum ersten Start.
| Ressource | Kosten |
|---|---|
| Pointer-Array | 16 Bytes (4 Zeiger) |
| Pro-Slot Struct | ~3,2 KB |
| Programm-Bytecode | variabel (malloc) |
| Heap (alle Arrays) | max 32 KB, bei Bedarf allokiert |
Callbacks mit mehreren Slots¶
Jeder Slot erhaelt seine eigenen Callbacks unabhaengig:
EverySecond(),Every100ms(),Every50ms()— werden an alle aktiven Slots verteiltWebCall()— jeder Slot kann eigene Sensorzeilen zur Hauptseite hinzufuegenJsonCall()— jeder Slot fuegt eigene Telemetriedaten hinzuTaskLoop()— laeuft im eigenen FreeRTOS-Task des Slots (ESP32)CleanUp()— wird auf allen Slots vor Geraete-Neustart aufgerufen
Geteilte Ressourcen (UDP, SPI, Datei-Handles) sind global — nur ein Slot sollte diese gleichzeitig nutzen.
Beispiel: Zwei Programme nebeneinander¶
Slot 0 — Temperaturueberwachung:
int temp = 0;
void EverySecond() { temp = tasm_analog0; }
void WebCall() {
char buf[64];
sprintf(buf, "{s}Temperatur{m}%d{e}", temp);
webSend(buf);
}
int main() { return 0; }
Slot 1 — Betriebszeitzaehler:
int uptime = 0;
void EverySecond() { uptime++; }
void WebCall() {
char buf[64];
sprintf(buf, "{s}Betriebszeit{m}%d s{e}", uptime);
webSend(buf);
}
int main() { return 0; }
Beide zeigen ihre Sensorzeilen gleichzeitig auf der Tasmota-Hauptseite an.
VM-Grenzen¶
| Ressource | ESP8266 | ESP32 | Browser | Anmerkungen |
|---|---|---|---|---|
| Stack-Tiefe | 64 | 256 | 256 | Operandenstack-Eintraege |
| Aufrufrahmen | 8 | 32 | 32 | Maximale Rekursions-/Aufruftiefe |
| Lokale pro Rahmen | 256 | 256 | 256 | Skalare + kleine Arrays ≤16 inline |
| Globale Variablen | 64 | 256 | 256 | Skalare + kleine Arrays ≤16 inline |
| Codegroesse | 4 KB | 128 KB | 64 KB | Bytecode; ESP32 faellt bei DRAM-Mangel auf PSRAM zurueck |
| Heap-Speicher | 8 KB | 32 KB | 64 KB | Fuer Arrays >16 Elemente (autom. Allokation) |
| Heap-Handles | 8 | 32 | 32 | Max. gleichzeitige Heap-Allokationen |
| Konstantenpool | 32 | 1024 | 65536 | Zeichenketten- & Float-Konstanten (DRAM, ESP32 faellt auf PSRAM zurueck) |
| Instruktionslimit | 1M | 1M | 1M | Sicherheitslimit pro Ausfuehrung |
| GPIO-Pins | 40 | 40 | 40 | Pins 0–39 (im Browser simuliert) |
| Datei-Handles | 4 | 4 | 8 | Gleichzeitig geoeffnete Dateien |
| VM-Slots | 1 | 6 | 1 | Gleichzeitige Programme |
| Cross-VM-Share | n/a | 32 Keys | n/a | Treiber-globale Share-Tabelle (nur ESP32) |
ESP32 PSRAM-Fallback (seit v1.3.19): TC_MAX_PROGRAM von 64 KB auf 128 KB angehoben. Bytecode-Puffer (s->program) und Konstantendaten-Pool (vm->const_data) werden zuerst aus dem internen DRAM allokiert; bei OOM faellt die Allokation automatisch auf heap_caps_malloc(MALLOC_CAP_SPIRAM) zurueck. Kleine/normale Skripte bleiben im schnellen statischen RAM; nur sehr grosse Programme (100+ KB) landen im PSRAM. Ein AddLog-INFO-Eintrag wird ausgegeben, wenn der PSRAM-Pfad genutzt wird.
Geraetedateiverwaltung (IDE)¶
IDE-Installation¶
Die IDE-Datei (tinyc_ide.html.gz) kann entweder auf dem Flash-Dateisystem oder der SD-Karte liegen — je nachdem, welches als Benutzer-Dateisystem (ufsp) gemountet ist. Laden Sie tinyc_ide.html.gz ueber die Tasmota-Seite Dateisystem verwalten hoch.
Hinweis: TinyC-Skripte und Datendateien (
.tc,.tcbusw.) werden ebenfalls auf dem Benutzer-Dateisystem (ufsp) gespeichert.
Dateioperationen¶
Die IDE-Werkzeugleiste enthaelt Steuerelemente zur Verwaltung von Dateien auf dem Tasmota-Geraetedateisystem:
- Geraetedateien-Dropdown — Listet alle Dateien auf dem Geraet auf. Waehlen Sie eine Datei, um sie in den Editor zu laden. Die Liste zeigt Dateiname und Groesse (z.B.
config.tc (1.2KB)). - Datei-Speichern-Schaltflaeche — Speichert den aktuellen Editorinhalt als Datei auf dem Geraet. Fragt nach einem Dateinamen (Standard ist der aktuelle Dateiname).
- Automatische Aktualisierung — Die Dateiliste wird automatisch aktualisiert, wenn die Geraete-IP eingegeben oder geaendert wird, und nach jedem Speichervorgang.
Alle Dateioperationen verwenden den /tc_api-Endpunkt mit CORS-Unterstuetzung, sodass die IDE von jedem Browser aus verwendet werden kann — sie muss nicht vom Geraet bereitgestellt werden.
API-Endpunkte¶
| Endpunkt | Methode | Beschreibung |
|---|---|---|
/tc_api?cmd=listfiles |
GET | Gibt JSON-Liste der Dateien zurueck: {"ok":true,"files":[{"name":"x","size":123},...]} |
/tc_api?cmd=readfile&path=/name |
GET | Gibt Dateiinhalt als Klartext zurueck |
/tc_api?cmd=readfile&path=/name@von_bis |
GET | Gibt zeitgefilterte CSV-Daten zurueck (siehe unten) |
/tc_api?cmd=writefile&path=/name |
POST | Schreibt POST-Body in Datei, gibt {"ok":true,"size":N} zurueck |
/tc_api?cmd=deletefile&path=/name |
GET | Loescht eine Datei vom Dateisystem |
Zeitbereichs-gefilterter Dateizugriff¶
Haengen Sie @von_bis an den Dateipfad an, um nur Zeilen innerhalb eines Zeitbereichs aus einer CSV-Datendatei zu extrahieren. Dies ist nuetzlich fuer die Bereitstellung von IoT-Zeitreihendaten an Chart-Bibliotheken.
URL-Format:
Beispiel:
Sowohl das deutsche (TT.MM.JJ HH:MM) als auch das ISO-Format (JJJJ-MM-TTTHH:MM:SS) werden unterstuetzt. Der _ (Unterstrich) trennt die Von- und Bis-Zeitstempel.
Antwort: Die Kopfzeile (erste Zeile) wird immer eingeschlossen, gefolgt von nur den Datenzeilen, deren Zeitstempel in der ersten Spalte innerhalb [von..bis] liegt. Zeilen nach dem Endzeitstempel werden effizient uebersprungen (fruehzeitiger Abbruch).
Leistungsoptimierung: Wenn eine Indexdatei existiert (gleicher Name mit .ind-Erweiterung, enthaltend Zeitstempel\tByte-Offset-Zeilen), werden Byte-Offsets verwendet, um direkt zur Startposition zu springen. Andernfalls wird eine geschaetzte Positionssuche basierend auf dem ersten und letzten Zeitstempel der Datei durchgefuehrt (aehnlich wie Scripters opt_fext).
Port 82 Download-Server (ESP32)¶
Fuer grosse Datenbankdateien kann der zeitgefilterte Dateizugriff auf Port 80 die Haupt-Webserver-Schleife blockieren. TinyC enthaelt einen dedizierten Port 82 Download-Server, der Dateien in einem FreeRTOS-Hintergrund-Task bereitstellt und das Geraet waehrend grosser Uebertragungen reaktionsfaehig haelt.
URL-Format:
Beispiele:
Eigenschaften:
- Laeuft in einem dedizierten FreeRTOS-Task (angeheftet an Core 1, Prioritaet 3)
- Blockiert nicht die Tasmota-Hauptschleife oder Weboberflaeche
- Unterstuetzt die gleiche @von_bis Zeitbereichsfilterung wie /tc_api readfile
- Verwendet Chunked-Transfer-Encoding fuer gefilterte Antworten
- Content-Disposition-Header fuer Browser-Download
- Ein Download gleichzeitig (gibt HTTP 503 zurueck wenn beschaeftigt)
- Automatische MIME-Typ-Erkennung (.csv/.txt = text/plain, .html, .json)
- Der Port kann durch Definition von TC_DLPORT vor der Kompilierung geaendert werden (Standard: 82)
Typischer Arbeitsablauf¶
- Geraete-IP in die Werkzeugleiste eingeben
- Das Geraetedateien-Dropdown fuellt sich automatisch mit allen Dateien auf dem Geraet
- Datei auswaehlen, um sie in den Editor zu laden — oder neuen Code schreiben
- Datei speichern klicken, um den Quellcode auf dem Geraet zu speichern (z.B. als
myapp.tc) - Auf Geraet ausfuehren klicken, um zu kompilieren, die
.tcb-Binaerdatei hochzuladen und die Ausfuehrung zu starten
So koennen Sie TinyC-Quelldateien zusammen mit ihrem kompilierten Bytecode auf dem Geraet aufbewahren, was das Bearbeiten von Programmen direkt ohne lokale Dateispeicherung erleichtert.
Tastenkuerzel (IDE)¶
| Tastenkuerzel | Aktion |
|---|---|
| Ctrl + Enter | Kompilieren |
| Ctrl + Shift + Enter | Kompilieren & Ausfuehren |
| Ctrl + S | Datei speichern |
| Ctrl + O | Datei oeffnen |
| Ctrl + F | Suchen |
| Enter (in Suche) | Naechstes finden |
| Shift + Enter (in Suche) | Vorheriges finden |
| Escape | Suchleiste schliessen |
| Tab (im Editor) | 4 Leerzeichen einfuegen |
Beispiele¶
Die IDE enthaelt 19 sofort einsatzbereite Beispiele im Dropdown "Beispiel laden..." — von einfachem Blinken bis zu Wetterstationsempfaengern und interaktiven WebUI-Dashboards.
Hello World¶
LED-Blinken¶
#define LED 2
#define INPUT 0x01
#define OUTPUT 0x03
#define INPUT_PULLUP 0x05
#define INPUT_PULLDOWN 0x09
int main() {
gpioInit(LED, OUTPUT);
while (true) {
digitalWrite(LED, 1);
delay(500);
digitalWrite(LED, 0);
delay(500);
}
return 0;
}
Fibonacci¶
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
int main() {
for (int i = 0; i < 10; i++) {
print(fib(i));
}
return 0;
}
Zeichenkettenoperationen¶
int main() {
char greeting[32] = "Hello";
char name[16] = "World";
char buf[64];
// Klassischer Funktionsstil
strcpy(buf, greeting);
strcat(buf, ", ");
strcat(buf, name);
strcat(buf, "!\n");
printString(buf); // Hello, World!
// Dasselbe mit +-Operator
buf = greeting;
buf += ", ";
buf += name;
buf = buf + "!\n";
printString(buf); // Hello, World!
// Formatierte Zeichenketten
char line[64];
sprintf(line, "count = %d", 42);
printString(line); // count = 42
// Mehrere Werte mit sprintfAppend
char report[128];
sprintf(report, "Sensor %d", 1);
sprintfAppend(report, " name=%s", name);
sprintfAppend(report, " temp=%.1f", 23.5);
printString(report); // Sensor 1 name=World temp=23.5
return 0;
}
Bubble Sort¶
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int data[8] = {64, 34, 25, 12, 22, 11, 90, 1};
bubbleSort(data, 8);
for (int i = 0; i < 8; i++) {
print(data[i]);
}
return 0;
}
WebUI-Dashboard¶
int power;
int brightness;
int mode;
void WebUI() {
int page = webPage();
if (page == 0) {
webButton(power, "Power");
webSlider(brightness, 0, 100, "Brightness");
}
if (page == 1) {
webPulldown(mode, "Mode", "Off|Auto|Manual");
}
}
int main() {
webPageLabel(0, "Controls");
webPageLabel(1, "Settings");
brightness = 50;
return 0;
}
Unterschiede zu Standard-C¶
| Merkmal | Standard-C | TinyC |
|---|---|---|
| Zeiger | Volle Unterstuetzung | Nicht unterstuetzt |
| Structs | Volle Unterstuetzung | Unterstuetzt: skalare Felder, Array-Felder (char text[32]), Member-Zugriff, Initialisierungslisten, zusammengesetzte Zuweisung. Keine verschachtelten Structs, keine Unions, keine Bit-Felder |
| Enums | Volle Unterstuetzung | Unterstuetzt: benannte/anonyme Enums, negative Werte, Auto-Inkrement, innerhalb von Funktionen |
| Dynamischer Speicher | malloc/free | Auto-Heap fuer Arrays >16 Elemente (kein explizites malloc) |
| Mehrdimensionale Arrays | int a[3][4] |
Nicht unterstuetzt |
| Zeichenkettentyp | char* |
Nur char arr[N] — keine Zeigerarithmetik |
| Praeprozessor | Volles CPP | #define (Konstanten + funktionsaehnliche Makros), #ifdef/#ifndef/#if/#else/#endif/#undef (kein #include) |
| Header-Dateien | #include |
Nicht unterstuetzt |
| typedef | Volle Unterstuetzung | Unterstuetzt: primitive Aliase, benannte Struct-Aliase, anonyme Struct-typedefs, verkettete Aliase, lokale typedefs |
const |
Typgeprueft | Akzeptiert (Dokumentationshinweis, zur Laufzeit nicht erzwungen) |
static lokale Variablen |
Volle Unterstuetzung | Unterstuetzt: nullinitialisiert, bleibt zwischen Aufrufen erhalten. Nicht-null-Initialisierer werden nicht ausgefuehrt |
| sizeof | Volle Unterstuetzung | Nur zur Uebersetzungszeit: sizeof(typ) und sizeof(name) unterstuetzt; sizeof(ausdruck) nicht unterstuetzt. Siehe sizeof-Operator |
Ternaerer Operator ?: |
Volle Unterstuetzung | Unterstuetzt, auch verschachtelt |
| do-while | Volle Unterstuetzung | Unterstuetzt |
| Zusammengesetzte Zuweisungen | Volle Unterstuetzung | Unterstuetzt: += -= *= /= %= &= \|= ^= <<= >>= |
Hex-Escape \xNN |
Volle Unterstuetzung | Unterstuetzt in String- und Char-Literalen |
| goto | Volle Unterstuetzung | Nicht unterstuetzt |
| Funktionszeiger | Volle Unterstuetzung | Nicht unterstuetzt |
| Variadische Benutzerfunktionen | va_list etc. |
Nicht unterstuetzt (nur sprintf/sprintfAppend akzeptieren mehrere Argumente per Compiler-Expansion) |
| Standardbibliothek | stdio, stdlib | Nur eingebaute Funktionen (siehe Eingebaute Funktionen) |
Generiert aus TinyC-Quellcode — lexer.js, parser.js, codegen.js, opcodes.js, vm.js