Diese Seite richtet sich an Entwickler. Wie du die Signage OS Integration in Flyo einrichtest und Bildschirme verwaltest, erfährst du auf der Hauptseite der Integration.
Ein Applet ist eine kleine Webapplikation (HTML, CSS, JavaScript), die von SignageOS auf dem Digital Signage Gerät ausgeführt wird. Das Applet definiert das Layout und Verhalten der Anzeige (Formatvorlage), während Flyo die Inhalte liefert und die Geräte automatisch aktualisiert. Einmal entwickelt, kann dasselbe Applet von beliebig vielen Integrationen und Bildschirmen verwendet werden.
Beispiel-Applets
Als Startpunkt für ein eigenes Applet stehen zwei Beispiel-Repositories zur Verfügung:
- flyocloud/signageos-js-example: Minimales Applet in reinem JavaScript, das die Inhalte aus Flyo als Slideshow anzeigt.
- flyocloud/signageos-vue2-example: Beispiel-Applet auf Basis von Vue 2.
Nützliche Links von SignageOS:
So erhält das Applet seine Daten
Beim Speichern der Integration weist Flyo das gewählte Applet den ausgewählten Geräten zu. Dabei übergibt Flyo dem Applet in der Konfiguration den Parameter api mit der URL der Datenschnittstelle. Im Applet liest du diese URL über das SignageOS SDK aus und lädst die Daten mit einem einfachen fetch:
import sos from '@signageos/front-applet';
sos.onReady().then(async () => {
// Während der lokalen Entwicklung ist sos.config nicht gesetzt,
// dann kann als Fallback direkt die API-URL der Integration verwendet werden.
const api = sos.config?.api || 'https://api.flyo.cloud/integration/signageos/<id>/<token>';
const response = await fetch(api);
const { data, config, vars } = await response.json();
// config.timeout: Anzeigedauer pro Slide in Millisekunden
// data: die Inhalte (siehe Slides oder Strukturiert)
// vars: die in der Integration erfassten Variabeln (siehe Variabeln)
});Die Antwort der Datenschnittstelle hat immer diese Grundstruktur:
{
"data": "...",
"config": {
"timeout": 15000
},
"vars": {}
}config.timeout: Die in der Integration gewählte «Anzeigedauer pro Slide» in Millisekunden.
data: Die Inhalte aus den verknüpften Content Pools. Die Struktur hängt von der in der Integration gewählten Datenstruktur ab, siehe Slides oder Strukturiert. Content Pools sind freiwillig: Eine Integration, die ausschliesslich mit Variabeln arbeitet, liefert hier eine leere Liste. Das Applet sollte diesen Fall abfangen.
vars: Die in der Integration frei erfassten Werte, siehe Variabeln. Ein leeres Objekt, wenn keine Variabeln konfiguriert sind.
Das Applet muss die Daten nicht regelmässig abfragen: Ändern sich Inhalte in Flyo, aktualisiert Flyo die Datenschnittstelle und löst auf allen verbundenen Geräten automatisch einen Applet-Refresh aus. Standardmässig geschieht das mit einer verzögerten Aktualisierung von 5 Minuten.
Schlägt der Abruf dagegen fehl, etwa weil das Gerät startet, bevor das Netzwerk bereit ist, löst Flyo keinen neuen Versuch aus. Das Applet muss diesen Fall selbst abfangen, siehe Laden automatisch wiederholen.
OpenAPI-Definition
Jede konfigurierte SignageOS Integration stellt eine OpenAPI-Definition ihrer Datenschnittstelle bereit. Du findest sie in der Integration im Tab «Entwickler», zusammen mit der API URL. Die Definition beschreibt alle Felder und Daten, welche die Schnittstelle liefert, inklusive der konfigurierten Variabeln mit ihrem jeweiligen Typ, und eignet sich auch zum Generieren von Typen oder Clients.
Slides oder Strukturiert
Im Tab «Inhalte» der Integration wird unter «Verknüpfung der Inhalte» die Datenstruktur festgelegt, in der die Inhalte an das Applet geliefert werden. Diese Wahl bestimmt massgeblich, wie du das Applet entwickelst.
Slides
Jeder Eintrag im Content Pool entspricht einem einzelnen Slide. data ist ein flaches Array; die Einträge aller verknüpften Content Pools werden nacheinander in dasselbe Array gelegt. Das ist die richtige Wahl für klassische Diashows, bei denen alle Inhalte nacheinander abgespielt werden.
Pro Content Pool können die Standard-Felder Bild, Titel, Text, QR Code Link und Sortierreihenfolge verknüpft werden. Zusätzlich definierte eigene Felder landen im Objekt custom.
{
"data": [
{
"image": "https://storage.flyo.cloud/welcome_ab12cd34.jpg",
"title": "Willkommen",
"teaser": "Neue Wasserwelten",
"uid": "f8855c2b6709de28df52df51aabe6ab6",
"entity_id": 29,
"entity_type": "poi",
"pool_id": 1,
"item_identifier": "hero",
"metric": "https://flyo.cloud/integration/metric/h/c3f0d71ed0a5bd97...",
"qrcode": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5v...",
"scanurl": "https://api.flyo.cloud/integration/sosc/12/1712000000/f8855c2b...",
"custom": {
"preis": "CHF 10.00"
}
}
],
"config": {
"timeout": 15000
}
}Jeder Slide enthält folgende Felder:
| Feld | Beschreibung |
|---|---|
image | URL des verknüpften Bildes |
title | Titel (leerer String, wenn nicht verknüpft) |
teaser | Text (leerer String, wenn nicht verknüpft) |
link | Der verknüpfte QR Code Link als Roh-URL (nur wenn verknüpft) |
hiddensort | Wert der verknüpften Sortierreihenfolge; wird nicht angezeigt, sondern dient nur der Sortierung |
uid | Eindeutige ID des Eintrags |
entity_id | ID der Entität |
entity_type | Typ der Entität, z.B. poi, event, file, tag |
pool_id | ID des Content Pools, aus dem der Eintrag stammt |
item_identifier | Der Entwicklungs-Identifier des Content Pools (optional) |
metric | URL für das Messen mit Flyo Metriken; ein Aufruf erfasst eine Anzeige des Slides |
qrcode | Fertiger QR-Code als SVG Data-URI, direkt als src eines img-Elements verwendbar; false, wenn kein Link verknüpft ist |
scanurl | Die URL, auf die der QR-Code zeigt; false, wenn kein Link verknüpft ist |
custom | Objekt mit den zusätzlich definierten eigenen Feldern (Schlüssel = Feldname aus der Integration) |
Strukturiert
Die Inhalte werden anhand des Entwicklungs-Identifiers gruppiert. data ist ein Objekt, dessen Schlüssel die Entwicklungs-Identifier der verknüpften Content Pools sind; jede Gruppe kann im Applet unabhängig verarbeitet oder gestaltet werden. Das ist die richtige Wahl für Layouts mit mehreren Bereichen, zum Beispiel einem Hero-Bereich, einer Infospalte oder einer Menükarte.
Im strukturierten Modus gibt es keine vorgegebenen Inhaltsfelder: Ausser dem QR Code Link definierst du alle Felder selbst. Die verknüpften Felder landen mit ihrem Feldnamen direkt im Eintrag (nicht in einem custom-Objekt). Der Entwicklungs-Identifier ist in diesem Modus für jeden Content Pool zwingend und muss pro Integration eindeutig sein.
{
"data": {
"hero": [
{
"titel": "Willkommen",
"bild": "https://storage.flyo.cloud/welcome_ab12cd34.jpg",
"uid": "f8855c2b6709de28df52df51aabe6ab6",
"qrcode": false,
"scanurl": false
}
],
"info": [
{
"titel": "Öffnungszeiten",
"text": "Mo bis Fr, 08:00 bis 18:00 Uhr",
"uid": "0c627b93419b41ebc3f0d71ed0a5bd97",
"qrcode": false,
"scanurl": false
},
{
"titel": "Adresse",
"text": "Bahnhofstrasse 1",
"uid": "41ebc3f0d71ed0a5bd970c627b93419b",
"qrcode": false,
"scanurl": false
}
]
},
"config": {
"timeout": 15000
}
}Jeder Eintrag einer Gruppe enthält neben den selbst definierten Feldern automatisch:
| Feld | Beschreibung |
|---|---|
uid | Eindeutige ID des Eintrags |
link | Der verknüpfte QR Code Link als Roh-URL (nur wenn verknüpft) |
qrcode | Fertiger QR-Code als SVG Data-URI; false, wenn kein Link verknüpft ist |
scanurl | Die URL, auf die der QR-Code zeigt; false, wenn kein Link verknüpft ist |
Variabeln
Neben den Inhalten aus den Content Pools kann eine Integration Werte enthalten, die direkt in Flyo erfasst werden: ein Wochentitel, ein Hinweistext, ein Coverbild, eine Menükarte. Sie werden im Tab «Konfiguration» der Integration festgelegt und im Tab «Inhalte» befüllt, siehe Variabeln.
Im Applet stehen sie unter vars, jeweils unter dem Identifier, der in der Integration vergeben wurde:
const { vars } = await response.json();
document.querySelector('h1').textContent = vars.wochentitel;
document.querySelector('img').src = vars.coverbild.source;{
"vars": {
"wochentitel": "Frühlingserwachen",
"coverbild": {
"source": "https://storage.flyo.cloud/cover_ab12cd34.jpg",
"caption": "Neue Wasserwelten",
"copyright": "Zoo Zürich",
"name": "cover.jpg",
"id": 4711,
"mime_type": "image/jpeg"
},
"menuekarte": [
{ "gericht": "Tagesteller", "preis": "18.50" },
{ "gericht": "Suppe", "preis": "7.00" }
]
}
}Variabeln liefern den vollen Wert ihres Typs
Das ist der wichtigste Unterschied zu den verknüpften Feldern eines Slides. Ein Slide-Feld wird immer auf einen Text reduziert, weil dort erst beim Verknüpfen feststeht, was für ein Inhalt kommt. Eine Variable dagegen wird mit ihrem Typ deklariert, also liefert sie den vollständigen Wert dieses Typs:
| Feldtyp | im Slide (data) | als Variable (vars) |
|---|---|---|
| Bild | die URL als Text | Objekt mit source, caption, copyright, name, id, mime_type |
| Link | die URL als Text | Objekt mit href, type, target, raw und extras |
| Text Editor (WYSIWYG) | das HTML als Text | Objekt mit html und json |
| Dropdown | das Label als Text | Objekt mit value, label und allen options |
| Mehrfacheingabe | nicht verfügbar | Array mit einem Objekt pro Zeile |
Welche Felder eine bestimmte Integration liefert und wie sie typisiert sind, steht in ihrer OpenAPI-Definition. Sie wird aus den Deklarationen erzeugt, ist also immer aktuell und eignet sich direkt zum Generieren von Typen.
Variabeln ohne gültigen Identifier werden nicht ausgeliefert. Ein Identifier beginnt mit einem Buchstaben oder einem Unterstrich und enthält danach nur Buchstaben, Zahlen und Unterstriche.
Ändert sich der Wert einer Variable, wird die Datenschnittstelle wie bei einer Inhaltsänderung aktualisiert und die verbundenen Bildschirme laden das Applet neu.
Der Entwicklungs-Identifier
Der Entwicklungs-Identifier kann im Tab «Inhalte» unter «Verknüpfung der Inhalte» pro Content Pool gesetzt werden. Das Feld ist nur für die Entwicklung vorgesehen und wird nie auf dem Bildschirm angezeigt. Es definiert, wie Inhalte aus dem Content Pool innerhalb des Applets strukturiert, gruppiert und im Frontend verwendet werden können:
- Bei Slides ist der Identifier optional und Teil jedes Datensatzes (Feld
item_identifier). Er dient der Zuordnung oder Filterung, zum Beispiel um Einträge aus einem bestimmten Content Pool im Applet anders darzustellen. - Bei Strukturiert ist der Identifier zwingend und wird zum Gruppenschlüssel im
data-Objekt.
Der Entwicklungs-Identifier ermöglicht es, Layouts, Komponenten oder Logik gezielt an eine bestimmte Datenstruktur oder Inhaltsgruppe anzupassen.
QR-Codes und Metriken
Wird in der Integration das Feld «QR Code Link» verknüpft, generiert Flyo für jeden Eintrag automatisch einen QR-Code:
qrcodeenthält den fertigen QR-Code als SVG Data-URI und kann im Applet direkt als Bildquelle verwendet werden.scanurlist die URL hinter dem QR-Code. Beim Scannen erfasst Flyo die Interaktion in den Flyo Metriken und leitet anschliessend auf den hinterlegten Link weiter.metric(nur bei Slides) ist eine URL, die das Applet aufrufen kann, wenn ein Slide angezeigt wird. So lassen sich die Anzeigen der Inhalte in den Flyo Metriken messen.
Fehler abfangen und protokollieren
Ein Bildschirm läuft oft wochenlang unbeaufsichtigt, und niemand steht davor, wenn etwas schiefgeht. Zwei Dinge haben sich in produktiven Applets bewährt: Das Applet schreibt Log-Einträge, die du direkt in Flyo einsehen kannst, und es wiederholt einen fehlgeschlagenen Abruf der Datenschnittstelle selbständig.
Logs senden
Mit sos.command.dispatch() aus dem SignageOS SDK schickt das Applet einen Log-Eintrag an SignageOS. Flyo holt diese Einträge pro Bildschirm ab und zeigt sie an, siehe Logs in Flyo ansehen. Ein Eintrag ist ein Objekt mit dem Pflichtfeld type; alle weiteren Felder bestimmst du selbst:
import sos from '@signageos/front-applet';
// Ein Log darf das Applet nie unterbrechen, auch nicht bei einem
// ungültigen type oder lokal ohne SignageOS.
function log(entry) {
Promise.resolve()
.then(() => sos.command.dispatch(entry))
.catch(() => {});
}
log({
type: 'Flyo.LoadFailed',
url: api,
status: 503,
message: 'TypeError: Failed to fetch'
});Darauf solltest du achten:
- Gültiger
type: Erlaubt sind nur Buchstaben, Zahlen, Punkt, Bindestrich und Unterstrich, höchstens 100 Zeichen. Andernfalls lehnt das SDK den Eintrag ab. Bewährt hat sich ein Präfix pro Bereich, zum BeispielFlyo.LoadFailed,Content.CacheFailedoderVideo.Ended. - Sparsam loggen: SignageOS drosselt das Senden auf rund 30 Einträge in 30 Sekunden; was darüber liegt, kann verloren gehen. Logge Ereignisse wie Fehler, Wiederholungen oder abgebrochene Videos, aber nicht jeden Slide-Wechsel.
- Fehler als Text: Übergib eine Fehlermeldung als
String(error). EinError-Objekt wird beim Übertragen nicht als lesbare Meldung übernommen. - Kontext mitgeben: Ein Eintrag ist erst hilfreich, wenn er ohne Zugriff auf das Gerät erklärt, was passiert ist: die betroffene URL, den HTTP-Status, den Grund oder einen Fehlercode.
Diese Einträge haben sich in der Praxis bewährt:
type | Wann | Felder |
|---|---|---|
Flyo.LoadFailed | Der Abruf der Datenschnittstelle ist fehlgeschlagen | url, status, attempt, message |
Content.CacheFailed | Eine Datei konnte nicht in den Offline-Cache geladen werden (sos.offline.cache.loadOrSaveFile); das Applet überspringt diesen Slide, statt ganz anzuhalten | url, message |
Video.LoadFailed | Ein Video konnte nicht geladen werden | url, message |
Video.Ended | Ein Video wurde beendet, regulär oder wegen eines Fehlers | url, reason, currentTime, errorCode |
Logs in Flyo ansehen
Öffne die Integration und wechsle in den Tab «Bildschirme». Mit dem Info-Symbol neben einem Bildschirm öffnest du die Geräteinformationen, ein Klick auf «Logs anzeigen» lädt die Einträge:
- Angezeigt werden die Einträge der letzten 7 Tage, die neusten zuoberst, höchstens 30.
- Die Spalte «Typ» zeigt den
typedes Eintrags, «Details» alle übrigen Felder. Verschachtelte Objekte und Arrays erscheinen als formatiertes JSON. - Es erscheinen nur die Logs des Applets, das dem Bildschirm aktuell zugewiesen ist.
- Mit «Logs neu laden» holst du neu eingetroffene Einträge.
Im selben Dialog findest du auch die Screenshots des Bildschirms. Zusammen ergeben sie meist schon das ganze Bild: was der Bildschirm gerade zeigt und weshalb.
Laden automatisch wiederholen
Bildschirme starten häufig, bevor das Netzwerk bereit ist, oder die Verbindung bricht beim Abruf kurz ab. Fängt das Applet diesen Fall nicht ab, bleibt der Bildschirm auf einer Fehlermeldung oder einem leeren Bild stehen. Von selbst löst sich das nicht: Flyo lädt das Applet erst bei der nächsten Inhaltsänderung neu, sonst hilft nur ein Neustart des Geräts.
Wiederhole deshalb einen fehlgeschlagenen Abruf in einem festen Abstand und mit einer Obergrenze an Versuchen:
const RETRY_DELAY = 10000; // 10 Sekunden zwischen zwei Versuchen
const MAX_RETRIES = 15; // zusammen rund zweieinhalb Minuten
let retryCount = 0;
async function loadContent(api) {
let status = null;
let json;
try {
const response = await fetch(api);
status = response.status;
// fetch wirft bei einer Fehlerantwort des Servers keinen Fehler
if (!response.ok) {
throw new Error(`HTTP ${status}`);
}
json = await response.json();
} catch (error) {
log({
type: 'Flyo.LoadFailed',
url: api,
status: status,
attempt: retryCount + 1,
message: String(error)
});
const message = status
? `Inhalte konnten nicht geladen werden (Fehler ${status})`
: 'Keine Verbindung zum Server';
if (retryCount < MAX_RETRIES) {
retryCount += 1;
showError(`${message}, neuer Versuch (${retryCount}/${MAX_RETRIES})`);
setTimeout(() => loadContent(api), RETRY_DELAY);
} else {
showError(message);
}
return;
}
// erst nach einem erfolgreichen Abruf zurücksetzen und anzeigen
retryCount = 0;
render(json);
}
sos.onReady().then(() => loadContent(sos.config.api));showError() und render() stehen für die Darstellung in deinem Applet. Wichtig dabei:
response.okprüfen:fetchschlägt nur fehl, wenn gar keine Verbindung zustande kommt. Antwortet der Server mit einem Fehler (zum Beispiel 502), muss das Applet das selbst erkennen, sonst verarbeitet es die Fehlerantwort als Inhalt.- Darstellung ausserhalb des
try: Nur der Abruf wird wiederholt. Ein Fehler beim Darstellen der Inhalte ist kein Verbindungsproblem und würde sonst sinnlos wiederholt. - Fortschritt anzeigen: Ein Hinweis wie «neuer Versuch (3/15)» zeigt auch vor Ort, dass der Bildschirm arbeitet und nicht hängt.
- Grenzen bewusst wählen: Nach dem letzten Versuch bleibt die Fehlermeldung stehen, bis Flyo das Applet neu lädt oder das Gerät neu startet. Wähle Abstand und Anzahl so, dass sie die Startzeit deiner Geräte sicher abdecken. Jeder Versuch schreibt einen Log-Eintrag; mit 10 Sekunden Abstand bleibt das deutlich unter der Drosselung von SignageOS.
- Timer aufräumen: Wird das Applet beendet (zum Beispiel beim Zerstören einer Vue-Komponente), stoppe einen ausstehenden Versuch mit
clearTimeout.
Applet hochladen
Upload nur via CLI: Das Applet muss via CLI in die SignageOS-Plattform hochgeladen werden. Nach dem erfolgreichen Upload ist das Applet in der Flyo Integration im Tab «Konfiguration» unter «Applet» verfügbar und kann ausgewählt werden.
- Clone das Beispiel-Repository (oder dein eigenes) auf deinen Computer:
git clone https://github.com/flyocloud/signageos-js-example.gitodergit clone https://github.com/flyocloud/signageos-vue2-example.git - Führe im Ordner des Clones den Befehl
npm installaus. - Installiere das globale SignageOS CLI Script mittels
npm install @signageos/cli -g - Logge dich mit dem Befehl
sos loginein (aktiver SignageOS Box Account vorausgesetzt). - Stelle die Standard-Organisation ein:
sos organization set-default - Lade das Applet mittels
npm run releasehoch. Das Ergebnis auf diesen Befehl sollte lauten: «Applet XYZ version 1.0.0 has been uploaded.»
Lokale Entwicklung
Für die lokale Entwicklung startest du das Applet mit npm start (im JavaScript-Beispiel läuft es danach unter http://localhost:8090). Da das Applet lokal keine Konfiguration von SignageOS erhält, hinterlegst du als Fallback die API-URL deiner Integration direkt im Code (siehe Codebeispiel oben). Die URL der Datenschnittstelle findest du in der OpenAPI-Definition der Integration.
Hinweis von SignageOS zum Testen im Emulator: Problem mit dem Emulator in Chrome (Adblock)

