Kompletní ukázka: teploměr (driver + protokol)
Ukázka ISS předvedla nejmenší možnou dvojici driver + protokol — zde je její protipól: kompletní, přeložitelná a funkční dvojice, driver simulující vícekanálový teploměr a protokol, který jeho data dekóduje do proměnných. Ukázka záměrně využívá všechny členy kontraktu včetně zápisové cesty, a přitom nepotřebuje žádný hardware (ani internet):
- TempSensorDriver zastupuje zařízení: v každé periodě vytvoří pro každý
kanál textový řádek
ch1=23.47(teplota pomalu kolísá) a předá ho protokolům. - TempSensorProtocol řádky dekóduje: proměnná adresuje kanál commParamem
channel="ch1", hodnota se převede na typ proměnné a notifikují se odběratelé (graf, logger, skripty). - Zápis: proměnná s
direction="write"projde cestou UI →Encode→ transmitter vložený driverem (ApplyWriteAsync), který kanál „přeladí“ na zapsanou hodnotu — v grafu je s dalším vzorkem patrná skoková změna a další kolísání kolem nové hodnoty.
Jak si ukázku vyzkoušet
- Oba pluginy už dodává instalátor
(
Qenex.QSuite.Examples.TempSensorDriver.dllve složceDrivers,Qenex.QSuite.Examples.TempSensorProtocol.dllve složceProtocolsvedle QInsight.exe). Přeložíte-li si vlastní upravenou kopii (projekty níže), nahraďte jí tyto DLL. - V Project Configuration přidejte driver Temperature Sensor
(example) — předvyplní se
periodMs=500;channels=2— a k němu protokol Temperature Sensor Protocol (example). Protokol svůj driver jmenuje vCompatibleDrivers(viz níže), takže dvojice se nabízí jen spolu. - Vytvořte proměnné, např.
Temp1(double),Temp2(double) aTempSet(double), a přiřaďte je protokolu s commParams:Temp1:direction="read";channel="ch1"Temp2:direction="read";channel="ch2"TempSet:direction="write";channel="ch1"
- Spusťte runtime: hodnoty
Temp1/Temp2přicházejí do grafu. Zapište přes control ve Write Mode (Single-Signal nebo Watch Table, viz Zápis do zařízení) doTempSetnapříklad50— teplota kanálu ch1 se změní na 50 °C. Control ukazuje jen hodnoty ze zařízení, takže novou teplotu uvidíte vTemp1, ne jako ozvěnu vTempSet.
Projekty
Dva běžné class-library projekty; referencují jen základní projekty QSuite
(Driver, resp. Protocol + VariableEvents).
<!-- TempSensorDriver.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Qenex.QSuite.Examples.TempSensorDriver</AssemblyName>
<RootNamespace>Qenex.QSuite.Examples.TempSensorDriver</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Drivers\Driver\Driver.csproj" />
</ItemGroup>
</Project>
<!-- TempSensorProtocol.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Qenex.QSuite.Examples.TempSensorProtocol</AssemblyName>
<RootNamespace>Qenex.QSuite.Examples.TempSensorProtocol</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Protocols\Protocol\Protocol.csproj" />
<ProjectReference Include="..\..\Variables\VariableEvents\VariableEvents.csproj" />
</ItemGroup>
</Project>
Hotový projekt s touto dvojicí pluginů je v instalátoru jako
Qenex_SimulTempMeasure.qproj, popsaný v příkladu
Simulovaný teploměr.
Driver: TempSensorDriver.cs
Za povšimnutí: ITransportSource<string> (driver předává protokolům
textové řádky), DefaultRawSettings (šablona nastavení) a SettingsParser
v SetConfiguration (neznámý klíč se hlásí varováním), životní cyklus se
stavy, SetTransmitters (injektáž zápisové cesty do protokolů) a delegace
přes IProtocolVariableCommandDriver — zápis operátora míří na
IProtocolVariableWriteProtocol protokolu, čtení On Request na
IProtocolVariableReadProtocol. To tento protokol neimplementuje
(zařízení hodnoty jen posílá), takže CanRequestRead vrací false
a tlačítko Read zůstane pro jeho proměnné neaktivní.
using System.Globalization;
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.TempSensorDriver;
/// <summary>
/// Example driver: simulates a temperature sensor device with a few channels. Every period it
/// produces one text line per channel ("ch1=23.47") and hands the lines to its protocols.
/// An operator write coming back through Send re-bases the channel's temperature, so the whole
/// read/write chain can be tried without any hardware.
/// </summary>
public class TempSensorDriver : DriverBase, IProtocolVariableCommandDriver, ITransportSource<string>
{
private int periodMs = 500;
private int channelCount = 2;
// The simulated device state: current temperature of every channel.
private readonly object channelLock = new();
private double[] channelTemps = [];
private readonly Random random = new();
private volatile bool exitRequested;
private CancellationTokenSource? runCts;
private Task? runTask;
public TempSensorDriver()
{
Specification = new SpecificationBase
{
Name = "TempSensorDriver",
Label = "Temperature Sensor (example)",
Description = "Example driver simulating a multi-channel temperature sensor.",
CreatedOn = new DateTime(2026, 7, 26),
Version = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(1, 0, 0),
Author = "Qenex",
Company = "QENEX Ltd."
};
}
#region Configuration
public override string DefaultRawSettings => "periodMs=500;channels=2";
private static readonly string[] KnownSettings = ["periodMs", "channels"];
public override void SetConfiguration()
{
// The shared parser splits "key=value;..." and warns about every key it does not know,
// the same way the built-in drivers do. A missing key keeps the default; an invalid
// value is reported and the default stays.
var settings = SettingsParser.Parse(RawSettings, KnownSettings, Logger, "Temperature sensor driver");
if (settings.TryGetValue("periodMs", out var periodText))
{
if (int.TryParse(periodText, out var parsedPeriod) && parsedPeriod > 0)
{
periodMs = parsedPeriod;
}
else
{
Logger?.Log(LogLevel.Warn, $"Temperature sensor driver: invalid periodMs '{periodText}', using {periodMs} ms.");
}
}
if (settings.TryGetValue("channels", out var channelsText))
{
if (int.TryParse(channelsText, out var parsedChannels))
{
channelCount = Math.Clamp(parsedChannels, 1, 16);
}
else
{
Logger?.Log(LogLevel.Warn, $"Temperature sensor driver: invalid channels '{channelsText}', using {channelCount}.");
}
}
}
#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);
exitRequested = false;
// Every channel starts at a slightly different temperature.
lock (channelLock)
{
channelTemps = Enumerable.Range(0, channelCount).Select(i => 20.0 + 2.0 * i).ToArray();
}
// Give write-capable protocols a TX path into this driver before they start.
SetTransmitters(ApplyWriteAsync);
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);
exitRequested = true;
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, "Temp sensor: the sampling loop did not stop before timeout.");
}
}
foreach (var protocol in Protocols)
{
await protocol.StopAsync(ct);
}
SetTransmitters(null);
runCts?.Dispose();
runCts = null;
runTask = null;
SetState(CommunicationState.Stopped);
}
public override void Dispose()
{
runCts?.Dispose();
}
#endregion
#region Communication
// The protocol encoded an operator write into a "ch1=50" line — apply it to the device.
public override void Send<T>(T data)
{
if (data is string line)
{
ApplyWrite(line);
}
}
public override Task SendAsync<T>(T data, CancellationToken ct = default)
{
Send(data);
return Task.CompletedTask;
}
// Operator writes: delegated to the owning protocol (same pattern as the CAN and serial drivers).
public bool CanSendCommand(IProtocolVariable protocolVariable)
{
return Protocols
.OfType<IProtocolVariableWriteProtocol>()
.Any(protocol => protocol.CanWriteVariable(protocolVariable));
}
public async Task OnProtocolVariableCommandAsync(IProtocolVariable protocolVariable, CancellationToken ct = default)
{
foreach (var protocol in Protocols.OfType<IProtocolVariableWriteProtocol>())
{
if (!protocol.CanWriteVariable(protocolVariable))
{
continue;
}
await protocol.WriteVariableAsync(protocolVariable, ct);
return;
}
}
// On-request reads: the module wires read-requested notifications to this driver; the driver
// delegates to the owning protocol (IProtocolVariableReadProtocol), which reads the variable
// once over this driver's transport. Failures propagate so the requesting control sees them.
public bool CanRequestRead(IProtocolVariable protocolVariable)
{
return Protocols
.OfType<IProtocolVariableReadProtocol>()
.Any(protocol => protocol.CanReadVariable(protocolVariable));
}
public async Task OnProtocolVariableReadRequestAsync(IProtocolVariable protocolVariable, CancellationToken ct = default)
{
foreach (var protocol in Protocols.OfType<IProtocolVariableReadProtocol>())
{
if (!protocol.CanReadVariable(protocolVariable))
{
continue;
}
await protocol.ReadVariableAsync(protocolVariable, ct);
return;
}
throw new InvalidOperationException(
$"No protocol of this driver can read variable '{protocolVariable.Variable?.Name}' on request.");
}
#endregion
#region Simulated device
private async Task RunLoopAsync(CancellationToken ct)
{
try
{
while (!ct.IsCancellationRequested && !exitRequested)
{
var lines = new List<string>(channelCount);
lock (channelLock)
{
for (var i = 0; i < channelTemps.Length; i++)
{
// The temperature drifts slowly around its current base.
channelTemps[i] += 0.1 * (random.NextDouble() * 2.0 - 1.0);
lines.Add($"ch{i + 1}={channelTemps[i].ToString("F2", CultureInfo.InvariantCulture)}");
}
}
foreach (var protocol in Protocols)
{
if (protocol is ProtocolBase<string> textProtocol)
{
await textProtocol.AddReceivedDataToQueueAsync(lines, ct);
}
}
await Task.Delay(periodMs, ct);
}
}
catch (OperationCanceledException) when (ct.IsCancellationRequested || exitRequested)
{
// Normal stop.
}
catch (Exception e)
{
Logger?.Log(LogLevel.Error, $"Temp sensor loop failed: {e.Message}");
SetState(CommunicationState.Faulted, e.Message);
}
}
private Task ApplyWriteAsync(string line, CancellationToken ct)
{
ApplyWrite(line);
return Task.CompletedTask;
}
private void ApplyWrite(string line)
{
var pair = line.Split('=', 2, StringSplitOptions.TrimEntries);
if (pair.Length != 2
|| !pair[0].StartsWith("ch", StringComparison.OrdinalIgnoreCase)
|| !int.TryParse(pair[0][2..], out var channelNumber)
|| !double.TryParse(pair[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
{
Logger?.Log(LogLevel.Warn, $"Temp sensor: cannot apply write '{line}'.");
return;
}
lock (channelLock)
{
var index = channelNumber - 1;
if (index < 0 || index >= channelTemps.Length)
{
Logger?.Log(LogLevel.Warn, $"Temp sensor: channel '{pair[0]}' does not exist.");
return;
}
channelTemps[index] = value;
}
Logger?.Log(LogLevel.Info, $"Temp sensor: channel {pair[0]} re-based to {pair[1]}.");
}
private void SetTransmitters(Func<string, CancellationToken, Task>? transmitter)
{
foreach (var protocol in Protocols)
{
if (protocol is ITransportProtocol<string> transportProtocol)
{
transportProtocol.SetTransmitter(transmitter);
}
}
}
#endregion
}
Protokol: TempSensorProtocol.cs
Za povšimnutí: CreateDefaultCommParam (šablona commParams z pluginu),
fronta přijatých dat (System.Threading.Channels), Decode/Encode
a zápisová rozhraní ITransportProtocol<string>
IProtocolVariableWriteProtocol.
using System.Globalization;
using System.Reflection;
using System.Threading.Channels;
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.TempSensorProtocol;
/// <summary>
/// Example protocol: decodes "ch1=23.47" text lines from the TempSensorDriver into variables
/// addressed by the "channel" commParam, and encodes operator writes back into the same lines.
/// The generic parameter (string) is what pairs the protocol with a text-line driver.
/// </summary>
public class TempSensorProtocol : ProtocolBase<string>, ITransportProtocol<string>, IProtocolVariableWriteProtocol
{
// Received lines waiting for processing; the channel keeps the consumer loop cancelable.
private Channel<string>? receivedLines;
private volatile Func<string, CancellationToken, Task>? transmitter;
private volatile bool exitRequested;
private CancellationTokenSource? runCts;
private Task? runTask;
public TempSensorProtocol()
{
Specification = new SpecificationBase
{
Name = "TempSensorProtocol",
Label = "Temperature Sensor Protocol (example)",
Description = "Example protocol decoding text lines of the simulated temperature sensor.",
CreatedOn = new DateTime(2026, 7, 26),
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 "chN=value" sensor lines, so it narrows itself to its driver.
public override IReadOnlyList<string> CompatibleDrivers => ["TempSensorDriver"];
#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, "Temperature sensor protocol");
}
public override string CreateDefaultCommParam(IVariableBase variable, IEnumerable<IVarEvent> variableEvents)
{
return "direction=\"read\";channel=\"ch1\"";
}
#endregion
#region Protocol variables
public override IProtocolVariable? CreateProtocolVariable(IVariableBase variable, string commParams, bool isCommunicated)
{
try
{
return new ProtocolVariable
{
Variable = variable,
IsCommunicated = isCommunicated,
ProtocolVariableSpecification = TempSensorVariableSpecification.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, $"direction=\"read\";channel=\"{id}\"", true);
}
public override IProtocolVariable? CreateProtocolVariable(IVariableBase variable, IEnumerable<IVarEvent> variableEvents,
string commParams, bool isCommunicated)
{
// The data arrive whenever the device sends them, so variable events are not used.
return CreateProtocolVariable(variable, commParams, isCommunicated);
}
#endregion
#region Protocol control
public override Task StartAsync(CancellationToken ct = default)
{
if (!IsEnabled)
{
SetState(CommunicationState.Disabled);
return Task.CompletedTask;
}
if (State == CommunicationState.Running || runTask is { IsCompleted: false })
{
return Task.CompletedTask;
}
SetState(CommunicationState.Starting);
exitRequested = false;
receivedLines = Channel.CreateUnbounded<string>();
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);
return Task.CompletedTask;
}
public override async Task StopAsync(CancellationToken ct = default)
{
SetState(CommunicationState.Stopping);
exitRequested = true;
receivedLines?.Writer.TryComplete();
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, "Temp sensor protocol did not stop before timeout.");
}
}
runCts?.Dispose();
runCts = null;
runTask = null;
SetState(CommunicationState.Stopped);
}
public override void Dispose()
{
runCts?.Dispose();
}
#endregion
#region Received data
public override Task AddReceivedDataToQueueAsync(IEnumerable<string> data, CancellationToken ct = default)
{
foreach (var line in data)
{
receivedLines?.Writer.TryWrite(line);
}
return Task.CompletedTask;
}
private async Task RunLoopAsync(CancellationToken ct)
{
try
{
await foreach (var line in receivedLines!.Reader.ReadAllAsync(ct))
{
if (exitRequested)
{
break;
}
await ProcessReceivedDataAsync([line], ct);
}
}
catch (OperationCanceledException) when (ct.IsCancellationRequested || exitRequested)
{
// Normal stop.
}
catch (Exception e)
{
Logger?.Log(LogLevel.Error, $"Temp sensor protocol loop failed: {e.Message}");
SetState(CommunicationState.Faulted, e.Message);
}
}
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 line in data)
{
var pair = line.Split('=', 2, StringSplitOptions.TrimEntries);
if (pair.Length != 2
|| !double.TryParse(pair[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
{
Logger?.Log(LogLevel.Warn, $"Temp sensor protocol: cannot decode line '{line}'.");
continue;
}
foreach (var protocolVariable in Variables)
{
if (!protocolVariable.IsCommunicated
|| protocolVariable.ProtocolVariableSpecification is not TempSensorVariableSpecification spec
|| spec.Direction != CommDirection.Read
|| !spec.Channel.Equals(pair[0], StringComparison.OrdinalIgnoreCase)
|| protocolVariable.Variable is not ScalarVariable scalarVariable)
{
continue;
}
if (!TrySetValue(scalarVariable, value))
{
Logger?.Log(LogLevel.Warn, $"Temp sensor protocol: variable '{scalarVariable.Name}' has an unsupported value type.");
continue;
}
scalarVariable.Timestamp = DateTime.UtcNow;
updated.Add(protocolVariable);
}
}
return updated;
}
protected override IEnumerable<string> Encode(IEnumerable<IProtocolVariable> protocolVariables)
{
foreach (var protocolVariable in protocolVariables)
{
if (protocolVariable.ProtocolVariableSpecification is not TempSensorVariableSpecification spec
|| protocolVariable.Variable is not ScalarVariable scalarVariable)
{
continue;
}
var value = Convert.ToDouble(scalarVariable.Values.GetValue(), CultureInfo.InvariantCulture);
yield return $"{spec.Channel}={value.ToString("F2", CultureInfo.InvariantCulture)}";
}
}
// 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
#region Operator writes
public void SetTransmitter(Func<string, CancellationToken, Task>? byteTransmitter)
{
transmitter = byteTransmitter;
}
public bool CanWriteVariable(IProtocolVariable protocolVariable)
{
return Variables.Contains(protocolVariable)
&& protocolVariable.ProtocolVariableSpecification is TempSensorVariableSpecification { Direction: CommDirection.Write };
}
public async Task WriteVariableAsync(IProtocolVariable protocolVariable, CancellationToken ct = default)
{
var currentTransmitter = transmitter
?? throw new InvalidOperationException("Transport is not available (no transmitter injected).");
foreach (var line in Encode([protocolVariable]))
{
await currentTransmitter(line, ct);
}
}
#endregion
}
Specifikace proměnné: TempSensorVariableSpecification.cs
Properties odpovídají klíčům commParams 1:1 (channel, direction), takže
zděděná serializace ToCommParam() funguje bez přepsání.
using Qenex.QSuite.Protocols.Protocol;
namespace Qenex.QSuite.Examples.TempSensorProtocol;
/// <summary>
/// Addressing of one variable: which device channel it reads (or writes) and in which direction.
/// The properties match the commParam keys 1:1 ("channel", "direction"), so the inherited
/// reflection-based ToCommParam() round-trips the specification without an override.
/// </summary>
public class TempSensorVariableSpecification : ProtVariableSpecification
{
public TempSensorVariableSpecification()
{
Name = "TempSensorVariableSpecification";
}
/// <summary>Channel key as sent by the device, e.g. "ch1".</summary>
public string Channel { get; set; } = string.Empty;
public CommDirection Direction { get; set; } = CommDirection.Read;
public static TempSensorVariableSpecification 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("channel", out var channel) || string.IsNullOrWhiteSpace(channel))
{
throw new ArgumentException("Missing mandatory commParam 'channel'.");
}
var direction = CommDirection.Read;
if (parameters.TryGetValue("direction", out var directionText) && !string.IsNullOrWhiteSpace(directionText))
{
direction = Enum.Parse<CommDirection>(directionText, ignoreCase: true);
}
return new TempSensorVariableSpecification
{
Channel = channel,
Direction = direction
};
}
}
Jak spolu členy souvisí
| Krok | Kdo | Členy |
|---|---|---|
| Přidání driveru/protokolu v UI | plugin | DefaultRawSettings, CreateDefaultCommParam |
| Přiřazení proměnné | protokol | CreateProtocolVariable → Create specifikace |
| Start runtime | driver | StartAsync → SetTransmitters → start protokolů |
| Čtení dat | driver → protokol | smyčka driveru → AddReceivedDataToQueueAsync → ProcessReceivedDataAsync → Decode → NotifyValueChangedAsync |
| Zápis z UI | QInsight → driver → protokol → driver | CanWriteVariable (nabídka Write Mode) → CanSendCommand → OnProtocolVariableCommandAsync → WriteVariableAsync → Encode → transmitter (ApplyWriteAsync) |
| Stop | driver | StopAsync (zrušení smyčky, stop protokolů, odpojení transmitteru) |
Dále
Zpět na Vlastní driver a Vlastní protokol.

