From 8a8b8ce39b2174b55ef56ca405e7534e60efca11 Mon Sep 17 00:00:00 2001 From: Marci Date: Sat, 29 Aug 2026 11:22:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=8A=B6=E6=80=81=E6=8E=A5=E5=8F=97=E7=AD=96?= =?UTF-8?q?=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 + README.md | 2 +- docs/GEM-FOUNDATION.md | 35 ++-- docs/ROADMAP.md | 9 +- docs/SECS-ENDPOINT-ROLES.md | 5 +- docs/STANDARDS.md | 5 +- src/SecsFrame.Gem/GemEquipmentServices.cs | 74 +++++++- .../GemRemoteCommandAcceptanceHandler.cs | 10 ++ .../GemRemoteCommandAcceptanceRegistration.cs | 21 +++ .../GemEquipmentServicesTests.cs | 27 +++ test/SecsFrame.Tests/GemFoundationTcpTests.cs | 160 ++++++++++++++---- 11 files changed, 296 insertions(+), 56 deletions(-) create mode 100644 src/SecsFrame.Gem/GemRemoteCommandAcceptanceHandler.cs create mode 100644 src/SecsFrame.Gem/GemRemoteCommandAcceptanceRegistration.cs 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),