Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
API 和可释放的 Equipment 单处理器注册;整体与参数结果码原样返回。
- 远程命令严格校验 W-Bit、二字段请求/回复、参数形状、名称唯一性和
Binary 结果码,并增加动态嵌套 Item、无处理器拒绝及真实 TCP 往返测试。
- 增加可释放的 Equipment 远程命令接受策略;应用可依据当前通讯状态、
在线状态和已解码命令决定是否交给执行处理器,未注册策略时保持现有行为。
- 策略拒绝使用 profile 失败应答和空参数结果,且不调用执行处理器;应用
改变决策后可在同一会话重试。增加注册生命周期和真实 TCP 状态门控测试。
- 增加可替换的报警目录查询消息对、不可变报警定义、Equipment 运行期注册
和 Host 全量/按动态标识查询;全量目录保持注册顺序,未知标识被忽略。
- 报警目录严格校验 W-Bit、请求 List、标识唯一性、三字段定义、单字节
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Deselect、Separate、T6/T7 和 Selected 数据门控。内部数据事务 actor
独立 <code>SecsFrame.Gem</code> 已提供可配置 GEM 通用行为,包含双向通讯
建立及应用接受策略、同会话显式重试恢复、上下线与应用转换策略、动态变量/
常量、应用托管时钟、报告定义、事件链接和 Collection Event、报警目录查询、
单报警发送启停,以及最小报警通知和远程命令链路。
单报警发送启停,以及最小报警通知、远程命令链路和应用状态接受策略。
Item 用法与边界见
[docs/SECS-II-ITEMS.md](docs/SECS-II-ITEMS.md),消息集成见
[docs/SECS-MESSAGES.md](docs/SECS-MESSAGES.md),传输适配边界见
Expand Down
35 changes: 25 additions & 10 deletions docs/GEM-FOUNDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
当前切片提供 Host/Equipment 两侧的通讯建立及应用接受策略、在线/离线及
应用转换策略、状态变量读取、设备常量读取、应用托管时钟、报告定义、事件
链接和 Collection Event,以及报警目录查询、单报警发送启停、最小报警
通知与远程命令链路,不把 GEM 状态或消息目录放入 HSMS 状态机。
通知、远程命令链路与应用状态接受策略,不把 GEM 状态或消息目录放入 HSMS
状态机。

<code>GemHostServices</code> 依赖 <code>SecsHost</code>;
<code>GemEquipmentServices</code> 依赖 <code>SecsEquipment</code>。服务不拥有
Expand Down Expand Up @@ -186,10 +187,22 @@ Equipment 使用 <code>RegisterRemoteCommandHandler</code> 注册单一可释放
Host 返回完整结果,不把非零代码折叠成异常,也不解释代码枚举。没有处理器
时 Equipment 使用 profile 的失败应答值和空参数结果立即回复。

处理器在锁外执行,注册快照、释放、异常和取消语义与报警处理器一致。当前
切片只要求请求参数名和结果参数名各自唯一,不推断结果必须覆盖哪些请求
参数。本切片不包含命令目录、参数 Schema、权限、排队、重试、幂等策略、
执行历史或基于 GEM 状态的命令门控。
Equipment 可另用 <code>RegisterRemoteCommandAcceptanceHandler</code>
注册单一可释放接受策略。存在执行处理器时,每个结构有效的请求先把当前
<code>CommunicationState</code>、<code>OnlineState</code> 和已解码命令
交给策略。返回 <code>false</code> 时 Equipment 使用 profile 的失败应答值
和空参数结果回复,执行处理器不会被调用,双方 GEM 状态不变;Host 仍返回
原始 <code>GemRemoteCommandResult</code>,不会把非零结果转换为异常。

没有注册接受策略时命令直接进入执行处理器,保持此前行为。没有执行处理器
时请求仍立即失败,接受策略不会执行。两个处理器在 Equipment 锁内按请求
一起快照并在锁外按策略、执行顺序调用;释放注册只影响后续请求,已取得的
快照会完成。策略或执行处理器的异常和取消继续传播给事件循环,不会被折叠
为失败应答。

当前切片只要求请求参数名和结果参数名各自唯一,不推断结果必须覆盖哪些
请求参数,也不内置任何通讯/在线状态矩阵。本切片不包含命令目录、参数
Schema、权限、排队、自动重试、幂等策略或执行历史。

## 严格失败边界

Expand Down Expand Up @@ -218,12 +231,14 @@ Host 返回完整结果,不把非零代码折叠成异常,也不解释代码
- 报告配置请求和 Collection Event 必须设置 W-Bit,Secondary 的消息对和
应答字节必须与 profile 匹配;报警通知遵循相同要求;
- 远程命令必须设置 W-Bit 且 Secondary 消息对完全匹配;整体和参数结果码
保持原始字节,不限制为 profile 的成功/失败两个值。
保持原始字节,不限制为 profile 的成功/失败两个值;接受策略拒绝时使用
profile 失败值和空参数结果,且不得调用执行处理器。

畸形输入和提供器失败会作为异常返回应用事件循环;语义有效但引用未知变量
或报告的配置使用失败应答拒绝。本切片尚未实现自动 S9Fx、错误 Secondary、
自动重新建立、业务消息状态门控、报警历史/批量控制、命令目录/权限/调度
或完整 GEM 错误恢复,这些行为不能从当前 API 推断为标准合规。
自动重新建立、远程命令以外的业务消息状态门控、报警历史/批量控制、命令
目录/权限/调度或完整 GEM 错误恢复,这些行为不能从当前 API 推断为标准
合规。

## 标准与验证边界

Expand All @@ -242,5 +257,5 @@ Passive 真实 TCP 下的双向通讯建立、首次建立拒绝后同会话恢
动态标识、报告定义、
事件链接、Collection Event 嵌套值与空报告、报警通知接受/拒绝、配置/事件
拒绝、报警目录全量/选择查询与释放快照、单报警发送启停与未知标识拒绝、
远程命令动态参数与结果、无处理器拒绝、时钟读写和 Linktest。它们是独立
工程验证,不是 SEMI 一致性认证。
远程命令动态参数与结果、无处理器拒绝、应用状态拒绝短路与同会话重试、
时钟读写和 Linktest。它们是独立工程验证,不是 SEMI 一致性认证。
9 changes: 7 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
- [x] 单个已注册报警的发送启停、未知标识拒绝和通知发送门控。
- [x] Equipment 在线/离线转换的应用决策、显式拒绝与状态保持。
- [x] Host/Equipment 双向通讯建立应用决策、显式拒绝与同会话重试恢复。
- [x] Equipment 入站远程命令的应用状态接受策略、拒绝短路与同会话重试。
- [ ] Collection Event、报警通知等其余业务消息的应用状态门控。
- [ ] 报警历史/批量控制、命令目录/权限/调度和更完整的状态/错误恢复。
- [ ] 使用授权 E30-0526 核对基础消息条件、数据项、应答值和状态转换。

Expand All @@ -53,8 +55,11 @@
工程基线;单报警发送启停也使用显式可替换控制码。在线转换策略默认不注册,
保持既有宽松接受行为。通讯建立策略在 Host 与 Equipment 两侧同样默认
不注册;拒绝不改变既有通讯状态或对端身份,恢复由应用在同一 Selected
会话显式重试,库不启动后台循环。下一优先级是默认关闭、由应用配置的业务
消息状态门控最小切片,再评估自动恢复、报警历史和批量控制。
会话显式重试,库不启动后台循环。远程命令接受策略也默认不注册;应用可
依据当前通讯/在线状态决定是否执行命令,库不内置未经 E30 核对的状态矩阵。
下一优先级是把相同的默认关闭模式扩展到 Equipment 主动业务上报,最小
切片先覆盖 Collection Event 发送入口,再评估报警通知、自动恢复、报警
历史和批量控制。

## 阶段 3:元数据与工具

Expand Down
5 changes: 3 additions & 2 deletions docs/SECS-ENDPOINT-ROLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ await equipment.WaitUntilSelectedAsync(cancellationToken);
报警目录查询支持运行期注册,Host 也可启停单个已注册报警的通知发送。
Host 与 Equipment 的 GEM 服务都可注册单一通讯建立策略;拒绝保留双方
既有通讯状态和对端身份,应用可在同一 Selected 会话显式重试。自动重新
建立、业务消息状态门控、报警历史/批量控制、命令目录/权限/调度仍未实现。
相关行为、事件循环要求和未核对边界见
建立仍未实现。Equipment 入站远程命令可注册默认关闭的应用状态接受策略;
Collection Event、报警通知等其他业务消息状态门控,以及报警历史/批量
控制、命令目录/权限/调度仍未实现。相关行为、事件循环要求和未核对边界见
[GEM-FOUNDATION.md](GEM-FOUNDATION.md),项目不声明 GEM 合规。
5 changes: 3 additions & 2 deletions docs/STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | 规划中 |

Expand Down Expand Up @@ -39,7 +39,8 @@
的必选/可选边界;还需核对基础消息条件、COMMACK/ONLACK/OFLACK/TIACK
取值、MDLN 条件结构、空 SVID/ECID 列表、报告定义与链接生命周期、
Collection Event 数据结构和应答值、报警通知/目录/发送启停的数据结构与
控制码、应用通讯建立/在线转换策略、时间格式、状态转换与错误响应。
控制码、应用通讯建立/在线转换策略、远程命令执行条件与状态门控、时间
格式、状态转换与错误响应。
- E172/E173:允许分发的 Schema/表示形式、版本标识、扩展点和验证规则。

## 版权与合规边界
Expand Down
74 changes: 70 additions & 4 deletions src/SecsFrame.Gem/GemEquipmentServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public sealed class GemEquipmentServices : IDisposable
private readonly Dictionary<SecsItem, GemAlarmRegistration> _alarms = new();
private readonly List<GemAlarmRegistration> _alarmCatalog = new();
private GemOnlineStateTransitionRegistration? _onlineStateTransitionHandler;
private GemRemoteCommandAcceptanceRegistration?
_remoteCommandAcceptanceHandler;
private GemRemoteCommandRegistration? _remoteCommandHandler;
private int _disposed;

Expand Down Expand Up @@ -129,6 +131,34 @@ public GemOnlineStateTransitionRegistration RegisterOnlineStateTransitionHandler
}
}

/// <summary>
/// Registers the single application policy evaluated before remote-command
/// execution.
/// </summary>
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;
}
}

/// <summary>Registers the single application remote-command handler.</summary>
public GemRemoteCommandRegistration RegisterRemoteCommandHandler(
GemRemoteCommandHandler handler)
Expand Down Expand Up @@ -276,6 +306,7 @@ public void Dispose()
_alarms.Clear();
_alarmCatalog.Clear();
_onlineStateTransitionHandler = null;
_remoteCommandAcceptanceHandler = null;
_remoteCommandHandler = null;
}
_services.Dispose();
Expand Down Expand Up @@ -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<GemRemoteCommandParameterResult>())
: await handler(command, cancellationToken).ConfigureAwait(false) ??
Array.Empty<GemRemoteCommandParameterResult>());
}
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));
Expand Down Expand Up @@ -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)
{
Expand Down
10 changes: 10 additions & 0 deletions src/SecsFrame.Gem/GemRemoteCommandAcceptanceHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace SecsFrame.Gem;

/// <summary>
/// Decides whether a decoded remote command may be passed to its executor.
/// </summary>
public delegate ValueTask<bool> GemRemoteCommandAcceptanceHandler(
GemCommunicationState communicationState,
GemOnlineState onlineState,
GemRemoteCommand command,
CancellationToken cancellationToken);
21 changes: 21 additions & 0 deletions src/SecsFrame.Gem/GemRemoteCommandAcceptanceRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace SecsFrame.Gem;

/// <summary>Owns one Equipment remote-command acceptance registration.</summary>
public sealed class GemRemoteCommandAcceptanceRegistration : IDisposable
{
private Action<GemRemoteCommandAcceptanceRegistration>? _unregister;

internal GemRemoteCommandAcceptanceRegistration(
GemRemoteCommandAcceptanceHandler handler,
Action<GemRemoteCommandAcceptanceRegistration> unregister)
{
Handler = handler;
_unregister = unregister;
}

internal GemRemoteCommandAcceptanceHandler Handler { get; }

/// <summary>Removes this exact handler registration. Repeated calls are harmless.</summary>
public void Dispose()
=> Interlocked.Exchange(ref _unregister, null)?.Invoke(this);
}
27 changes: 27 additions & 0 deletions test/SecsFrame.Tests/GemEquipmentServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>(true);

Assert.Throws<ArgumentNullException>(() =>
services.RegisterRemoteCommandAcceptanceHandler(null!));
var first =
services.RegisterRemoteCommandAcceptanceHandler(handler);
Assert.Throws<InvalidOperationException>(() =>
services.RegisterRemoteCommandAcceptanceHandler(handler));
first.Dispose();
first.Dispose();
using var replacement =
services.RegisterRemoteCommandAcceptanceHandler(handler);
services.Dispose();
Assert.Throws<ObjectDisposedException>(() =>
services.RegisterRemoteCommandAcceptanceHandler(handler));
}

[Fact]
public async Task Alarm_registration_is_exact_disposable_and_replaceable()
{
Expand Down
Loading
Loading