Első lépések
Az első két Komod
Ebben az oktatóanyagban két Komodot készítesz, amelyek a KOFA gráfban egymásnak adják az adatot:
┌───────────────┐ temperature (Scalar, °C) ┌───────────────┐ heater (BoolValue)
│ Hőmérő │ ─────────────────────────▶ │ Termosztát │ ───────────────────▶ …
│ (szimulált) │ │ + dashboard │
└───────────────┘ │ csempe │
└───────────────┘
- A hőmérő szinuszosan változó, zajos hőmérsékletet szimulál, és a
temperaturekimenetén adja. - A termosztát a
temperaturebemenetére érkező értékből dönti el, hogy a fűtés be- vagy kikapcsoljon (hiszterézissel), és aheaterkimenetén jelzi. Ha a hőmérő elnémul, biztonsági állapotba (fűtés ki) kapcsol. - A termosztát csempét is ad a dashboardra: mutatja a hőmérsékletet és a fűtést, és a cél hőmérséklet állítható rajta.
Előfeltétel: az eszközök telepítve vannak.
1. A projekt létrehozása
dotnet new kofa-komod-quickstart -n Acme.Demo --authorName "Acme Kft."
cd Acme.Demo
A -n után a saját neved áll (Cég.Projekt alakban). Visual Studióban az Új projekt ablakban a „KOFA Komod példa” sablon ugyanezt csinálja. A létrejött mappa:
Acme.Demo/
Acme.Demo.slnx
nuget.config a KOFA NuGet forrás
Acme.Demo.Thermometer/ a hőmérő (szerver rész)
ThermometerKomod.cs
Package.props a csomag azonosítója, verziója, neve
Acme.Demo.Thermostat/ a termosztát (szerver rész)
ThermostatKomod.cs
Package.props
Acme.Demo.Thermostat.Ui/ a termosztát csempéje (böngésző rész)
ThermostatWidget.razor
Egy Komod csomag két projektből állhat: a szerveren futó X és a böngészőben futó X.Ui projektből, amelyek közös Package.props-ot használnak. A hőmérőnek nincs felülete, ezért csak szerver projektje van.
2. A hőmérő
Beállítások
A beállítás egy egyszerű rekord. A KOFA ebből készít űrlapot, ellenőrzi az attribútumokat ([Range] stb.), és JSON-ként tárolja.
public sealed record ThermometerSettings
{
[Range(-50, 150)]
public double BaseCelsius { get; init; } = 21;
[Range(0, 50)]
public double AmplitudeCelsius { get; init; } = 4;
[Range(1, 3600)]
public double PeriodSeconds { get; init; } = 60;
[Range(0, 5)]
public double NoiseCelsius { get; init; } = 0.2;
[Range(0.1, 20)]
public double RateHz { get; init; } = 2;
}
A Komod
[Komod("acme.demo.thermometer",
DisplayName = "Hőmérő (szimulált)",
Description = "Szimulált hőmérő: szinuszos hőmérsékletet és zajt ad a kimenetén.",
Category = KomodCategory.Source)]
public sealed class ThermometerKomod : KomodBase<ThermometerSettings>
{
[Output("temperature", DisplayName = "Hőmérséklet", Description = "°C")]
public required IOutput<Scalar> Temperature { get; init; }
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
var started = Context.TimeProvider.GetTimestamp();
// Settings are re-read in every round: a settings change applies without restarting the instance.
while (!cancellationToken.IsCancellationRequested)
{
var settings = Settings;
var elapsed = Context.TimeProvider.GetElapsedTime(started).TotalSeconds;
var value = settings.BaseCelsius
+ settings.AmplitudeCelsius * Math.Sin(2 * Math.PI * elapsed / settings.PeriodSeconds)
+ (Random.Shared.NextDouble() * 2 - 1) * settings.NoiseCelsius;
Temperature.Publish(new Scalar(Math.Round(value, 2), "°C"));
Context.ReportStatus(KomodHealth.Ok, $"{value:0.0} °C");
try
{
await Task.Delay(TimeSpan.FromSeconds(1 / settings.RateHz), Context.TimeProvider, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
}
}
// True: the new settings are already in use (see above), no restart needed.
protected override Task<bool> OnSettingsChangedAsync(ThermometerSettings oldSettings, ThermometerSettings newSettings) =>
Task.FromResult(true);
}
Mi történik itt:
- A
[Komod("…")]attribútum azonosítja az osztályt. Az azonosító kisbetűs, ponttal vagy kötőjellel tagolt, és verziók között nem változhat: ezzel hivatkozik rá a gráf és a csomag. ACategorya Komod tárbeli és palettabeli besorolása. - A
[Output("temperature")]property egy kimeneti port. A KOFA tölti ki betöltéskor (required … init), te csakPublish-olsz bele. AScalaregy általános mérőszám mértékegységgel; a többi adattípus is hasonlóan használható. - Az
ExecuteAsynca fő ciklus: addig fut, amíg a példány le nem áll (cancellationToken). Visszatérhet korán is (passzív Komod). - A
Settingsmindig az aktuális beállítás. A ciklus minden körben újra olvassa, ezértOnSettingsChangedAsynctrue-t ad: a változás újraindítás nélkül érvényesül.falseesetén a KOFA újraindítja a példányt. Context.ReportStatusa gráf csomópontján és a dashboardon megjelenő állapotsor.
3. A termosztát
Beállítások
public sealed record ThermostatSettings
{
[Range(5, 35)]
public double TargetCelsius { get; init; } = 21;
[Range(0.1, 5)]
public double HysteresisCelsius { get; init; } = 0.5;
}
A Komod
[Komod("acme.demo.thermostat",
DisplayName = "Termosztát",
Description = "Hiszterézises termosztát: a hőmérséklet alapján kapcsolja a fűtést.",
Category = KomodCategory.Controller)]
public sealed class ThermostatKomod : KomodBase<ThermostatSettings>
{
// The input must be linked in the graph. If no message arrives for 5 seconds, OnInputTimeout runs (failsafe).
[Input("temperature", DisplayName = "Hőmérséklet", Deadline = "00:00:05")]
public required IInput<Scalar> Temperature { get; init; }
[Output("heater", DisplayName = "Fűtés")]
public required IOutput<BoolValue> Heater { get; init; }
private bool _heating;
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Context.ReportStatus(KomodHealth.Warning, "Nincs adat");
SetHeater(false);
await foreach (var message in Temperature.ReadAllAsync(cancellationToken))
{
var settings = Settings;
var celsius = message.Value.Value;
if (celsius < settings.TargetCelsius - settings.HysteresisCelsius)
SetHeater(true);
else if (celsius > settings.TargetCelsius + settings.HysteresisCelsius)
SetHeater(false);
Context.ReportStatus(KomodHealth.Ok, $"{celsius:0.0} °C → cél {settings.TargetCelsius:0.#} °C, fűtés {(_heating ? "BE" : "KI")}");
}
}
// The deadline elapsed: with no measurement the safe state is "off".
protected override void OnInputTimeout(IInputPort port)
{
SetHeater(false);
Context.ReportStatus(KomodHealth.Warning, "Nincs hőmérséklet adat, a fűtés kikapcsolt");
}
protected override Task<bool> OnSettingsChangedAsync(ThermostatSettings oldSettings, ThermostatSettings newSettings) =>
Task.FromResult(true);
private void SetHeater(bool on)
{
_heating = on;
Heater.Publish(new BoolValue(on));
}
}
Mi történik itt:
- A
[Input("temperature", Deadline = "00:00:05")]egy bemeneti port. AzIInput<T>ReadAllAsyncmetódusa aszinkron folyamként adja a beérkező üzeneteket. Az üzenet (KofaMessage<T>) aValuemellett időbélyeget és forrást is hordoz. - A
Deadlinea failsafe alapja: ha 5 másodpercig nem érkezik üzenet, a KOFA meghívja azOnInputTimeout-ot. Itt a fűtést kikapcsoljuk. Minden olyan Komodnak, amely valamit működtet, kell határidő; részletek: Életciklus és biztonság. - A hiszterézis megakadályozza, hogy a fűtés a célérték körül gyorsan ki-be kapcsoljon.
4. A csempe
A böngésző rész egy közönséges Blazor komponens, amelyet a KOFA futás közben tölt be. A [KomodView] attribútum köti a Komodhoz (azonosító) és a helyhez (Widget = dashboard csempe).
<div style="display:flex;flex-direction:column;gap:6px;font-variant-numeric:tabular-nums">
<div style="font-size:1.8rem;font-weight:600">
@(_temperature is { } t ? t.ToString("0.0", CultureInfo.InvariantCulture) + " °C" : "–")
</div>
<div style="display:flex;align-items:center;gap:8px">
<button type="button" @onclick="() => AdjustTargetAsync(-0.5)">−</button>
<span>cél: @(_target?.ToString("0.0", CultureInfo.InvariantCulture) ?? "–") °C</span>
<button type="button" @onclick="() => AdjustTargetAsync(+0.5)">+</button>
</div>
<div style="font-size:.85rem;opacity:.8">Fűtés: @(_heating ? "BE" : "KI")</div>
@if (_error is not null)
{
<div style="font-size:.8rem;color:#c42b1c">@_error</div>
}
</div>
Az élő adat a szerverről jön:
protected override async Task OnInitializedAsync()
{
_target = ReadTarget(await Kofa.GetSettingsAsync(InstanceId, _cts.Token));
// Both an input and an output port can be subscribed; the server thins the stream to maxRateHz.
_loops.Add(Task.Run(async () =>
{
await foreach (var message in Kofa.SubscribeAsync<Scalar>(Port("temperature"), maxRateHz: 5, _cts.Token))
{
_temperature = message.Value.Value;
await InvokeAsync(StateHasChanged);
}
}));
_loops.Add(Task.Run(async () =>
{
await foreach (var message in Kofa.SubscribeAsync<BoolValue>(Port("heater"), maxRateHz: 5, _cts.Token))
{
_heating = message.Value.Value;
await InvokeAsync(StateHasChanged);
}
}));
}
A cél hőmérséklet állítása a példány beállításának mentése. A szerver ellenőrzi az értéket, élőben alkalmazza (ezért kellett a termosztátnál OnSettingsChangedAsync → true), és beírja az aktív gráfba is:
private async Task AdjustTargetAsync(double delta)
{
try
{
// The server validates the settings, applies them live and writes them into the active graph too.
var settings = JsonNode.Parse((await Kofa.GetSettingsAsync(InstanceId)).GetRawText())!.AsObject();
_target = Math.Round((ReadTarget(JsonSerializer.SerializeToElement(settings)) ?? 21) + delta, 1);
settings["targetCelsius"] = _target;
await Kofa.SaveSettingsAsync(InstanceId, JsonSerializer.SerializeToElement(settings));
_error = null;
}
catch (KomodClientException ex)
{
_error = ex.Message;
}
}
Részletek, további nézettípusok és a stílusozás: Felület (UI).
5. Build és csomagolás
dotnet build Acme.Demo.slnx
A build a csomagokat kicsomagolva az artifacts/komods/<id>/<verzió>/ mappába teszi. Telepíthető .komod fájlt így készítesz (előbb a UI projektet buildeld, mert a szerver csomag erre hivatkozik):
dotnet build Acme.Demo.Thermostat.Ui
dotnet build Acme.Demo.Thermostat -p:KomodPack=true
dotnet build Acme.Demo.Thermometer -p:KomodPack=true
A fájlok az artifacts/packages/ mappába kerülnek (acme.demo.thermometer-1.0.0.komod, acme.demo.thermostat-1.0.0.komod). Ellenőrzés a kofa-pack eszközzel:
kofa-pack inspect artifacts/packages/acme.demo.thermostat-1.0.0.komod
Kiírja a Komodokat és a portjaikat (be temperature kofa.scalar/1, ki heater kofa.bool/1), így látod, hogy a KOFA mit fog látni.
6. Telepítés és összekötés
- A KOFA-ban nyisd meg a Komodok oldalt (admin fiókkal), és húzd rá a két
.komodfájlt a feltöltési mezőre. Az előnézet a manifestet és a portokat mutatja; telepítsd mindkettőt. - A Gráf oldalon húzd a vászonra a Hőmérő (szimulált) és a Termosztát Komodot a palettáról.
- Kösd a hőmérő
Hőmérsékletkimenetét a termosztátHőmérsékletbemenetére. - Nyomd meg az Alkalmaz gombot. A csomópontokon megjelenik az állapotsor, a kötésen az élő érték.
- A termosztát csempéjét add a dashboardhoz. Állítsd a célt a −/+ gombokkal, és figyeld a fűtés kapcsolását.
Tipp
Ha módosítasz a kódon: emeld a Package.props KomodPackageVersion értékét, csomagold újra és töltsd fel. Fejlesztés közben gyorsabb, ha a KOFA szerver az artifacts/komods mappát figyeli, mert ott elég az Újratöltés gomb; lásd: Build és csomagolás.
Hogyan tovább
- Több Komod, a fenti elemekből: Alapfogalmak.
- Valódi hardver (I2C szenzor, kamera) a szimulált hőmérő helyett: Hardver.
- Gomb vagy kar a böngészőben, amely a Komodot vezérli: UI bemenet.
- Közzététel a Komod tárban: Telepítés és közzététel.