diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38d9caa..de1f66c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -96,6 +96,10 @@
API 和可释放的 Equipment 单处理器注册;整体与参数结果码原样返回。
- 远程命令严格校验 W-Bit、二字段请求/回复、参数形状、名称唯一性和
Binary 结果码,并增加动态嵌套 Item、无处理器拒绝及真实 TCP 往返测试。
+- 增加可释放的 Equipment 远程命令接受策略;应用可依据当前通讯状态、
+ 在线状态和已解码命令决定是否交给执行处理器,未注册策略时保持现有行为。
+- 策略拒绝使用 profile 失败应答和空参数结果,且不调用执行处理器;应用
+ 改变决策后可在同一会话重试。增加注册生命周期和真实 TCP 状态门控测试。
- 增加可替换的报警目录查询消息对、不可变报警定义、Equipment 运行期注册
和 Host 全量/按动态标识查询;全量目录保持注册顺序,未知标识被忽略。
- 报警目录严格校验 W-Bit、请求 List、标识唯一性、三字段定义、单字节
diff --git a/README.md b/README.md
index ca4d82f..1136856 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ Deselect、Separate、T6/T7 和 Selected 数据门控。内部数据事务 actor
独立 SecsFrame.Gem 已提供可配置 GEM 通用行为,包含双向通讯
建立及应用接受策略、同会话显式重试恢复、上下线与应用转换策略、动态变量/
常量、应用托管时钟、报告定义、事件链接和 Collection Event、报警目录查询、
-单报警发送启停,以及最小报警通知和远程命令链路。
+单报警发送启停,以及最小报警通知、远程命令链路和应用状态接受策略。
Item 用法与边界见
[docs/SECS-II-ITEMS.md](docs/SECS-II-ITEMS.md),消息集成见
[docs/SECS-MESSAGES.md](docs/SECS-MESSAGES.md),传输适配边界见
diff --git a/docs/GEM-FOUNDATION.md b/docs/GEM-FOUNDATION.md
index 09079f4..4e0b942 100644
--- a/docs/GEM-FOUNDATION.md
+++ b/docs/GEM-FOUNDATION.md
@@ -6,7 +6,8 @@
当前切片提供 Host/Equipment 两侧的通讯建立及应用接受策略、在线/离线及
应用转换策略、状态变量读取、设备常量读取、应用托管时钟、报告定义、事件
链接和 Collection Event,以及报警目录查询、单报警发送启停、最小报警
-通知与远程命令链路,不把 GEM 状态或消息目录放入 HSMS 状态机。
+通知、远程命令链路与应用状态接受策略,不把 GEM 状态或消息目录放入 HSMS
+状态机。
GemHostServices 依赖 SecsHost;
GemEquipmentServices 依赖 SecsEquipment。服务不拥有
@@ -186,10 +187,22 @@ Equipment 使用 RegisterRemoteCommandHandler 注册单一可释放
Host 返回完整结果,不把非零代码折叠成异常,也不解释代码枚举。没有处理器
时 Equipment 使用 profile 的失败应答值和空参数结果立即回复。
-处理器在锁外执行,注册快照、释放、异常和取消语义与报警处理器一致。当前
-切片只要求请求参数名和结果参数名各自唯一,不推断结果必须覆盖哪些请求
-参数。本切片不包含命令目录、参数 Schema、权限、排队、重试、幂等策略、
-执行历史或基于 GEM 状态的命令门控。
+Equipment 可另用 RegisterRemoteCommandAcceptanceHandler
+注册单一可释放接受策略。存在执行处理器时,每个结构有效的请求先把当前
+CommunicationState、OnlineState 和已解码命令
+交给策略。返回 false 时 Equipment 使用 profile 的失败应答值
+和空参数结果回复,执行处理器不会被调用,双方 GEM 状态不变;Host 仍返回
+原始 GemRemoteCommandResult,不会把非零结果转换为异常。
+
+没有注册接受策略时命令直接进入执行处理器,保持此前行为。没有执行处理器
+时请求仍立即失败,接受策略不会执行。两个处理器在 Equipment 锁内按请求
+一起快照并在锁外按策略、执行顺序调用;释放注册只影响后续请求,已取得的
+快照会完成。策略或执行处理器的异常和取消继续传播给事件循环,不会被折叠
+为失败应答。
+
+当前切片只要求请求参数名和结果参数名各自唯一,不推断结果必须覆盖哪些
+请求参数,也不内置任何通讯/在线状态矩阵。本切片不包含命令目录、参数
+Schema、权限、排队、自动重试、幂等策略或执行历史。
## 严格失败边界
@@ -218,12 +231,14 @@ Host 返回完整结果,不把非零代码折叠成异常,也不解释代码
- 报告配置请求和 Collection Event 必须设置 W-Bit,Secondary 的消息对和
应答字节必须与 profile 匹配;报警通知遵循相同要求;
- 远程命令必须设置 W-Bit 且 Secondary 消息对完全匹配;整体和参数结果码
- 保持原始字节,不限制为 profile 的成功/失败两个值。
+ 保持原始字节,不限制为 profile 的成功/失败两个值;接受策略拒绝时使用
+ profile 失败值和空参数结果,且不得调用执行处理器。
畸形输入和提供器失败会作为异常返回应用事件循环;语义有效但引用未知变量
或报告的配置使用失败应答拒绝。本切片尚未实现自动 S9Fx、错误 Secondary、
-自动重新建立、业务消息状态门控、报警历史/批量控制、命令目录/权限/调度
-或完整 GEM 错误恢复,这些行为不能从当前 API 推断为标准合规。
+自动重新建立、远程命令以外的业务消息状态门控、报警历史/批量控制、命令
+目录/权限/调度或完整 GEM 错误恢复,这些行为不能从当前 API 推断为标准
+合规。
## 标准与验证边界
@@ -242,5 +257,5 @@ Passive 真实 TCP 下的双向通讯建立、首次建立拒绝后同会话恢
动态标识、报告定义、
事件链接、Collection Event 嵌套值与空报告、报警通知接受/拒绝、配置/事件
拒绝、报警目录全量/选择查询与释放快照、单报警发送启停与未知标识拒绝、
-远程命令动态参数与结果、无处理器拒绝、时钟读写和 Linktest。它们是独立
-工程验证,不是 SEMI 一致性认证。
+远程命令动态参数与结果、无处理器拒绝、应用状态拒绝短路与同会话重试、
+时钟读写和 Linktest。它们是独立工程验证,不是 SEMI 一致性认证。
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index fd2b3db..e8666ae 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -44,6 +44,8 @@
- [x] 单个已注册报警的发送启停、未知标识拒绝和通知发送门控。
- [x] Equipment 在线/离线转换的应用决策、显式拒绝与状态保持。
- [x] Host/Equipment 双向通讯建立应用决策、显式拒绝与同会话重试恢复。
+- [x] Equipment 入站远程命令的应用状态接受策略、拒绝短路与同会话重试。
+- [ ] Collection Event、报警通知等其余业务消息的应用状态门控。
- [ ] 报警历史/批量控制、命令目录/权限/调度和更完整的状态/错误恢复。
- [ ] 使用授权 E30-0526 核对基础消息条件、数据项、应答值和状态转换。
@@ -53,8 +55,11 @@
工程基线;单报警发送启停也使用显式可替换控制码。在线转换策略默认不注册,
保持既有宽松接受行为。通讯建立策略在 Host 与 Equipment 两侧同样默认
不注册;拒绝不改变既有通讯状态或对端身份,恢复由应用在同一 Selected
-会话显式重试,库不启动后台循环。下一优先级是默认关闭、由应用配置的业务
-消息状态门控最小切片,再评估自动恢复、报警历史和批量控制。
+会话显式重试,库不启动后台循环。远程命令接受策略也默认不注册;应用可
+依据当前通讯/在线状态决定是否执行命令,库不内置未经 E30 核对的状态矩阵。
+下一优先级是把相同的默认关闭模式扩展到 Equipment 主动业务上报,最小
+切片先覆盖 Collection Event 发送入口,再评估报警通知、自动恢复、报警
+历史和批量控制。
## 阶段 3:元数据与工具
diff --git a/docs/SECS-ENDPOINT-ROLES.md b/docs/SECS-ENDPOINT-ROLES.md
index 2ea48d3..91484ef 100644
--- a/docs/SECS-ENDPOINT-ROLES.md
+++ b/docs/SECS-ENDPOINT-ROLES.md
@@ -67,6 +67,7 @@ await equipment.WaitUntilSelectedAsync(cancellationToken);
报警目录查询支持运行期注册,Host 也可启停单个已注册报警的通知发送。
Host 与 Equipment 的 GEM 服务都可注册单一通讯建立策略;拒绝保留双方
既有通讯状态和对端身份,应用可在同一 Selected 会话显式重试。自动重新
-建立、业务消息状态门控、报警历史/批量控制、命令目录/权限/调度仍未实现。
-相关行为、事件循环要求和未核对边界见
+建立仍未实现。Equipment 入站远程命令可注册默认关闭的应用状态接受策略;
+Collection Event、报警通知等其他业务消息状态门控,以及报警历史/批量
+控制、命令目录/权限/调度仍未实现。相关行为、事件循环要求和未核对边界见
[GEM-FOUNDATION.md](GEM-FOUNDATION.md),项目不声明 GEM 合规。
diff --git a/docs/STANDARDS.md b/docs/STANDARDS.md
index ad9a50c..564f5bc 100644
--- a/docs/STANDARDS.md
+++ b/docs/STANDARDS.md
@@ -9,7 +9,7 @@
|---|---|---|
| SECS-II 消息内容 | SEMI E5-0725 | Item、动态消息编解码与内部 T3 数据事务已有工程实现;完整事务规则待核对 |
| HSMS / HSMS-SS | SEMI E37-0222 / E37.1-0819 | 帧、Data Payload、内部传输、T5/T8 工程计时、控制状态机与会话绑定数据发送已有实现;仍待授权标准和一致性测试核对 |
-| GEM | SEMI E30-0526 | 通讯建立与双向应用接受策略、上下线、动态变量/常量、时钟、报告/事件、报警通知/目录/发送启停和远程命令已有可替换 profile 的工程实现;规范性条件待核对 |
+| GEM | SEMI E30-0526 | 通讯建立与双向应用接受策略、上下线、动态变量/常量、时钟、报告/事件、报警通知/目录/发送启停,以及带应用状态接受策略的远程命令已有可替换 profile 的工程实现;规范性条件待核对 |
| SEDD | SEMI E172-0225 | 规划中 |
| SMN | SEMI E173-0721 | 规划中 |
@@ -39,7 +39,8 @@
的必选/可选边界;还需核对基础消息条件、COMMACK/ONLACK/OFLACK/TIACK
取值、MDLN 条件结构、空 SVID/ECID 列表、报告定义与链接生命周期、
Collection Event 数据结构和应答值、报警通知/目录/发送启停的数据结构与
- 控制码、应用通讯建立/在线转换策略、时间格式、状态转换与错误响应。
+ 控制码、应用通讯建立/在线转换策略、远程命令执行条件与状态门控、时间
+ 格式、状态转换与错误响应。
- E172/E173:允许分发的 Schema/表示形式、版本标识、扩展点和验证规则。
## 版权与合规边界
diff --git a/src/SecsFrame.Gem/GemEquipmentServices.cs b/src/SecsFrame.Gem/GemEquipmentServices.cs
index 9a9f300..4e7b94b 100644
--- a/src/SecsFrame.Gem/GemEquipmentServices.cs
+++ b/src/SecsFrame.Gem/GemEquipmentServices.cs
@@ -19,6 +19,8 @@ public sealed class GemEquipmentServices : IDisposable
private readonly Dictionary _alarms = new();
private readonly List _alarmCatalog = new();
private GemOnlineStateTransitionRegistration? _onlineStateTransitionHandler;
+ private GemRemoteCommandAcceptanceRegistration?
+ _remoteCommandAcceptanceHandler;
private GemRemoteCommandRegistration? _remoteCommandHandler;
private int _disposed;
@@ -129,6 +131,34 @@ public GemOnlineStateTransitionRegistration RegisterOnlineStateTransitionHandler
}
}
+ ///
+ /// Registers the single application policy evaluated before remote-command
+ /// execution.
+ ///
+ public GemRemoteCommandAcceptanceRegistration
+ RegisterRemoteCommandAcceptanceHandler(
+ GemRemoteCommandAcceptanceHandler handler)
+ {
+ if (handler is null)
+ throw new ArgumentNullException(nameof(handler));
+
+ lock (_gate)
+ {
+ ThrowIfDisposed();
+ if (_remoteCommandAcceptanceHandler is not null)
+ {
+ throw new InvalidOperationException(
+ "A remote-command acceptance handler is already registered.");
+ }
+
+ var registration = new GemRemoteCommandAcceptanceRegistration(
+ handler,
+ UnregisterRemoteCommandAcceptanceHandler);
+ _remoteCommandAcceptanceHandler = registration;
+ return registration;
+ }
+ }
+
/// Registers the single application remote-command handler.
public GemRemoteCommandRegistration RegisterRemoteCommandHandler(
GemRemoteCommandHandler handler)
@@ -276,6 +306,7 @@ public void Dispose()
_alarms.Clear();
_alarmCatalog.Clear();
_onlineStateTransitionHandler = null;
+ _remoteCommandAcceptanceHandler = null;
_remoteCommandHandler = null;
}
_services.Dispose();
@@ -468,17 +499,38 @@ links is null
GemMessageCodec.RequireReplyExpected(context, "remote-command request");
var command = GemMessageCodec.DecodeRemoteCommand(
context.Message.RootItem);
+ GemRemoteCommandAcceptanceHandler? acceptanceHandler;
GemRemoteCommandHandler? handler;
+ GemCommunicationState communicationState;
+ GemOnlineState onlineState;
lock (_gate)
+ {
+ acceptanceHandler = _remoteCommandAcceptanceHandler?.Handler;
handler = _remoteCommandHandler?.Handler;
+ communicationState = CommunicationState;
+ onlineState = OnlineState;
+ }
- var result = handler is null
- ? new GemRemoteCommandResult(
+ GemRemoteCommandResult result;
+ if (handler is null ||
+ (acceptanceHandler is not null &&
+ !await acceptanceHandler(
+ communicationState,
+ onlineState,
+ command,
+ cancellationToken).ConfigureAwait(false)))
+ {
+ result = new GemRemoteCommandResult(
Profile.FailedAcknowledgement,
- Array.Empty())
- : await handler(command, cancellationToken).ConfigureAwait(false) ??
+ Array.Empty());
+ }
+ else
+ {
+ result = await handler(command, cancellationToken).ConfigureAwait(false) ??
throw new GemProtocolException(
"The remote-command handler returned null.");
+ }
+
return GemEndpointServices.CreateSecondary(
Profile.RemoteCommand,
GemMessageCodec.EncodeRemoteCommandResult(result));
@@ -783,6 +835,20 @@ private void UnregisterRemoteCommandHandler(
}
}
+ private void UnregisterRemoteCommandAcceptanceHandler(
+ GemRemoteCommandAcceptanceRegistration registration)
+ {
+ lock (_gate)
+ {
+ if (ReferenceEquals(
+ _remoteCommandAcceptanceHandler,
+ registration))
+ {
+ _remoteCommandAcceptanceHandler = null;
+ }
+ }
+ }
+
private void UnregisterOnlineStateTransitionHandler(
GemOnlineStateTransitionRegistration registration)
{
diff --git a/src/SecsFrame.Gem/GemRemoteCommandAcceptanceHandler.cs b/src/SecsFrame.Gem/GemRemoteCommandAcceptanceHandler.cs
new file mode 100644
index 0000000..088e900
--- /dev/null
+++ b/src/SecsFrame.Gem/GemRemoteCommandAcceptanceHandler.cs
@@ -0,0 +1,10 @@
+namespace SecsFrame.Gem;
+
+///
+/// Decides whether a decoded remote command may be passed to its executor.
+///
+public delegate ValueTask GemRemoteCommandAcceptanceHandler(
+ GemCommunicationState communicationState,
+ GemOnlineState onlineState,
+ GemRemoteCommand command,
+ CancellationToken cancellationToken);
diff --git a/src/SecsFrame.Gem/GemRemoteCommandAcceptanceRegistration.cs b/src/SecsFrame.Gem/GemRemoteCommandAcceptanceRegistration.cs
new file mode 100644
index 0000000..540ceca
--- /dev/null
+++ b/src/SecsFrame.Gem/GemRemoteCommandAcceptanceRegistration.cs
@@ -0,0 +1,21 @@
+namespace SecsFrame.Gem;
+
+/// Owns one Equipment remote-command acceptance registration.
+public sealed class GemRemoteCommandAcceptanceRegistration : IDisposable
+{
+ private Action? _unregister;
+
+ internal GemRemoteCommandAcceptanceRegistration(
+ GemRemoteCommandAcceptanceHandler handler,
+ Action unregister)
+ {
+ Handler = handler;
+ _unregister = unregister;
+ }
+
+ internal GemRemoteCommandAcceptanceHandler Handler { get; }
+
+ /// Removes this exact handler registration. Repeated calls are harmless.
+ public void Dispose()
+ => Interlocked.Exchange(ref _unregister, null)?.Invoke(this);
+}
diff --git a/test/SecsFrame.Tests/GemEquipmentServicesTests.cs b/test/SecsFrame.Tests/GemEquipmentServicesTests.cs
index a262dba..e216712 100644
--- a/test/SecsFrame.Tests/GemEquipmentServicesTests.cs
+++ b/test/SecsFrame.Tests/GemEquipmentServicesTests.cs
@@ -325,6 +325,33 @@ public async Task Remote_command_handler_is_exact_disposable_and_replaceable()
services.RegisterRemoteCommandHandler(handler));
}
+ [Fact]
+ public async Task Remote_command_acceptance_handler_is_exact_disposable_and_replaceable()
+ {
+ await using var endpoint = new SecsEquipment(
+ CreateOptions(GetFreePort(), HsmsConnectionMode.Active));
+ using var services = new GemEquipmentServices(
+ endpoint,
+ new GemIdentity("EQ-01", "1.0"),
+ new TestGemClock(Epoch));
+ GemRemoteCommandAcceptanceHandler handler = static (_, _, _, _) =>
+ new ValueTask(true);
+
+ Assert.Throws(() =>
+ services.RegisterRemoteCommandAcceptanceHandler(null!));
+ var first =
+ services.RegisterRemoteCommandAcceptanceHandler(handler);
+ Assert.Throws(() =>
+ services.RegisterRemoteCommandAcceptanceHandler(handler));
+ first.Dispose();
+ first.Dispose();
+ using var replacement =
+ services.RegisterRemoteCommandAcceptanceHandler(handler);
+ services.Dispose();
+ Assert.Throws(() =>
+ services.RegisterRemoteCommandAcceptanceHandler(handler));
+ }
+
[Fact]
public async Task Alarm_registration_is_exact_disposable_and_replaceable()
{
diff --git a/test/SecsFrame.Tests/GemFoundationTcpTests.cs b/test/SecsFrame.Tests/GemFoundationTcpTests.cs
index cf35479..b734eb8 100644
--- a/test/SecsFrame.Tests/GemFoundationTcpTests.cs
+++ b/test/SecsFrame.Tests/GemFoundationTcpTests.cs
@@ -527,58 +527,148 @@ await context.HostServices.SetAlarmSendEnabledAsync(
}
private static async Task AssertRemoteCommandsAsync(GemTcpContext context)
+ {
+ var command =
+ await AssertRemoteCommandAcceptancePolicyAsync(context)
+ .ConfigureAwait(true);
+ Assert.Equal(SecsItem.U4(7001), command.Command);
+ Assert.Equal(2, command.Parameters.Count);
+ Assert.Equal(SecsItem.Ascii("SPEED"), command.Parameters[0].Name);
+ Assert.Equal(SecsItem.U2(25), command.Parameters[0].Value);
+ Assert.Equal(SecsItem.U1(2), command.Parameters[1].Name);
+ Assert.Equal(
+ SecsItem.List(SecsItem.Boolean(true), SecsItem.F8(1.5)),
+ command.Parameters[1].Value);
+
+ var acceptanceCalls = 0;
+ using (context.EquipmentServices.RegisterRemoteCommandAcceptanceHandler(
+ (_, _, _, _) =>
+ {
+ acceptanceCalls++;
+ return new ValueTask(true);
+ }))
+ {
+ var unavailable = await context.HostServices.ExecuteRemoteCommandAsync(
+ CreateRemoteCommand(),
+ context.Token).ConfigureAwait(true);
+ AssertRejectedRemoteCommandResult(unavailable);
+ }
+
+ Assert.Equal(0, acceptanceCalls);
+ }
+
+ private static async Task
+ AssertRemoteCommandAcceptancePolicyAsync(GemTcpContext context)
{
var received = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
+ var executionCount = 0;
using (context.EquipmentServices.RegisterRemoteCommandHandler(
(command, cancellationToken) =>
{
cancellationToken.ThrowIfCancellationRequested();
+ executionCount++;
received.TrySetResult(command);
return new ValueTask(
- new GemRemoteCommandResult(
- 0,
- new[]
- {
- new GemRemoteCommandParameterResult(
- SecsItem.Ascii("SPEED"),
- 0),
- new GemRemoteCommandParameterResult(
- SecsItem.U1(2),
- 7),
- }));
+ CreateAcceptedRemoteCommandResult());
}))
{
- var result = await context.HostServices.ExecuteRemoteCommandAsync(
- CreateRemoteCommand(),
- context.Token).ConfigureAwait(true);
- Assert.Equal((byte)0, result.Acknowledgement);
- Assert.Equal(2, result.ParameterResults.Count);
- Assert.Equal(
- SecsItem.Ascii("SPEED"),
- result.ParameterResults[0].Name);
- Assert.Equal((byte)0, result.ParameterResults[0].Acknowledgement);
- Assert.Equal(SecsItem.U1(2), result.ParameterResults[1].Name);
- Assert.Equal((byte)7, result.ParameterResults[1].Acknowledgement);
+ var acceptCommands = false;
+ var observed = new List<(
+ GemCommunicationState Communication,
+ GemOnlineState Online,
+ SecsItem Command)>();
+ using (context.EquipmentServices.RegisterRemoteCommandAcceptanceHandler(
+ (communication, online, command, cancellationToken) =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ observed.Add((communication, online, command.Command));
+ return new ValueTask(acceptCommands);
+ }))
+ {
+ var rejected = await context.HostServices.ExecuteRemoteCommandAsync(
+ CreateRemoteCommand(),
+ context.Token).ConfigureAwait(true);
+ AssertRejectedRemoteCommandResult(rejected);
+ Assert.Equal(0, executionCount);
+ Assert.Equal(
+ GemCommunicationState.Communicating,
+ context.EquipmentServices.CommunicationState);
+ Assert.Equal(
+ GemOnlineState.Online,
+ context.EquipmentServices.OnlineState);
+
+ acceptCommands = true;
+ AssertAcceptedRemoteCommandResult(
+ await context.HostServices.ExecuteRemoteCommandAsync(
+ CreateRemoteCommand(),
+ context.Token).ConfigureAwait(true));
+ Assert.Equal(1, executionCount);
+ }
+
+ AssertAcceptedRemoteCommandResult(
+ await context.HostServices.ExecuteRemoteCommandAsync(
+ CreateRemoteCommand(),
+ context.Token).ConfigureAwait(true));
+ Assert.Equal(2, executionCount);
+ AssertRemoteCommandPolicyObservations(observed);
}
- var command = await received.Task.ConfigureAwait(true);
- Assert.Equal(SecsItem.U4(7001), command.Command);
- Assert.Equal(2, command.Parameters.Count);
- Assert.Equal(SecsItem.Ascii("SPEED"), command.Parameters[0].Name);
- Assert.Equal(SecsItem.U2(25), command.Parameters[0].Value);
- Assert.Equal(SecsItem.U1(2), command.Parameters[1].Name);
+ return await received.Task.ConfigureAwait(true);
+ }
+
+ private static GemRemoteCommandResult CreateAcceptedRemoteCommandResult()
+ => new(
+ 0,
+ new[]
+ {
+ new GemRemoteCommandParameterResult(
+ SecsItem.Ascii("SPEED"),
+ 0),
+ new GemRemoteCommandParameterResult(
+ SecsItem.U1(2),
+ 7),
+ });
+
+ private static void AssertAcceptedRemoteCommandResult(
+ GemRemoteCommandResult result)
+ {
+ Assert.Equal((byte)0, result.Acknowledgement);
+ Assert.Equal(2, result.ParameterResults.Count);
Assert.Equal(
- SecsItem.List(SecsItem.Boolean(true), SecsItem.F8(1.5)),
- command.Parameters[1].Value);
+ SecsItem.Ascii("SPEED"),
+ result.ParameterResults[0].Name);
+ Assert.Equal((byte)0, result.ParameterResults[0].Acknowledgement);
+ Assert.Equal(SecsItem.U1(2), result.ParameterResults[1].Name);
+ Assert.Equal((byte)7, result.ParameterResults[1].Acknowledgement);
+ }
- var unavailable = await context.HostServices.ExecuteRemoteCommandAsync(
- CreateRemoteCommand(),
- context.Token).ConfigureAwait(true);
- Assert.Equal((byte)1, unavailable.Acknowledgement);
- Assert.Empty(unavailable.ParameterResults);
+ private static void AssertRejectedRemoteCommandResult(
+ GemRemoteCommandResult result)
+ {
+ Assert.Equal((byte)1, result.Acknowledgement);
+ Assert.Empty(result.ParameterResults);
}
+ private static void AssertRemoteCommandPolicyObservations(
+ IReadOnlyList<(
+ GemCommunicationState Communication,
+ GemOnlineState Online,
+ SecsItem Command)> observed)
+ => Assert.Equal(
+ new[]
+ {
+ (
+ GemCommunicationState.Communicating,
+ GemOnlineState.Online,
+ SecsItem.U4(7001)),
+ (
+ GemCommunicationState.Communicating,
+ GemOnlineState.Online,
+ SecsItem.U4(7001)),
+ },
+ observed);
+
private static GemRemoteCommand CreateRemoteCommand()
=> new(
SecsItem.U4(7001),