Zum Hauptinhalt springen

Übersetzer entwickeln

Dies ist eine praktische Anleitung zum Schreiben eigener Übersetzer. Sie ist die Begleitung zur Referenz Translator API - die Referenz definiert das Objektschema und wie der Service den Code ausführt; diese Anleitung zeigt, wie man einen guten Übersetzer baut, testet und hochlädt, beginnend mit der wichtigsten Regel: konsistent mit dem Datenmodell der IoT-Plattform zu sein.

Sie führt durch fünf vollständige, kopierbare Vorlagen mit steigender Komplexität, dann durch einen lokalen Test-Harness und wie man einen Übersetzer für den Upload paketiert:

  1. Hardware-Übersetzer - aus dem Referenz-Decoder eines Herstellers
  2. Hardware-Übersetzer - von Grund auf, gegen eine Byte-Spezifikation
  3. calculate - ein abgeleiteter Wert (verkettet)
  4. set-alarm - Schwellenwerte mit Hysterese (verkettet, zustandsbehaftet)
  5. Analytics - periodenweise Akkumulation (verkettet, zustandsbehaftet)

1. Warum die Konsistenz des Datenmodells wichtig ist (zuerst lesen)​

Die IoT-Plattform legt die Messwerte jedes Geräts auf einem einzigen Node (dem Iotnode) als flache Menge von Feldnamen ab. Es gibt kein Schema pro Gerät und keinen Entitätstyp-Kontext zur Lesezeit - die Bedeutung eines Wertes liegt vollständig in seinem Feldnamen, seiner Einheit und seiner Größe.

Das ist nur nützlich, wenn jeder Übersetzer dieselben Namen verwendet. Ein relativeHumidity-Messwert von einem Dragino, einem Milesight, einem Elsys und einem Netvox muss das identische Feld sein - derselbe Name (relativeHumidity), dieselbe Einheit (%), dieselbe Größe (relativeHumidity). Wenn das so ist:

  • Funktionieren Dashboards, Alarmansichten, Exporte und Diagramme für jedes Gerät ohne herstellerspezifische Konfiguration.
  • Setzt sich Logik zusammen: Ein set-alarm- oder calculate-Übersetzer läuft auf jedem Gerät, das das Feld ausgibt, das er liest.
  • Teilen sich alle ein mentales Modell statt vieler herstellerspezifischer Schreibweisen.

Erfinden Sie humidity, BatV oder temp_c, erscheint der Wert trotzdem - aber jede nachgeschaltete Regel, jeder Alarm und jedes Dashboard stimmt still nicht mehr damit überein.

Die Regeln, die das Modell konsistent halten:

  • Verwenden Sie kanonische Feldnamen, Einheiten und Größen. Die gängigen sind in der Datenmodell-Tabelle der Translator API aufgeführt; die vollständige Menge bereits verwendeter Übersetzer kann mit GET /api/translators aufgelistet werden. Prüfen Sie auf einen existierenden Namen, bevor Sie einen erfinden.

  • Namen sind lowerCamelCase, flach. Kein snake_case, kein ALL_CAPS, kein Herstellerjargon (TempC1, BatV) in der Ausgabe. Behalten Sie die rohen Herstellernamen im Decoder; benennen Sie sie im finalen Ausgabeobjekt in kanonische Namen um.

  • Einheiten sind SI-Symbole (V, A, W, Pa, m, m/s, kWh, °C, %). Dimensionslose Werte (Zählungen, Indizes, Alarme) verwenden die Einheit '' mit einer camelCase- quantity. Konvertieren Sie im Wrapper (z. B. hPa → Pa ist × 100, mV → V ist / 1000).

  • Die Temperatur trägt ihr Medium im Namen - ein klassischer Fehler:

    FeldBedeutung
    temperatureLuft / Umgebung, geräteinterne Messung
    waterTemperatureWassermedium (Zähler, Tauchsonden)
    soilTemperatureBodensonde
    surfaceTemperatureOberfläche / Kontakt
    externalTemperature / 2 / 3Sonde mit nicht näher bestimmtem Medium
    internalTemperature / cpuTemperatureGeräteelektronik

Wenn Sie nur eine Sache aus dieser Anleitung mitnehmen: Bilden Sie die rohen Gerätenamen an genau einer Stelle - dem finalen Ausgabeobjekt - auf die kanonischen Felder der IoT-Plattform ab, und erfinden Sie nie einen Namen, der bereits existiert.


2. Anatomie eines Übersetzers​

Ein Übersetzer ist ein JSON-Objekt (siehe die Translator API für das vollständige Schema):

{
name: 'acme-th-100', // Kleinbuchstaben, Bindestriche, beginnt mit einem Buchstaben
version: '1.0.0', // semver
apiVersion: '1.0',
description: '...', // benutzerseitiges Markdown (siehe unten)
match: {deviceModelName: 'acme-th-100'},
parameters: { /* optionale Nutzereingaben */ },
spec: { /* die Felder, die Sie ausgeben, siehe unten */ },
code: '...', // Ihre translate()-Funktion als Zeichenkette
}

spec deklariert jedes Feld, das Sie ausgeben; der Service lehnt jeden ausgegebenen Schlüssel ab, der nicht in spec steht. Jeder Eintrag ist entweder eine reine Typ-Zeichenkette ('string', 'number', 'boolean', 'date', 'object', 'array') oder ein Messgrößen-Deskriptor {type, unit, quantity}:

spec: {
temperature: {type: 'number', unit: '°C', quantity: 'temperature'},
batteryLow: 'boolean',
}

code ist Ihre translate-Funktion als Zeichenkette. Zwei Formen:

  • Hardware-Decoder: function translate({encodedData}) { ... } - erhält den rohen Uplink (encodedData.hexEncoded, encodedData.port).
  • Verketteter Übersetzer: function translate(iotnode, parameters) { ... } - läuft nach einem anderen Übersetzer und liest bereits dekodierte kanonische Felder vom iotnode (z. B. iotnode.temperature). Siehe „Verkettung“ in translator-api.md.

Die Sandbox stellt alle Standard-JavaScript-Built-ins bereit (Date, Math, JSON, …) sowie lodash (_), Buffer und console.log. Sie stellt kein require, keine Timer und keinen Netzwerk-/Dateisystemzugriff bereit. Date.now() / new Date() funktionieren zwar - aber bevorzugen Sie iotnode.reportedAt für Beobachtungs-/Ereigniszeiten: Die Systemzeit ist die Verarbeitungszeit und nicht deterministisch (das bricht die Reproduzierbarkeit, wenn eine Übersetzung wiederholt wird).

description ist benutzerseitige Dokumentation - der Text, den ein Kunde in der IoT-Plattform liest, um zu entscheiden, ob ein Übersetzer passt, also lohnt es sich, ihn gut zu schreiben. Beginnen Sie mit einem einfachen Satz darüber, was das Gerät ist (oder, bei einem verketteten Übersetzer, was er berechnet), dann eine Liste ### Decoded output mit `feldname` (Einheit) - Bedeutung für jedes Feld.

Bei Analytics-/verketteten Übersetzern besondere Sorgfalt walten lassen. Ihr Wert ist nicht aus einer Feldliste ersichtlich, daher muss die Beschreibung auch erklären, was sie berechnet, welche Eingaben sie liest, und den Anwendungsfall - z. B. „Verwandelt einen kumulativen Energiezählerwert in einen Verbrauch pro Tag/Woche/Monat, der jeweils um Mitternacht lokaler Zeit zurückgesetzt wird; kombinieren Sie ihn mit set-alarm-energy-consumption, um Überverbrauch zu erkennen.“ Eine klare Beschreibung macht einen Übersetzer auffindbar und korrekt anwendbar; eine vage wird falsch verwendet.

Die untenstehenden Vorlagen halten jeden Übersetzer in zwei Dateien - translate.js (die Funktion) und manifest.json (alles andere) - weil das der Test-Harness und das Paketierungsskript verwenden. Sie können die Funktion auch mit code: translate.toString() inline einbinden.


3. Vorlage 1 - Hardware-Übersetzer aus dem Referenz-Decoder eines Herstellers​

Der häufigste Fall. Der Hersteller (oder das lorawan-devices-Repository von The Things Network) liefert einen JS-Decoder. Behalten Sie ihn unverändert bei und fügen Sie einen dünnen Wrapper hinzu, der seine Rohausgabe in die kanonischen Felder der IoT-Plattform umbenennt - harmonisieren Sie als letzten Schritt und skalieren Sie die Einheiten korrekt.

translate.js

/* global _, Buffer */

function translate ({encodedData}) {
const {hexEncoded, port} = encodedData;
if (!hexEncoded || !port) {
throw new Error('Expected fields hexEncoded and/or port are missing');
}

const bytes = [...Buffer.from(hexEncoded, 'hex')];
const decoded = decodeUplink({bytes, fPort: port}).data; // Herstellerdecoder aufrufen

// Der EINZIGE Harmonisierungsschritt: rohe Herstellernamen auf kanonische Felder abbilden.
return {
result: {
temperature: _.get(decoded, 'TempC_SHT'), // Herstellername -> kanonisch
relativeHumidity: _.get(decoded, 'Hum_SHT'),
batteryVoltage: _.get(decoded, 'BatV'),
},
};
}

// --- Herstellerdecoder, unverändert -----------------------------------------
// Importiert von https://github.com/TheThingsNetwork/lorawan-devices/.../acme-th-100.js
function decodeUplink (input) {
// ...Herstellercode, unverändert...
return {data: {TempC_SHT: 24.3, Hum_SHT: 52.1, BatV: 3.01}};
}

manifest.json

{
"name": "acme-th-100",
"version": "1.0.0",
"apiVersion": "1.0",
"description": "Acme TH-100 is a wireless sensor that measures room air temperature and humidity.\n\n### Decoded output\n- `temperature` (°C) - air temperature\n- `relativeHumidity` (%) - relative air humidity\n- `batteryVoltage` (V) - battery voltage",
"match": {"deviceModelName": "acme-th-100"},
"spec": {
"temperature": {"type": "number", "unit": "°C", "quantity": "temperature"},
"relativeHumidity": {"type": "number", "unit": "%", "quantity": "relativeHumidity"},
"batteryVoltage": {"type": "number", "unit": "V", "quantity": "voltage"}
}
}

Wichtige Punkte:

  • „Reparieren“ Sie nicht den Herstellerdecoder (Bit-Offsets, Skalierung) auf Verdacht - prüfen Sie zuerst gegen die Gerätespezifikation. Zitieren Sie die Quell-URL in einem Kommentar.
  • Jede Hilfsfunktion, die der Herstellerdecoder aufruft, muss in derselben Datei translate.js stehen - die gesamte Zeichenkette läuft in der Sandbox, sodass eine nicht enthaltene Hilfsfunktion auf oberster Ebene „X is not defined“ auslöst.

Variante - ein Decoder, der einen Buffer erwartet​

Viele Herstellerdecoder lesen die Payload als Node-Buffer (mit buf.readInt16BE(...), buf.readUInt8(...), …) statt als Byte-Array. Buffer ist in der Sandbox verfügbar, bauen Sie also einen aus dem Hex und übergeben Sie ihn direkt an die Herstellerfunktion - harmonisieren Sie dann genau wie oben:

/* global _, Buffer */

function translate ({encodedData}) {
const {hexEncoded, port} = encodedData;
if (!hexEncoded || !port) {
throw new Error('Expected fields hexEncoded and/or port are missing');
}

const buf = Buffer.from(hexEncoded, 'hex'); // der Herstellerdecoder erwartet einen Buffer
const decoded = decode(buf, port); // Herstellerfunktion, unverändert

return {
result: {
temperature: decoded.temp / 10, // roh 0,1 °C -> °C
relativeHumidity: decoded.hum,
batteryVoltage: decoded.batt_mv / 1000, // mV -> V
},
};
}

/* eslint-disable */
// Importiert von <vendor url>
function decode (buf, port) {
return {
temp: buf.readInt16BE(0), // 0,1 °C, signiert
hum: buf.readUInt8(2), // %
batt_mv: buf.readUInt16BE(3), // mV
};
}

Dieselbe Regel: Behalten Sie decode unverändert bei, und führen Sie alle Umbenennungen und Einheitenskalierungen im result des Wrappers durch.


4. Vorlage 2 - Hardware-Übersetzer von Grund auf (Byte-Spezifikation)​

Kein Herstellerdecoder in JS, nur eine Byte-Layout-Spezifikation: Parsen Sie die Bytes selbst. Annahme: byte0 = Nachrichtentyp, byte1-2 = Temperatur ×10 (signiertes int16, big-endian), byte3 = Feuchtigkeit %, byte4-5 = Batterie mV.

translate.js

/* global _, Buffer */

function translate ({encodedData}) {
const {hexEncoded, port} = encodedData;
if (!hexEncoded || !port) {
throw new Error('Expected fields hexEncoded and/or port are missing');
}

const bytes = [...Buffer.from(hexEncoded, 'hex')];
const int16 = (hi, lo) => { // signiertes 16-Bit big-endian
const raw = (bytes[hi] << 8) | bytes[lo];
return raw & 0x8000 ? raw - 0x10000 : raw;
};
const uint16 = (hi, lo) => (bytes[hi] << 8) | bytes[lo];

return {
result: {
temperature: int16(1, 2) / 10, // ×10 in der Payload
relativeHumidity: bytes[3], // ganze %
batteryVoltage: uint16(4, 5) / 1000, // mV -> V (kanonische Einheit ist V)
},
};
}

Die manifest.json hat dieselbe Form wie Vorlage 1. Beachten Sie die Einheitenumrechnung (mV → V), die im Wrapper vorgenommen wird, damit die Ausgabe in der kanonischen Einheit erfolgt. Für einen geräte­spezifischen Wert ohne kanonisches Äquivalent geben Sie trotzdem eine Einheit + Größe inline an: "chamberPressure": {"type": "number", "unit": "Pa", "quantity": "pressure"}.


5. Vorlage 3 - calculate (ein abgeleiteter Wert, verkettet)​

Ein verketteter Übersetzer läuft nach dem Hardware-Übersetzer und liest kanonische Felder vom iotnode. Dieser hier leitet den Taupunkt aus temperature + relativeHumidity ab, sodass er auf jedem Gerät funktioniert, das diese beiden Felder ausgibt.

translate.js

/* global _ */

// Ein numerisches Feld lesen: ein optionaler Override-Pfad gewinnt; ansonsten die
// Standardkandidaten in Reihenfolge versuchen und die erste endliche Zahl nehmen.
const resolveNumeric = (iotnode, overrideField, defaultPaths) => {
const paths = overrideField ? [overrideField] : defaultPaths;
return paths.map(p => {
const v = _.get(iotnode, p);
return typeof v === 'number' ? v : parseFloat(v);
}).find(Number.isFinite);
};

function translate (iotnode, parameters) {
const temperature = resolveNumeric(iotnode, _.get(parameters, 'temperatureField'), ['temperature']);
const relativeHumidity = resolveNumeric(iotnode, _.get(parameters, 'relativeHumidityField'), ['relativeHumidity']);

if (!_.isFinite(temperature) || !_.isFinite(relativeHumidity)) {
return {}; // nichts zu tun, bis beide Eingaben vorhanden sind
}

// Magnus-Formel
const a = 17.62;
const b = 243.12;
const gamma = Math.log(relativeHumidity / 100) + (a * temperature) / (b + temperature);
const dewPoint = Math.round(((b * gamma) / (a - gamma)) * 100) / 100;

return {result: {dewPoint}};
}

manifest.json (parameters + spec)

{
"name": "calculate-dew-point-from-temperature-rh",
"version": "1.0.0",
"apiVersion": "1.0",
"description": "Calculates the dew point (°C) from a sensor's temperature and relative humidity.\n\nChained translator - runs on top of the device's hardware translator and reads its decoded temperature and relativeHumidity.\n\n### Decoded output\n- `dewPoint` (°C) - the calculated dew point",
"match": {"deviceModelName": "calculate-dew-point-from-temperature-rh"},
"parameters": {
"temperatureField": {"type": "string", "description": "Field to read temperature from. Default \"temperature\".", "default": "temperature", "optional": true},
"relativeHumidityField": {"type": "string", "description": "Field to read relative humidity from. Default \"relativeHumidity\".", "default": "relativeHumidity", "optional": true}
},
"spec": {
"dewPoint": {"type": "number", "unit": "°C", "quantity": "temperature"}
}
}

Der Service entfernt undefined/null aus result, sodass ein Übersetzer ein Feld bedingt ausgeben und einfach das reine Objekt zurückgeben kann.


6. Vorlage 4 - set-alarm (Schwellenwerte + Hysterese, verkettet & zustandsbehaftet)​

Alarme brauchen Hysterese (damit sie nicht am Schwellenwert flattern) und vorherigen Zustand (vom Iotnode gelesen - die eigene letzte Ausgabe eines Alarm-Übersetzers ist beim nächsten Lauf verfügbar).

translate.js

/* global _ */

const resolveNumeric = (iotnode, overrideField, defaultPaths) => {
const paths = overrideField ? [overrideField] : defaultPaths;
return paths.map(p => {
const v = _.get(iotnode, p);
return typeof v === 'number' ? v : parseFloat(v);
}).find(Number.isFinite);
};

function translate (iotnode, parameters) {
const highLevel = _.get(parameters, 'temperatureHighAlarmLevel', 30);
const lowLevel = _.get(parameters, 'temperatureLowAlarmLevel', 4);
const hysteresis = _.get(parameters, 'temperatureAlarmHysteresis', 1);
const temperature = resolveNumeric(iotnode, _.get(parameters, 'temperatureField'), ['temperature']);

if (highLevel <= lowLevel) return {result: {errorMessage: 'high alarm level must be greater than low alarm level'}};
if (!_.isFinite(temperature)) return {result: {errorMessage: 'temperature is missing'}};

const prevHigh = _.get(iotnode, 'temperatureHighAlarm');
const prevLow = _.get(iotnode, 'temperatureLowAlarm');

// Auslösen oberhalb des Levels; erst zurücksetzen, wenn wieder unter (Level - Band). undefined = Zustand behalten.
const highAlarm = temperature > highLevel ? true
: (temperature < highLevel - hysteresis || _.isUndefined(prevHigh)) ? false : undefined;
const lowAlarm = temperature < lowLevel ? true
: (temperature > lowLevel + hysteresis || _.isUndefined(prevLow)) ? false : undefined;

const result = {};
if (highAlarm !== undefined) result.temperatureHighAlarm = highAlarm;
if (lowAlarm !== undefined) result.temperatureLowAlarm = lowAlarm;

return _.isEmpty(result) ? {} : {result};
}

manifest.json (parameters + spec) - Alarmfelder sind subjekt-first Booleans:

{
"parameters": {
"temperatureHighAlarmLevel": {"type": "number", "description": "High threshold (°C). Default 30.", "default": 30, "optional": true},
"temperatureLowAlarmLevel": {"type": "number", "description": "Low threshold (°C). Default 4.", "default": 4, "optional": true},
"temperatureAlarmHysteresis": {"type": "number", "description": "Hysteresis band (°C). Default 1.", "default": 1, "optional": true},
"temperatureField": {"type": "string", "description": "Field to read. Default \"temperature\".", "optional": true}
},
"spec": {
"temperatureHighAlarm": "boolean",
"temperatureLowAlarm": "boolean",
"errorMessage": "string"
}
}

Da er temperature generisch liest, funktioniert derselbe Alarm für Luft-, Wasser- oder Oberflächentemperatur, indem temperatureField auf waterTemperature / surfaceTemperature gesetzt wird.


7. Vorlage 5 - Analytics (periodenweise Akkumulation, verkettet & zustandsbehaftet)​

Die komplexeste Form: Sie hält Zustand über Uplinks hinweg. Es gibt keine andere Persistenz - schreiben Sie den Zustand als gewöhnliches Feld zurück (ein *State-Objekt) und lesen Sie ihn beim nächsten Lauf. Dies verwandelt einen kumulativen Zählerwert in Verbrauch pro Tag/Woche/Monat/Quartal/Jahr, wobei jede Periode an ihrer lokalen Zeitzonengrenze zurückgesetzt wird.

translate.js

/* global _ */

const isValidTimeZone = tz => { try { Intl.DateTimeFormat(undefined, {timeZone: tz}); return true; } catch (e) { return false; } };
const weekStart = d => { const x = new Date(d); const day = (x.getDay() + 6) % 7; x.setDate(x.getDate() - day); x.setHours(0, 0, 0, 0); return x; };

// Ein flaches Ausgabefeld pro rollierender Periode + ein "changed"-Prädikat.
const PERIODS = [
{field: 'energyConsumptionDay', changed: (c, p) => c.toDateString() !== p.toDateString()},
{field: 'energyConsumptionWeek', changed: (c, p) => weekStart(c).getTime() !== weekStart(p).getTime()},
{field: 'energyConsumptionMonth', changed: (c, p) => c.getFullYear() !== p.getFullYear() || c.getMonth() !== p.getMonth()},
{field: 'energyConsumptionQuarter', changed: (c, p) => c.getFullYear() !== p.getFullYear() || ((c.getMonth() / 3) | 0) !== ((p.getMonth() / 3) | 0)},
{field: 'energyConsumptionYear', changed: (c, p) => c.getFullYear() !== p.getFullYear()},
];

function translate (iotnode, parameters) {
const energyField = _.get(parameters, 'energyField', 'activeEnergyImport');
const timeZone = _.get(parameters, 'timeZone', 'Europe/Stockholm');
const current = _.get(iotnode, energyField);

if (!_.isFinite(current)) return {result: {errorMessage: `field "${energyField}" not found or not numeric`}};
if (!isValidTimeZone(timeZone)) return {result: {errorMessage: `invalid timeZone "${timeZone}"`}};

// Die Uplink-Zeit verwenden; new Date() funktioniert, spiegelt aber die Verarbeitungszeit (nicht deterministisch).
const reportedAt = _.get(iotnode, 'reportedAt', new Date().toISOString());
const prevReportedAt = _.get(iotnode, 'energyConsumptionPrevReportedAt', reportedAt);
const now = new Date(new Date(reportedAt).toLocaleString('sv-SE', {timeZone}));
const prev = new Date(new Date(prevReportedAt).toLocaleString('sv-SE', {timeZone}));

// Vorherigen Zustand lesen; Baselines bei erstem Auftreten setzen.
const state = _.get(iotnode, 'energyConsumptionState', {});
let baselines = Array.isArray(state.baselines) && state.baselines.length === PERIODS.length
? state.baselines.slice()
: PERIODS.map(() => current);
const latest = _.isFinite(state.latest) ? state.latest : current;

// Zählerwechsel / Rollover (Messwert gefallen) -> überall neu baselinen.
if (current < latest) baselines = PERIODS.map(() => current);

const result = {};
baselines = baselines.map((baseline, i) => {
const rebase = PERIODS[i].changed(now, prev) ? current : baseline; // Periode gewechselt -> neu starten
result[PERIODS[i].field] = Math.round((current - rebase) * 1000) / 1000;
return rebase;
});

result.energyConsumptionState = {baselines, latest: current}; // Zustand für den nächsten Lauf zurückschreiben
result.energyConsumptionPrevReportedAt = reportedAt;
return {result};
}

manifest.json (spec-Auszug)

{
"parameters": {
"energyField": {"type": "string", "description": "Cumulative energy field (kWh). Default \"activeEnergyImport\".", "default": "activeEnergyImport", "optional": true},
"timeZone": {"type": "string", "description": "IANA time zone for period boundaries. Default \"Europe/Stockholm\".", "default": "Europe/Stockholm", "optional": true}
},
"spec": {
"energyConsumptionDay": {"type": "number", "unit": "kWh", "quantity": "energy"},
"energyConsumptionWeek": {"type": "number", "unit": "kWh", "quantity": "energy"},
"energyConsumptionMonth": {"type": "number", "unit": "kWh", "quantity": "energy"},
"energyConsumptionQuarter": {"type": "number", "unit": "kWh", "quantity": "energy"},
"energyConsumptionYear": {"type": "number", "unit": "kWh", "quantity": "energy"},
"energyConsumptionState": "object",
"energyConsumptionPrevReportedAt": "string",
"errorMessage": "string"
}
}

Analytics-Lektionen:

  • Zustand ist ein Feld, das in result zurückgeschrieben und beim nächsten Lauf gelesen wird. Nennen Sie es *State und dokumentieren Sie „nicht direkt konsumieren“.
  • Behandeln Sie Sensor-Resets/Rollover (Messwert fällt) durch Neu-Baselinen.
  • Verwenden Sie die Uplink-Zeit (reportedAt), konvertiert in die konfigurierte Zeitzone, für Periodengrenzen - Date.now() funktioniert, ist aber nicht deterministisch, es darf also nicht die Periodenlogik steuern.

8. Lokal testen​

Sie brauchen keine Infrastruktur der IoT-Plattform zum Testen - ein winziger Node-Harness rekonstruiert die Sandbox und führt exakt den Code aus, den Sie hochladen werden.

  1. Legen Sie Ihre Funktion in translate.js ab (wie in den obigen Vorlagen).
  2. Laden Sie translator-harness.js in denselben Ordner herunter und bearbeiten Sie sein CASES-Array mit echten Payloads und erwarteter Ausgabe.
  3. Führen Sie aus:
    npm install lodash
    node translator-harness.js

Der Harness gibt pro Fall PASS/FAIL aus. Er spiegelt die Entfernung von undefined/null aus result der Plattform, sodass Ihre erwarteten Objekte nur die tatsächlich ausgegebenen Felder enthalten.


9. Paketieren und hochladen​

  1. Legen Sie Ihre Metadaten (name, version, apiVersion, description, match, spec, parameters) in manifest.json ab (wie in den obigen Vorlagen).
  2. Laden Sie translator-build.js in denselben Ordner herunter und führen Sie es aus - es führt manifest.json + translate.js zu translator.json zusammen:
    node translator-build.js
  3. Laden Sie es hoch:
    curl -X POST "https://staging.yggio.net/api/translators" \
    -H "Authorization: Bearer $YGGIO_TOKEN" \
    -H "Content-Type: application/json" \
    --data @translator.json
    Siehe https://staging.yggio.net/swagger für die genaue Route und Authentifizierung. Verbinden Sie den Übersetzer nach dem Upload über die translatorPreferences mit einem Gerät (siehe translator-api.md).

Die von Ihnen getestete translate.js wird byte-für-byte als code hochgeladen, sodass ein grüner Harness-Lauf bedeutet, dass die Sandbox denselben Code ausführt. Er bedeutet nicht dasselbe Ergebnis: Die Plattform wendet auf den translate-Aufruf ein Timeout von 1 Sekunde und ein Speicherlimit von 16 MB an, löst translate als globale Variable auf und stellt ein Buffer-Polyfill statt Nodes eigenem bereit.


10. Fehlerbehebung​

Wenn ein Übersetzer lokal funktioniert, aber nach dem Hochladen nichts (oder das Falsche) tut, passiert normalerweise eines von drei Dingen:

  1. Ausgabe von der Spezifikation abgelehnt. Die häufigste Ursache. Jedes ausgegebene Feld muss in spec mit passendem Typ deklariert sein - eine exakte Übereinstimmung von Zeichen/Groß- Kleinschreibung. Erzwingen Sie Typen explizit (Number(...), String(...), Array.isArray(...)) und bestätigen Sie, dass jeder Feldname kanonisch ist.
  2. Der Übersetzer hat zur Laufzeit eine Ausnahme geworfen - meist ein undefinierter Wert aus einer unerwarteten Payload.
  3. Er ließ sich nicht kompilieren - ein Syntaxfehler nach der Stringifizierung, oder eine Hilfsfunktion, die die Funktion aufruft, aber nicht im hochgeladenen Code enthalten war.

Wo Sie den Fehler sehen. Wenn ein Übersetzer abstürzt, zeichnet die IoT-Plattform den Fehler in den Logs des Geräts auf, 6 Stunden lang aufbewahrt. Öffnen Sie die Logs des Geräts und filtern Sie:

  • Type = Debug
  • Category = System

Kopieren Sie die fehlerhafte Eingabe aus dem Log-Eintrag in Ihren lokalen Harness, um sie zu reproduzieren und zu beheben.

Eine erfolgreiche Übersetzung schreibt kein Log - ein leeres Debug/System- Log bedeutet also, dass der Übersetzer entweder einwandfrei lief oder nie ausgelöst wurde. Um festzustellen, welches der Fälle zutrifft, prüfen Sie die Spalte Last reported des Geräts in der Geräteliste: Wenn sie sich aktualisiert hat, lief der Übersetzer.


11. Checkliste vor dem Upload​

  1. Jedes Ausgabefeld verwendet einen kanonischen Namen, eine kanonische Einheit und Größe - keine erfundenen Namen (prüfen Sie die Datenmodell-Tabelle oder GET /translators).
  2. Temperaturfelder tragen das korrekte Medium (temperature vs. waterTemperature vs. surfaceTemperature…).
  3. Namen sind lowerCamelCase; Einheiten sind SI; Konvertierungen erfolgen im Wrapper (mV→V, hPa→Pa, …).
  4. Herstellerdecoder unverändert belassen; jede Hilfsfunktion, die er aufruft, befindet sich in translate.js; Herkunfts-URL zitiert.
  5. description ist einfache Sprache + eine Liste ### Decoded output.
  6. Major-Version erhöht, wenn (und nur wenn) sich das Datenmodell geändert hat (Feld hinzugefügt/umbenannt/entfernt, oder Einheit/Skalierung/Bedeutung geändert).
  7. Mit dem Harness getestet; erwartete Ausgabe stimmt überein.

Siehe die Referenz Translator API für das vollständige Objektschema, die Datenmodell-Feldtabelle, Verkettung, Gateways (additionalDeviceUpdates), upgradePolicy und Versionierung.