diff --git a/CHANGELOG.md b/CHANGELOG.md index 68cb660..f164477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,10 @@ SecsItem 标识注册多个可释放处理器。 - 命令分派优先使用精确目录项,未匹配时回退现有单一全局处理器;两者都 不存在时保持失败结果且不调用接受策略。增加注册生命周期和真实 TCP 测试。 +- 增加远程命令精确目录项的运行期可用性;新项默认允许执行,应用可原子 + 停用或恢复单个动态命令。 +- 已匹配但禁用的精确项使用失败结果短路,不调用接受策略、精确处理器或 + 全局回退;重新启用后支持同一 Selected 会话显式重试。增加状态机测试。 - 增加可替换的报警目录查询消息对、不可变报警定义、Equipment 运行期注册 和 Host 全量/按动态标识查询;全量目录保持注册顺序,未知标识被忽略。 - 报警目录严格校验 W-Bit、请求 List、标识唯一性、三字段定义、单字节 diff --git a/README.md b/README.md index ea57da4..8ec5c45 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ Deselect、Separate、T6/T7 和 Selected 数据门控。内部数据事务 actor 建立及应用接受策略、同会话显式重试恢复、上下线与应用转换策略、动态变量/ 常量、应用托管时钟、报告定义、事件链接和带 Equipment 应用发送策略的 Collection Event、报警目录查询、单报警发送启停,以及最小报警通知、 -Equipment 报警通知发送策略、远程命令运行期精确目录/全局回退和应用状态 -接受策略。 +Equipment 报警通知发送策略、远程命令运行期精确目录/逐项可用性/全局回退 +和应用状态接受策略。 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 d5c8008..cd3dc55 100644 --- a/docs/GEM-FOUNDATION.md +++ b/docs/GEM-FOUNDATION.md @@ -214,12 +214,19 @@ Host 使用 GemRemoteCommand 携带动态命令标识和有序参 Equipment 使用 RegisterRemoteCommand 按不可变 SecsItem 命令标识注册多个精确运行期处理器;返回的 -GemRemoteCommandEntryRegistration 暴露精确标识并可释放。 -同一结构相等标识不能重复注册。应用还可使用 +GemRemoteCommandEntryRegistration 暴露精确标识和 +IsExecutionEnabled,可用 +SetExecutionEnabled(bool) 在运行期启停,也可释放。同一结构 +相等标识不能重复注册,新注册项默认允许执行。应用还可使用 RegisterRemoteCommandHandler 注册单一全局回退处理器。 分派优先选择精确处理器,未匹配时才选择全局回退;两者都不存在时 Equipment 使用 profile 的失败应答值和空参数结果立即回复。 +已匹配但禁用的精确项表示“已知但当前不可执行”,同样立即返回失败结果; +它不会调用精确处理器、接受策略或全局回退,避免停用命令经回退路径绕过。 +应用重新启用后可在同一 Selected 会话显式重试。释放精确项后,该标识才 +重新成为未匹配命令并可进入全局回退;重新注册的新项恢复默认启用状态。 + 选中的处理器返回 GemRemoteCommandResult,其中包含一个原始 整体完成码和有序参数结果;每个参数结果保留动态名称与原始单字节代码。 Host 返回完整结果,不把非零代码折叠成异常,也不解释代码枚举。 @@ -232,15 +239,16 @@ Equipment 可另用 RegisterRemoteCommandAcceptanceHandler 原始 GemRemoteCommandResult,不会把非零结果转换为异常。 没有注册接受策略时命令直接进入选中的执行处理器,保持此前行为。没有执行 -处理器时请求仍立即失败,接受策略不会执行。接受策略、精确/回退选择和当前 -状态在 Equipment 锁内按请求一起快照,并在锁外按策略、执行顺序调用;释放 -注册只影响后续请求,已取得的快照会完成。策略或执行处理器的异常和取消 -继续传播给事件循环,不会被折叠为失败应答。 +处理器或精确项已禁用时请求立即失败,接受策略不会执行。接受策略、精确项 +可用性、精确/回退选择和当前状态在 Equipment 锁内按请求一起快照,并在 +锁外按策略、执行顺序调用;启停或释放注册只影响后续请求,已取得的快照会 +完成。策略或执行处理器的异常和取消继续传播给事件循环,不会被折叠为失败 +应答。 当前切片只要求请求参数名和结果参数名各自唯一,不推断结果必须覆盖哪些 请求参数,也不内置任何通讯/在线状态矩阵。运行期目录只负责应用侧精确 -分派,不新增 E30 消息、不提供 Host 目录查询,也不包含参数 Schema、权限、 -排队、自动重试、幂等策略或执行历史。 +分派和本地可用性,不新增 E30 消息、不提供 Host 目录查询,也不包含参数 +Schema、角色/用户权限、排队、自动重试、幂等策略或执行历史。 ## 严格失败边界 @@ -270,13 +278,13 @@ Equipment 可另用 RegisterRemoteCommandAcceptanceHandler 应答字节必须与 profile 匹配;报警通知遵循相同要求; - 远程命令必须设置 W-Bit 且 Secondary 消息对完全匹配;整体和参数结果码 保持原始字节,不限制为 profile 的成功/失败两个值;接受策略拒绝时使用 - profile 失败值和空参数结果,且不得调用执行处理器。 + profile 失败值和空参数结果,且不得调用执行处理器;已禁用精确项也使用 + 相同失败结果,并且不得调用接受策略、精确处理器或全局回退。 畸形输入和提供器失败会作为异常返回应用事件循环;语义有效但引用未知变量 或报告的配置使用失败应答拒绝。本切片尚未实现自动 S9Fx、错误 Secondary、 -自动重新建立、远程命令以外的业务消息状态门控、报警历史/批量控制、命令 -目录/权限/调度或完整 GEM 错误恢复,这些行为不能从当前 API 推断为标准 -合规。 +自动重新建立、报警历史/批量控制、完整命令权限/参数 Schema/调度或完整 +GEM 错误恢复,这些行为不能从当前 API 推断为标准合规。 ## 标准与验证边界 @@ -297,5 +305,6 @@ Passive 真实 TCP 下的双向通讯建立、首次建立拒绝后同会话恢 事务和同会话重试、报警通知接受/拒绝、应用发送策略拒绝零事务与同会话 重试、已禁用报警优先拒绝、配置/事件拒绝、报警目录全量/选择查询与释放 快照、单报警发送启停与未知标识拒绝、远程命令动态参数与结果、精确目录 -优先/全局回退/释放恢复、无处理器拒绝、应用状态拒绝短路与同会话重试、 -时钟读写和 Linktest。它们是独立工程验证,不是 SEMI 一致性认证。 +优先/全局回退/释放恢复、精确项禁用无回退绕过和重新启用后的同会话重试、 +无处理器拒绝、应用状态拒绝短路与同会话重试、时钟读写和 Linktest。它们 +是独立工程验证,不是 SEMI 一致性认证。 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5ee65fe..00e0928 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -42,6 +42,7 @@ epoch 收口与 Passive 监听加固。后续切片继续扩展 GEM 通用行为 - [x] 报警通知的动态标识、代码透传、Host 处理器和 Equipment 发送链路。 - [x] 远程命令的动态参数/结果、Host 发送和 Equipment 处理链路。 - [x] Equipment 运行期远程命令精确目录、全局回退与可释放生命周期。 +- [x] Equipment 远程命令精确目录项本地可用性、无回退绕过与同会话恢复。 - [x] Equipment 运行期报警目录注册与 Host 全量/按标识查询。 - [x] 单个已注册报警的发送启停、未知标识拒绝和通知发送门控。 - [x] Equipment 在线/离线转换的应用决策、显式拒绝与状态保持。 @@ -74,9 +75,16 @@ Collection Event 发送策略沿用相同的默认不注册模式;拒绝发生 完成前不固化新的默认消息对。 当前仓库没有可用于该核对的授权材料,因此先完成不新增消息映射的 Equipment -运行期命令目录:动态标识精确处理器优先,现有单一处理器作为全局回退, -未知命令保持失败结果。下一可执行优先级是评估命令权限/参数约束如何复用 -现有全局接受策略而不重复策略层;自动恢复和调度继续保持后续评估项。 +运行期命令目录和逐项本地可用性:动态标识精确处理器优先,现有单一处理器 +作为全局回退;已匹配但禁用的精确项保持失败结果且不进入接受策略或回退, +应用重新启用后可在同一 Selected 会话显式重试。 + +命令权限/参数约束的评估结论是暂不增加第二套通用策略:现有全局接受策略 +已经覆盖通讯/在线状态等应用决策,精确处理器可以校验命令专属参数并返回 +原始整体/参数结果码。完整角色权限、参数 Schema 与调度仍是后续能力。 +下一可执行优先级转向更完整的状态/错误恢复,最小切片先评估断线重选后由 +应用显式协调 GEM 通讯重建的状态、取消和重试边界;在授权 E30 状态表核对 +前不引入后台重试循环或隐式状态矩阵。 ## 阶段 3:元数据与工具 diff --git a/docs/SECS-ENDPOINT-ROLES.md b/docs/SECS-ENDPOINT-ROLES.md index 91484ef..33b01ab 100644 --- a/docs/SECS-ENDPOINT-ROLES.md +++ b/docs/SECS-ENDPOINT-ROLES.md @@ -67,7 +67,9 @@ await equipment.WaitUntilSelectedAsync(cancellationToken); 报警目录查询支持运行期注册,Host 也可启停单个已注册报警的通知发送。 Host 与 Equipment 的 GEM 服务都可注册单一通讯建立策略;拒绝保留双方 既有通讯状态和对端身份,应用可在同一 Selected 会话显式重试。自动重新 -建立仍未实现。Equipment 入站远程命令可注册默认关闭的应用状态接受策略; -Collection Event、报警通知等其他业务消息状态门控,以及报警历史/批量 -控制、命令目录/权限/调度仍未实现。相关行为、事件循环要求和未核对边界见 -[GEM-FOUNDATION.md](GEM-FOUNDATION.md),项目不声明 GEM 合规。 +建立仍未实现。Equipment 入站远程命令、主动 Collection Event 和报警通知 +都可注册默认不启用的应用状态策略。远程命令还支持运行期精确目录、全局 +回退和默认启用的逐项本地可用性;禁用精确项不会进入全局回退。报警历史/ +批量控制、完整命令权限/参数 Schema/调度及自动恢复仍未实现。相关行为、 +事件循环要求和未核对边界见 [GEM-FOUNDATION.md](GEM-FOUNDATION.md), +项目不声明 GEM 合规。 diff --git a/docs/STANDARDS.md b/docs/STANDARDS.md index dc6837f..43c31cd 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 | 规划中 | diff --git a/src/SecsFrame.Gem/GemEquipmentServices.cs b/src/SecsFrame.Gem/GemEquipmentServices.cs index bd569d4..00902e9 100644 --- a/src/SecsFrame.Gem/GemEquipmentServices.cs +++ b/src/SecsFrame.Gem/GemEquipmentServices.cs @@ -634,11 +634,18 @@ links is null lock (_gate) { acceptanceHandler = _remoteCommandAcceptanceHandler?.Handler; - handler = _remoteCommandEntries.TryGetValue( + if (_remoteCommandEntries.TryGetValue( command.Command, - out var registration) - ? registration.Handler - : _remoteCommandHandler?.Handler; + out var registration)) + { + handler = registration.IsExecutionEnabled + ? registration.Handler + : null; + } + else + { + handler = _remoteCommandHandler?.Handler; + } communicationState = CommunicationState; onlineState = OnlineState; } diff --git a/src/SecsFrame.Gem/GemRemoteCommandEntryRegistration.cs b/src/SecsFrame.Gem/GemRemoteCommandEntryRegistration.cs index 7f34520..2abbbcb 100644 --- a/src/SecsFrame.Gem/GemRemoteCommandEntryRegistration.cs +++ b/src/SecsFrame.Gem/GemRemoteCommandEntryRegistration.cs @@ -4,6 +4,7 @@ namespace SecsFrame.Gem; public sealed class GemRemoteCommandEntryRegistration : IDisposable { private Action? _unregister; + private int _executionEnabled = 1; internal GemRemoteCommandEntryRegistration( SecsItem command, @@ -18,6 +19,13 @@ internal GemRemoteCommandEntryRegistration( /// Gets the exact dynamic command identifier. public SecsItem Command { get; } + /// Gets whether this command may currently execute. + public bool IsExecutionEnabled => Volatile.Read(ref _executionEnabled) != 0; + + /// Changes whether this command may execute. + public void SetExecutionEnabled(bool enabled) + => Volatile.Write(ref _executionEnabled, enabled ? 1 : 0); + internal GemRemoteCommandHandler Handler { get; } /// Removes this exact directory entry. Repeated calls are harmless. diff --git a/test/SecsFrame.Tests/GemEquipmentServicesTests.cs b/test/SecsFrame.Tests/GemEquipmentServicesTests.cs index b8646e7..5af9f13 100644 --- a/test/SecsFrame.Tests/GemEquipmentServicesTests.cs +++ b/test/SecsFrame.Tests/GemEquipmentServicesTests.cs @@ -347,11 +347,17 @@ public async Task Remote_command_entry_is_exact_disposable_and_replaceable() services.RegisterRemoteCommand(command, null!)); var first = services.RegisterRemoteCommand(command, handler); Assert.Equal(command, first.Command); + Assert.True(first.IsExecutionEnabled); + first.SetExecutionEnabled(false); + Assert.False(first.IsExecutionEnabled); + first.SetExecutionEnabled(true); + Assert.True(first.IsExecutionEnabled); Assert.Throws(() => services.RegisterRemoteCommand(SecsItem.Ascii("START"), handler)); first.Dispose(); first.Dispose(); using var replacement = services.RegisterRemoteCommand(command, handler); + Assert.True(replacement.IsExecutionEnabled); services.Dispose(); Assert.Throws(() => services.RegisterRemoteCommand(SecsItem.U4(2), handler)); diff --git a/test/SecsFrame.Tests/GemFoundationTcpTests.cs b/test/SecsFrame.Tests/GemFoundationTcpTests.cs index 4a40b29..718a8a4 100644 --- a/test/SecsFrame.Tests/GemFoundationTcpTests.cs +++ b/test/SecsFrame.Tests/GemFoundationTcpTests.cs @@ -655,6 +655,7 @@ private static async Task AssertRemoteCommandDirectoryAsync( { var exactCalls = 0; var fallbackCalls = 0; + var acceptanceCalls = 0; using var fallbackRegistration = context.EquipmentServices.RegisterRemoteCommandHandler( (_, _) => @@ -672,25 +673,45 @@ private static async Task AssertRemoteCommandDirectoryAsync( return new ValueTask( CreateAcceptedRemoteCommandResult()); }); - + using var acceptanceRegistration = + context.EquipmentServices.RegisterRemoteCommandAcceptanceHandler( + (_, _, _, _) => + { + acceptanceCalls++; + return new ValueTask(true); + }); AssertAcceptedRemoteCommandResult( await context.HostServices.ExecuteRemoteCommandAsync( CreateRemoteCommand(), context.Token).ConfigureAwait(true)); - Assert.Equal((1, 0), (exactCalls, fallbackCalls)); + Assert.Equal((1, 0, 1), (exactCalls, fallbackCalls, acceptanceCalls)); + + exactRegistration.SetExecutionEnabled(false); + AssertRejectedRemoteCommandResult( + await context.HostServices.ExecuteRemoteCommandAsync( + CreateRemoteCommand(), + context.Token).ConfigureAwait(true)); + Assert.Equal((1, 0, 1), (exactCalls, fallbackCalls, acceptanceCalls)); AssertAcceptedRemoteCommandResult( await context.HostServices.ExecuteRemoteCommandAsync( CreateRemoteCommand(SecsItem.Ascii("FALLBACK")), context.Token).ConfigureAwait(true)); - Assert.Equal((1, 1), (exactCalls, fallbackCalls)); + Assert.Equal((1, 1, 2), (exactCalls, fallbackCalls, acceptanceCalls)); + + exactRegistration.SetExecutionEnabled(true); + AssertAcceptedRemoteCommandResult( + await context.HostServices.ExecuteRemoteCommandAsync( + CreateRemoteCommand(), + context.Token).ConfigureAwait(true)); + Assert.Equal((2, 1, 3), (exactCalls, fallbackCalls, acceptanceCalls)); exactRegistration.Dispose(); AssertAcceptedRemoteCommandResult( await context.HostServices.ExecuteRemoteCommandAsync( CreateRemoteCommand(), context.Token).ConfigureAwait(true)); - Assert.Equal((1, 2), (exactCalls, fallbackCalls)); + Assert.Equal((2, 2, 4), (exactCalls, fallbackCalls, acceptanceCalls)); } private static async Task