Kompletní ukázka: poloha ISS (driver + protokol)

Předchozí kapitoly ukázaly kostry — zde je nejmenší skutečně funkční dvojice driver + protokol, jakou lze napsat. Přestože se celá vejde na několik obrazovek, pracuje s reálnými živými daty: jednou za sekundu se dotáže veřejného REST API na aktuální polohu Mezinárodní vesmírné stanice a plní čtyři proměnné — zeměpisnou šířku, délku, výšku a rychlost objektu, který právě obíhá Zemi rychlostí přibližně 27 600 km/h.

Dělba práce je přesně ta, kterou používají všechny drivery a protokoly v QInsight:

  • IssDriver má jediný úkol: v zadané periodě stáhne přes HttpClient jednu JSON odpověď a předá ji protokolům tak, jak přišla. O jejím obsahu nic neví — z jeho pohledu jde pouze o text.
  • IssJsonProtocol dostane text, rozparsuje ho jako JSON a rozdělí hodnoty proměnným. Každá proměnná určuje commParamem path="latitude" své pole, takže jedna odpověď naplní všechny proměnné najednou.

Projekt ISS Live za běhu — graf šířky a délky, ukazatel výšky, aktuální hodnotyProjekt ISS Live za běhu — graf šířky a délky, ukazatel výšky, aktuální hodnoty

Data mají navíc užitečnou vlastnost: nejsou náhodná. Zeměpisná šířka kreslí sinusoidu s periodou jednoho oběhu (~92 minut). Zeměpisná délka zase pilový průběh: plynule roste, při 180° přejde na −180° a roste znovu — stanice letí stále „rovně“, ale Země se pod ní otáčí, takže každý oběh začíná o něco západněji. Necháte-li graf běžet několik minut, tyto tvary se stanou zřetelnými.

Odkud data pocházejí

Ukázka používá API wheretheiss.at — je zdarma, bez registrace a klíče. Jeden GET požadavek:

https://api.wheretheiss.at/v1/satellites/25544

vrátí jeden plochý JSON objekt (25544 je katalogové číslo ISS):

{
  "name": "iss",
  "latitude": -29.06,
  "longitude": 20.04,
  "altitude": 423.31,
  "velocity": 27575.13,
  "visibility": "daylight",
  ...
}

Provozovatel žádá o nejvýše ~1 požadavek za sekundu — driver proto rychlejší periodu odmítne (viz SetConfiguration níže). K běhu ukázky je potřeba připojení k internetu.

Jak si ukázku vyzkoušet

Hotový projekt s touto dvojicí pluginů je v instalátoru jako Qenex_IssLiveTracking.qproj, popsaný v příkladu Poloha ISS naživo. Postup níže jej sestavuje od začátku.

  1. Oba pluginy už dodává instalátor (Qenex.QSuite.Examples.IssDriver.dll ve složce Drivers, Qenex.QSuite.Examples.IssJsonProtocol.dll ve složce Protocols vedle QInsight.exe). Přeložíte-li si vlastní upravenou kopii (projekty níže), nahraďte jí tyto DLL.
  2. V Project Configuration přidejte driver ISS Position (example) — předvyplní se periodMs=1000;reconnectDelayMs=5000;reconnectAttempts=-1 — a k němu protokol ISS JSON Protocol (example). Protokol svůj driver jmenuje v CompatibleDrivers (viz níže), takže pod jiným driverem je zašedlý — a naopak.
  3. Vytvořte čtyři proměnné typu double a přiřaďte je protokolu s commParams:
    • Latitude: path="latitude"
    • Longitude: path="longitude"
    • Altitude: path="altitude"
    • Velocity: path="velocity"
  4. Aby grafy a ukazatele měly smysluplné rozsahy, doporučujeme prezentace: šířka −90…90 °, délka −180…180 °, výška 380…460 km, rychlost 27 400…27 700 km/h.
  5. Spusťte runtime — hodnoty se začnou aktualizovat ihned po první odpovědi API.

Projekty

Dva běžné class-library projekty; referencují jen základní projekty QSuite (Driver, resp. Protocol + VariableEvents). HttpClient je součást .NET, žádný NuGet balíček není potřeba.

<!-- IssDriver.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <AssemblyName>Qenex.QSuite.Examples.IssDriver</AssemblyName>
        <RootNamespace>Qenex.QSuite.Examples.IssDriver</RootNamespace>
    </PropertyGroup>
    <ItemGroup>
      <ProjectReference Include="..\..\Drivers\Driver\Driver.csproj" />
    </ItemGroup>
</Project>
<!-- IssJsonProtocol.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <AssemblyName>Qenex.QSuite.Examples.IssJsonProtocol</AssemblyName>
        <RootNamespace>Qenex.QSuite.Examples.IssJsonProtocol</RootNamespace>
    </PropertyGroup>
    <ItemGroup>
      <ProjectReference Include="..\..\Protocols\Protocol\Protocol.csproj" />
      <ProjectReference Include="..\..\Variables\VariableEvents\VariableEvents.csproj" />
    </ItemGroup>
</Project>

Driver: IssDriver.cs

Celý životní cyklus driveru na jednom místě: DefaultRawSettings jako šablona nastavení, stavový automat ve StartAsync/StopAsync a jedna smyčka, která v periodě volá API. Za povšimnutí stojí tři místa:

  • SetConfiguration čte nastavení společným SettingsParser se seznamem známých klíčů, takže překlep v Project Configuration skončí varováním v logu, ne tichým použitím výchozí hodnoty. Periodu zároveň omezuje na minimálně 1000 ms — jde o bezplatnou veřejnou službu a ohleduplnost k ní je součástí návrhu, nikoli poznámka pod čarou.
  • Chybu sítě řeší ReconnectPolicy stejně jako dodávané drivery: neúspěšný požadavek přepne driver do Faulted, po reconnectDelayMs se pokus opakuje a po vyčerpání reconnectAttempts se driver zastaví s důvodem ve stavu (-1 = zkoušet donekonečna, výchozí). Zastavený driver znovu spustí tlačítko Start v jeho Properties, ostatní drivery projektu běží dál. První úspěšná odpověď vrátí Running.
  • Send/SendAsync jsou prázdné. API je jen ke čtení, takže driver nemá co odesílat. Jak vypadá plná zápisová cesta, ukazuje ukázka teploměru.
using System.Reflection;
using Qenex.QSuite.Common.CoreComm;
using Qenex.QSuite.Drivers.Driver;
using Qenex.QSuite.LogSystems.LogSystem;
using Qenex.QSuite.Protocols.Protocol;
using Qenex.QSuite.Specifications.Specification;

namespace Qenex.QSuite.Examples.IssDriver;

/// <summary>
/// Example driver: reads live data from a public REST API — the current position of the
/// International Space Station. Every period it performs one HTTP GET and hands the raw JSON
/// response to its protocols; turning the JSON into variable values is the protocol's job
/// (see the IssJsonProtocol example).
/// This is about the smallest possible real-data driver: read-only, one fixed URL, one setting
/// of its own — plus the shared settings parser and reconnect policy every QENEX driver uses,
/// so the driver behaves in Project Configuration and at runtime like the built-in ones.
/// </summary>
public class IssDriver : DriverBase, ITransportSource<string>
{
    // Free, key-less API returning one flat JSON object with the current ISS position.
    // Be polite to the public service: do not poll faster than ~1 request per second.
    private const string Url = "https://api.wheretheiss.at/v1/satellites/25544";
    private const int MinPeriodMs = 1000;

    // The internet is a flaky transport: wait a few seconds before the next attempt.
    private const int DefaultReconnectDelayMs = 5000;

    private int periodMs = MinPeriodMs;
    private ReconnectPolicy reconnect = new(DefaultReconnectDelayMs);

    private HttpClient? httpClient;
    private CancellationTokenSource? runCts;
    private Task? runTask;

    public IssDriver()
    {
        Specification = new SpecificationBase
        {
            Name = "IssDriver",
            Label = "ISS Position (example)",
            Description = "Example driver polling the ISS position from a public REST API.",
            CreatedOn = new DateTime(2026, 7, 27),
            Version = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(1, 0, 0),
            Author = "Qenex",
            Company = "QENEX Ltd."
        };
    }

    #region Configuration

    // Shown pre-filled when the driver is added in Project Configuration. The reconnect part
    // (reconnectDelayMs / reconnectAttempts) comes from the shared template, so it reads the
    // same as in every built-in driver: -1 = retry forever, 0 = stop at the first failure,
    // N = give up after N consecutive failures.
    public override string DefaultRawSettings =>
        $"periodMs={MinPeriodMs};" + ReconnectPolicy.SettingsTemplate(DefaultReconnectDelayMs);

    private static readonly string[] KnownSettings =
    [
        "periodMs", ReconnectPolicy.ReconnectDelayKey, ReconnectPolicy.ReconnectAttemptsKey
    ];

    public override void SetConfiguration()
    {
        // The shared parser splits "key=value;..." and warns about every key it does not know —
        // a typo in Project Configuration is reported instead of being ignored silently.
        var settings = SettingsParser.Parse(RawSettings, KnownSettings, Logger, "ISS position driver");

        if (settings.TryGetValue("periodMs", out var periodText))
        {
            if (int.TryParse(periodText, out var parsedPeriod))
            {
                // The API asks for at most ~1 request per second, so slower is allowed, faster is not.
                periodMs = Math.Max(parsedPeriod, MinPeriodMs);
            }
            else
            {
                Logger?.Log(LogLevel.Warn, $"ISS position driver: invalid periodMs '{periodText}', using {periodMs} ms.");
            }
        }

        reconnect = ReconnectPolicy.Parse(settings, DefaultReconnectDelayMs, Logger, "ISS position driver");
    }

    #endregion

    #region Driver control

    public override async Task StartAsync(CancellationToken ct = default)
    {
        if (!IsEnabled)
        {
            SetState(CommunicationState.Disabled);
            return;
        }

        if (State == CommunicationState.Running || runTask is { IsCompleted: false })
        {
            return;
        }

        SetState(CommunicationState.Starting);

        // Short timeout so a slow or unreachable server cannot block the loop for long.
        httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };

        foreach (var protocol in Protocols)
        {
            await protocol.StartAsync(ct);
        }

        runCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        // Task.Run: the loop must not inherit the caller's (UI) SynchronizationContext —
        // a blocked dispatcher (window drag, busy UI) would stall the whole session.
        // Capture the token now: a racing StopAsync may null runCts before the loop starts.
        var runToken = runCts.Token;
        runTask = Task.Run(() => RunLoopAsync(runToken));
        SetState(CommunicationState.Running);
    }

    public override async Task StopAsync(CancellationToken ct = default)
    {
        SetState(CommunicationState.Stopping);

        if (runCts != null)
        {
            await runCts.CancelAsync();
        }

        if (runTask != null)
        {
            try
            {
                await runTask.WaitAsync(TimeSpan.FromSeconds(5), ct);
            }
            catch (OperationCanceledException)
            {
            }
            catch (TimeoutException)
            {
                Logger?.Log(LogLevel.Warn, "ISS position: the polling loop did not stop before timeout.");
            }
        }

        foreach (var protocol in Protocols)
        {
            await protocol.StopAsync(ct);
        }

        httpClient?.Dispose();
        httpClient = null;
        runCts?.Dispose();
        runCts = null;
        runTask = null;
        SetState(CommunicationState.Stopped);
    }

    public override void Dispose()
    {
        httpClient?.Dispose();
        runCts?.Dispose();
    }

    #endregion

    #region Communication

    // The API is read-only, so there is nothing to send. A writable driver would push the
    // encoded data to the device here (see the TempSensorDriver example).
    public override void Send<T>(T data)
    {
    }

    public override Task SendAsync<T>(T data, CancellationToken ct = default)
    {
        return Task.CompletedTask;
    }

    #endregion

    #region Polling loop

    private async Task RunLoopAsync(CancellationToken ct)
    {
        string? stopMessage = null;
        try
        {
            while (!ct.IsCancellationRequested)
            {
                try
                {
                    // One GET returns one JSON object carrying several signals at once
                    // (latitude, longitude, altitude, velocity, ...).
                    var json = await httpClient!.GetStringAsync(Url, ct);

                    // Back online after a failure: clear the failure counter and the Faulted state.
                    reconnect.ResetAfterSuccess();
                    if (State != CommunicationState.Running)
                    {
                        SetState(CommunicationState.Running);
                    }

                    foreach (var protocol in Protocols)
                    {
                        if (protocol is ProtocolBase<string> textProtocol)
                        {
                            await textProtocol.AddReceivedDataToQueueAsync([json], ct);
                        }
                    }

                    await Task.Delay(periodMs, ct);
                }
                catch (Exception e) when (e is HttpRequestException or TaskCanceledException && !ct.IsCancellationRequested)
                {
                    // Network hiccups are normal with a public API. The reconnect policy decides
                    // whether to keep trying (Faulted, retry after reconnectDelayMs) or to give up
                    // (Stopped with the reason; the Start button in the driver's Properties restarts it).
                    if (!reconnect.RegisterFailure())
                    {
                        stopMessage = reconnect.GiveUpMessage(e.Message);
                        Logger?.Log(LogLevel.Error, $"ISS position driver '{Label}' {stopMessage}");
                        break;
                    }

                    Logger?.Log(reconnect.FailureLogLevel(),
                        $"ISS position driver '{Label}' request failed ({reconnect.AttemptText}): {e.Message} "
                        + $"Retrying in {reconnect.ReconnectDelayMs} ms.");
                    SetState(CommunicationState.Faulted, e.Message);
                    await Task.Delay(reconnect.ReconnectDelayMs, ct);
                }
            }
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // Normal stop.
        }
        catch (Exception e)
        {
            Logger?.Log(LogLevel.Error, $"ISS position loop failed: {e.Message}");
            SetState(CommunicationState.Faulted, e.Message);
            return;
        }

        if (stopMessage != null)
        {
            // Gave up: stop the protocols and keep the reason visible in the driver state.
            foreach (var protocol in Protocols)
            {
                await protocol.StopAsync(CancellationToken.None);
            }

            SetState(CommunicationState.Stopped, stopMessage);
        }
    }

    #endregion
}

Protokol: IssJsonProtocol.cs

Protokol je ještě kratší než driver, a to hlavně díky jednomu rozhodnutí: nemá vlastní frontu ani worker vlákno. Data přicházejí jednou za sekundu, takže je dekóduje přímo v AddReceivedDataToQueueAsync — na vlákně smyčky driveru. StartAsync/StopAsync pak jen přepínají stav. U rychlých sběrnic by takový přístup nebyl vhodný (driver nesmí čekat na zpracování); jak vypadá plnohodnotná fronta s vlastním konzumentem, ukazuje ukázka teploměru.

Dekódování v Decode je přímočaré: rozparsovat dokument, a každé proměnné, jejíž path ukazuje na číselné pole, nastavit hodnotu. Encode vrací prázdnou sekvenci — REST API je pouze ke čtení, takže není co kódovat.

using System.Reflection;
using System.Text.Json;
using Qenex.QSuite.Common.CoreComm;
using Qenex.QSuite.LogSystems.LogSystem;
using Qenex.QSuite.Protocols.Protocol;
using Qenex.QSuite.Specifications.Specification;
using Qenex.QSuite.Variables.QVariables;
using Qenex.QSuite.Variables.QVariables.Values;
using Qenex.QSuite.Variables.VariableEvents;

namespace Qenex.QSuite.Examples.IssJsonProtocol;

/// <summary>
/// Example protocol: picks numeric fields out of the received ISS position JSON by name. Each
/// variable states which field it wants via the "path" commParam (path="latitude"), so one JSON
/// document can feed many variables at once. Nothing here is ISS-specific except the name — the
/// same code would decode any flat JSON object — but as an example it is paired with the IssDriver.
/// </summary>
public class IssJsonProtocol : ProtocolBase<string>
{
    public IssJsonProtocol()
    {
        Specification = new SpecificationBase
        {
            Name = "IssJsonProtocol",
            Label = "ISS JSON Protocol (example)",
            Description = "Example protocol decoding numeric fields of the ISS position JSON into variables.",
            CreatedOn = new DateTime(2026, 7, 27),
            Version = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(1, 0, 0),
            Author = "Qenex",
            Company = "QENEX Ltd."
        };
    }

    // The transport type (string) alone cannot tell text-line drivers apart, and this
    // protocol only understands the ISS position JSON, so it narrows itself to its driver.
    public override IReadOnlyList<string> CompatibleDrivers => ["IssDriver"];

    #region Configuration

    // The protocol has no protocol-level settings; everything is per-variable commParams.
    // The shared parser still runs so that a key typed into the protocol settings is reported
    // as unknown instead of being ignored silently.
    public override void SetConfiguration()
    {
        SettingsParser.Parse(RawSettings, [], Logger, "ISS JSON protocol");
    }

    public override string CreateDefaultCommParam(IVariableBase variable, IEnumerable<IVarEvent> variableEvents)
    {
        return "path=\"latitude\"";
    }

    #endregion

    #region Protocol variables

    public override IProtocolVariable? CreateProtocolVariable(IVariableBase variable, string commParams, bool isCommunicated)
    {
        try
        {
            return new ProtocolVariable
            {
                Variable = variable,
                IsCommunicated = isCommunicated,
                ProtocolVariableSpecification = IssJsonVariableSpecification.Create(commParams)
            };
        }
        catch (Exception e)
        {
            Logger?.Log(LogLevel.Warn, $"Protocol variable specification for variable {variable.Name} could not be created ({e.Message}).");
            return null;
        }
    }

    public override IProtocolVariable? CreateProtocolVariable(IVariableBase variable, IVarEvent variableEvent, string id)
    {
        return CreateProtocolVariable(variable, $"path=\"{id}\"", true);
    }

    public override IProtocolVariable? CreateProtocolVariable(IVariableBase variable, IEnumerable<IVarEvent> variableEvents,
        string commParams, bool isCommunicated)
    {
        // The data arrive whenever the driver polls them, so variable events are not used.
        return CreateProtocolVariable(variable, commParams, isCommunicated);
    }

    #endregion

    #region Protocol control

    // No worker loop is needed: received documents are decoded directly in
    // AddReceivedDataToQueueAsync, so start/stop only maintain the state machine.
    // (For a queued, cancelable consumer loop see the TempSensorProtocol example.)
    public override Task StartAsync(CancellationToken ct = default)
    {
        SetState(IsEnabled ? CommunicationState.Running : CommunicationState.Disabled);
        return Task.CompletedTask;
    }

    public override Task StopAsync(CancellationToken ct = default)
    {
        SetState(CommunicationState.Stopped);
        return Task.CompletedTask;
    }

    public override void Dispose()
    {
    }

    #endregion

    #region Received data

    // At ~1 document per second there is no need for an internal queue — the data are
    // decoded right away on the driver's polling task.
    public override async Task AddReceivedDataToQueueAsync(IEnumerable<string> data, CancellationToken ct = default)
    {
        if (State != CommunicationState.Running)
        {
            return;
        }

        await ProcessReceivedDataAsync(data, ct);
    }

    protected override void ProcessReceivedData(IEnumerable<string> data)
    {
        foreach (var protocolVariable in Decode(data))
        {
            protocolVariable.NotifyValueChanged();
        }
    }

    protected override async Task ProcessReceivedDataAsync(IEnumerable<string> data, CancellationToken ct = default)
    {
        var notifyTasks = Decode(data).Select(protocolVariable => protocolVariable.NotifyValueChangedAsync());
        await Task.WhenAll(notifyTasks);
    }

    #endregion

    #region Encoding and decoding

    protected override IEnumerable<IProtocolVariable> Decode(IEnumerable<string> data)
    {
        var updated = new List<IProtocolVariable>();

        foreach (var json in data)
        {
            JsonDocument document;
            try
            {
                document = JsonDocument.Parse(json);
            }
            catch (JsonException e)
            {
                Logger?.Log(LogLevel.Warn, $"ISS JSON protocol: cannot parse received data ({e.Message}).");
                continue;
            }

            using (document)
            {
                // Every variable whose "path" names a numeric field of this document gets its value.
                foreach (var protocolVariable in Variables)
                {
                    if (!protocolVariable.IsCommunicated
                        || protocolVariable.ProtocolVariableSpecification is not IssJsonVariableSpecification spec
                        || protocolVariable.Variable is not ScalarVariable scalarVariable
                        || !document.RootElement.TryGetProperty(spec.Path, out var field)
                        || field.ValueKind != JsonValueKind.Number)
                    {
                        continue;
                    }

                    if (!TrySetValue(scalarVariable, field.GetDouble()))
                    {
                        Logger?.Log(LogLevel.Warn, $"ISS JSON protocol: variable '{scalarVariable.Name}' has an unsupported value type.");
                        continue;
                    }

                    scalarVariable.Timestamp = DateTime.UtcNow;
                    updated.Add(protocolVariable);
                }
            }
        }

        return updated;
    }

    // The protocol is read-only (a REST API you can only GET), so nothing is ever encoded.
    // For the write direction see the TempSensorProtocol example.
    protected override IEnumerable<string> Encode(IEnumerable<IProtocolVariable> protocolVariables)
    {
        yield break;
    }

    // Values<T>.SetValue does not convert, so the received double is converted to the
    // variable's type here; the example supports the common numeric types.
    private static bool TrySetValue(ScalarVariable scalarVariable, double value)
    {
        switch (scalarVariable.Values)
        {
            case Values<double> doubleValues:
                doubleValues.Value = value;
                return true;
            case Values<float> floatValues:
                floatValues.Value = (float)value;
                return true;
            case Values<int> intValues:
                intValues.Value = (int)Math.Round(value);
                return true;
            default:
                return false;
        }
    }

    #endregion
}

Specifikace proměnné: IssJsonVariableSpecification.cs

Adresování je zde jediné: jméno JSON pole. Property Path odpovídá klíči commParamu 1:1, takže zděděná serializace ToCommParam() funguje bez přepsání — stejný postup jako v ukázce teploměru.

using Qenex.QSuite.Protocols.Protocol;

namespace Qenex.QSuite.Examples.IssJsonProtocol;

/// <summary>
/// Addressing of one variable: the name of the JSON field whose value the variable receives.
/// The property matches the commParam key 1:1 ("path"), so the inherited reflection-based
/// ToCommParam() round-trips the specification without an override.
/// </summary>
public class IssJsonVariableSpecification : ProtVariableSpecification
{
    public IssJsonVariableSpecification()
    {
        Name = "IssJsonVariableSpecification";
    }

    /// <summary>Name of the field in the received JSON object, e.g. "latitude". Case-sensitive.</summary>
    public string Path { get; set; } = string.Empty;

    public static IssJsonVariableSpecification Create(string commParams)
    {
        var parameters = commParams
            .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(parameter => parameter.Split('=', 2))
            .Where(parts => parts.Length == 2)
            .ToDictionary(parts => parts[0].Trim(), parts => parts[1].Trim().Trim('"'), StringComparer.OrdinalIgnoreCase);

        if (!parameters.TryGetValue("path", out var path) || string.IsNullOrWhiteSpace(path))
        {
            throw new ArgumentException("Missing mandatory commParam 'path'.");
        }

        return new IssJsonVariableSpecification { Path = path };
    }
}

Jak spolu členy souvisí

KrokKdoČleny
Přidání driveru/protokolu v UIpluginDefaultRawSettings, CreateDefaultCommParam
Přiřazení proměnnéprotokolCreateProtocolVariable → Create specifikace
Start runtimedriverStartAsync → start protokolů → spuštění smyčky
Čtení datdriver → protokolHTTP GET → AddReceivedDataToQueueAsync → Decode → NotifyValueChangedAsync
StopdriverStopAsync (zrušení smyčky, stop protokolů, úklid HttpClient)

Co ukázka záměrně vynechává

Aby zůstala co nejkratší, tři věci v ní záměrně chybí — a všechny tři najdete v ukázce teploměru:

  • zápisová cesta (operátor zapíše hodnotu → protokol ji zakóduje → driver odešle),
  • fronta přijatých dat s vlastním konzumentem — nezbytná všude, kde data přicházejí rychleji, než je lze zpracovat,
  • směr komunikace v commParams (direction="read/write") — zde se pouze čte, takže parametr není potřeba.

Poznámka na závěr: kromě jména na protokolu není nic specifického pro ISS. Stejný kód by dekódoval libovolný plochý JSON objekt — stačí jiná URL v driveru a jiné hodnoty path.

Dále

Ukázka teploměr — plný vzor včetně zápisu a fronty. Kostry jednotlivých částí: Vlastní driver a Vlastní protokol.