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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@
状态接受或拒绝 Host 在线/离线请求,未注册处理器时保持自动接受。
- 拒绝使用 profile 失败应答且双方状态保持不变;接受仍在 Secondary 完整
写出后更新 Equipment 状态。增加注册生命周期和真实 TCP 状态测试。
- 增加 Host/Equipment 双向可释放通讯建立策略;应用可依据请求方身份接受
或拒绝 S1F13,未注册策略时保持自动接受。
- 拒绝使用 profile 失败应答和空身份 List,双方既有通讯状态与对端身份均
保持不变;应用随后可在同一 Selected 会话显式重试恢复。增加双向注册
生命周期、首次建立拒绝/恢复和已通讯重建拒绝/恢复的真实 TCP 状态测试。
- 增加定义、链接、S6F11 动态 Item 向量和真实 TCP 垂直测试,覆盖嵌套值、
空报告、配置拒绝、触发前快照与 Host 拒绝。

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ Deselect、Separate、T6/T7 和 Selected 数据门控。内部数据事务 actor
进一步提供实际写出后启动的 T3、复合事务键关联、入站回复和会话失效
隔离。公共 <code>HsmsConnection</code> 将这些内部层组合成显式计时
配置、动态消息收发、状态等待和单消费者事件流。
独立 <code>SecsFrame.Gem</code> 已提供可配置 GEM 通用行为,包含通讯建立、
上下线与应用转换策略、动态变量/常量、应用托管时钟、报告定义、事件链接和
Collection Event、报警目录查询、单报警发送启停,以及最小报警通知和
远程命令链路。
独立 <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
41 changes: 29 additions & 12 deletions docs/GEM-FOUNDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
## 模块与范围

<code>SecsFrame.Gem</code> 是只依赖 <code>SecsFrame</code> 的独立程序集。
当前切片提供 Host/Equipment 两侧的通讯建立、在线/离线及应用转换策略、
状态变量读取、设备常量读取、应用托管时钟、报告定义、事件链接和
Collection Event,以及报警目录查询、单报警发送启停、最小报警通知与
远程命令链路,不把 GEM 状态或消息目录放入 HSMS 状态机。
当前切片提供 Host/Equipment 两侧的通讯建立及应用接受策略、在线/离线及
应用转换策略、状态变量读取、设备常量读取、应用托管时钟、报告定义、事件
链接和 Collection Event,以及报警目录查询、单报警发送启停、最小报警
通知与远程命令链路,不把 GEM 状态或消息目录放入 HSMS 状态机。

<code>GemHostServices</code> 依赖 <code>SecsHost</code>;
<code>GemEquipmentServices</code> 依赖 <code>SecsEquipment</code>。服务不拥有
Expand Down Expand Up @@ -65,17 +65,33 @@ await foreach (var connectionEvent in equipment.GetEventsAsync(cancellationToken
非 Selected 的状态事件后,这些状态会重置;因此只分派数据事件会遗漏
断线重置。

Host 与 Equipment 都可使用
<code>RegisterCommunicationEstablishmentHandler</code> 注册单一可释放
策略。每个结构有效的对端通讯建立请求都会把请求方
<code>GemIdentity</code> 交给处理器。返回 <code>false</code> 时服务发送
profile 的失败应答和空身份 List,发起方抛出包含操作和原始应答字节的
<code>GemRequestRejectedException</code>。双方均不更新
<code>CommunicationState</code> 或 <code>PeerIdentity</code>;如果请求是
已通讯状态下的重新建立,既有状态和身份也会保留。

返回 <code>true</code> 时服务先完整写出成功 Secondary,再记录请求方身份
并进入 <code>Communicating</code>。没有注册策略时继续自动接受,保持此前
行为。应用可以改变策略结果并在同一 Selected 会话再次调用
<code>EstablishCommunicationAsync</code>;库不自动重试、不创建计时器或
后台恢复循环。

Equipment 可使用 <code>RegisterOnlineStateTransitionHandler</code> 注册
单一可释放策略。每个结构有效的 Host 在线/离线请求都会把当前状态和目标
状态交给处理器;返回 <code>false</code> 时 Equipment 发送 profile 的失败
应答且保持当前状态,Host 抛出包含操作和原始应答字节的
<code>GemRequestRejectedException</code>,不会更新观察状态。返回
<code>true</code> 时仍先完整写出成功 Secondary,再更新 Equipment 状态。

没有注册处理器时继续自动接受,保持此前行为。处理器在服务锁外执行;释放
这两类策略没有注册处理器时都继续自动接受。处理器在服务锁外执行;释放
注册只影响后续请求,已经取得的处理器快照会完成。处理器异常和取消继续
传播给事件循环,不会被折叠为失败应答。同状态请求也会交给处理器。应用应
遵守角色端点的单消费者事件循环约束,使状态转换按请求顺序执行。
传播给事件循环,不会被折叠为失败应答。同状态在线请求也会交给处理器。
应用应遵守角色端点的单消费者事件循环约束,使决策和状态转换按请求顺序
执行。

当前切片仍没有把变量、常量、时钟、报告、报警或命令硬性门控在某个 GEM
状态上,也不自动重新建立通讯。这样可在尚未获得授权 E30 状态表前避免把
Expand Down Expand Up @@ -182,7 +198,8 @@ Host 返回完整结果,不把非零代码折叠成异常,也不解释代码
- 基础 Primary 设置 W-Bit,请求体符合 profile 对应的空 Body、List、
ASCII 或 Identity 结构;
- Secondary 的 Stream、Function 和 W-Bit 与配置完全匹配;
- 应答是单个 Binary 字节,接受的通讯建立应答包含两项 ASCII Identity;
- 通讯建立回复是确认字节与身份组成的二元素 List;接受时必须包含两项
ASCII Identity,策略拒绝时使用 profile 失败确认和空身份 List;
- 在线/离线请求必须设置 W-Bit 且 Body 为空;应用拒绝使用 profile 失败
应答,接受应答写出完成前不得改变 Equipment 在线状态;
- 状态变量和设备常量请求只引用已注册标识,提供器不得返回 null;
Expand All @@ -205,9 +222,8 @@ Host 返回完整结果,不把非零代码折叠成异常,也不解释代码

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

## 标准与验证边界

Expand All @@ -221,7 +237,8 @@ Host 返回完整结果,不把非零代码折叠成异常,也不解释代码
[STANDARDS.md](STANDARDS.md)。

当前证据包括三目标框架的严格输入测试,以及 Host Active / Equipment
Passive 真实 TCP 下的双向通讯建立、在线转换接受/拒绝与状态保持、异构
Passive 真实 TCP 下的双向通讯建立、首次建立拒绝后同会话恢复、已通讯
重新建立拒绝时的状态/身份保持与恢复、在线转换接受/拒绝与状态保持、异构
动态标识、报告定义、
事件链接、Collection Event 嵌套值与空报告、报警通知接受/拒绝、配置/事件
拒绝、报警目录全量/选择查询与释放快照、单报警发送启停与未知标识拒绝、
Expand Down
7 changes: 5 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,18 @@
- [x] Equipment 运行期报警目录注册与 Host 全量/按标识查询。
- [x] 单个已注册报警的发送启停、未知标识拒绝和通知发送门控。
- [x] Equipment 在线/离线转换的应用决策、显式拒绝与状态保持。
- [x] Host/Equipment 双向通讯建立应用决策、显式拒绝与同会话重试恢复。
- [ ] 报警历史/批量控制、命令目录/权限/调度和更完整的状态/错误恢复。
- [ ] 使用授权 E30-0526 核对基础消息条件、数据项、应答值和状态转换。

当前报告、报警通知和远程命令切片使用可替换工程 profile、动态
<code>SecsItem</code> 标识,并保留报警与命令结果原始代码,不宣称已经
核对 E30 的生命周期、代码语义或应答枚举。报警目录查询同样是非规范性
工程基线;单报警发送启停也使用显式可替换控制码。在线转换策略默认不注册,
保持既有宽松接受行为。下一优先级是通讯建立拒绝与重新建立恢复,再评估
业务消息状态门控、报警历史和批量控制。
保持既有宽松接受行为。通讯建立策略在 Host 与 Equipment 两侧同样默认
不注册;拒绝不改变既有通讯状态或对端身份,恢复由应用在同一 Selected
会话显式重试,库不启动后台循环。下一优先级是默认关闭、由应用配置的业务
消息状态门控最小切片,再评估自动恢复、报警历史和批量控制。

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

Expand Down
6 changes: 4 additions & 2 deletions docs/SECS-ENDPOINT-ROLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ await equipment.WaitUntilSelectedAsync(cancellationToken);
设备常量、时钟、报告定义、事件链接、Collection Event,以及最小报警通知
和远程命令;
报警目录查询支持运行期注册,Host 也可启停单个已注册报警的通知发送。
通讯建立拒绝、自动重新建立、业务消息状态门控、报警历史/批量控制、命令
目录/权限/调度仍未实现。相关行为、事件循环要求和未核对边界见
Host 与 Equipment 的 GEM 服务都可注册单一通讯建立策略;拒绝保留双方
既有通讯状态和对端身份,应用可在同一 Selected 会话显式重试。自动重新
建立、业务消息状态门控、报警历史/批量控制、命令目录/权限/调度仍未实现。
相关行为、事件循环要求和未核对边界见
[GEM-FOUNDATION.md](GEM-FOUNDATION.md),项目不声明 GEM 合规。
4 changes: 2 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,7 @@
的必选/可选边界;还需核对基础消息条件、COMMACK/ONLACK/OFLACK/TIACK
取值、MDLN 条件结构、空 SVID/ECID 列表、报告定义与链接生命周期、
Collection Event 数据结构和应答值、报警通知/目录/发送启停的数据结构与
控制码、应用在线转换策略、时间格式、状态转换与错误响应。
控制码、应用通讯建立/在线转换策略、时间格式、状态转换与错误响应。
- E172/E173:允许分发的 Schema/表示形式、版本标识、扩展点和验证规则。

## 版权与合规边界
Expand Down
11 changes: 11 additions & 0 deletions src/SecsFrame.Gem/GemCommunicationEstablishmentHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SecsFrame.Gem;

/// <summary>
/// Decides whether to accept a peer-requested communication establishment.
/// </summary>
/// <param name="peerIdentity">The identity supplied by the requesting peer.</param>
/// <param name="cancellationToken">Cancels the pending request.</param>
/// <returns><see langword="true"/> to accept the request; otherwise, <see langword="false"/>.</returns>
public delegate ValueTask<bool> GemCommunicationEstablishmentHandler(
GemIdentity peerIdentity,
CancellationToken cancellationToken);
21 changes: 21 additions & 0 deletions src/SecsFrame.Gem/GemCommunicationEstablishmentRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace SecsFrame.Gem;

/// <summary>Owns one communication-establishment handler registration.</summary>
public sealed class GemCommunicationEstablishmentRegistration : IDisposable
{
private Action<GemCommunicationEstablishmentRegistration>? _unregister;

internal GemCommunicationEstablishmentRegistration(
GemCommunicationEstablishmentHandler handler,
Action<GemCommunicationEstablishmentRegistration> unregister)
{
Handler = handler;
_unregister = unregister;
}

internal GemCommunicationEstablishmentHandler Handler { get; }

/// <summary>Removes this exact handler registration. Repeated calls are harmless.</summary>
public void Dispose()
=> Interlocked.Exchange(ref _unregister, null)?.Invoke(this);
}
66 changes: 63 additions & 3 deletions src/SecsFrame.Gem/GemEndpointServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ internal sealed class GemEndpointServices : IDisposable
{
private readonly SecsEndpoint _endpoint;
private readonly List<HsmsPrimaryRouteRegistration> _routes = new();
#if NET9_0_OR_GREATER
private readonly Lock _gate = new();
#else
private readonly object _gate = new();
#endif
private GemCommunicationEstablishmentRegistration?
_communicationEstablishmentHandler;
private GemIdentity? _peerIdentity;
private int _communicationState;
private int _onlineState;
Expand Down Expand Up @@ -66,6 +73,30 @@ internal async Task<GemIdentity> EstablishCommunicationAsync(
return reply.Identity;
}

internal GemCommunicationEstablishmentRegistration
RegisterCommunicationEstablishmentHandler(
GemCommunicationEstablishmentHandler handler)
{
if (handler is null)
throw new ArgumentNullException(nameof(handler));

lock (_gate)
{
ThrowIfDisposed();
if (_communicationEstablishmentHandler is not null)
{
throw new InvalidOperationException(
"A communication-establishment handler is already registered.");
}

var registration = new GemCommunicationEstablishmentRegistration(
handler,
UnregisterCommunicationEstablishmentHandler);
_communicationEstablishmentHandler = registration;
return registration;
}
}

internal async Task<GemIdentity> AreYouOnlineAsync(
CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -176,6 +207,9 @@ public void Dispose()
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;

lock (_gate)
_communicationEstablishmentHandler = null;

for (var index = _routes.Count - 1; index >= 0; index--)
_routes[index].Dispose();
_routes.Clear();
Expand All @@ -193,14 +227,26 @@ public void Dispose()
var identity = GemMessageCodec.DecodeIdentity(
context.Message.RootItem,
"communication-establishment request");
GemCommunicationEstablishmentHandler? handler;
lock (_gate)
{
ThrowIfDisposed();
handler = _communicationEstablishmentHandler?.Handler;
}

var accepted = handler is null ||
await handler(identity, cancellationToken).ConfigureAwait(false);
await ReplyAsync(
context,
Profile.EstablishCommunication,
GemMessageCodec.EncodeCommunicationReply(
Profile.AcceptedAcknowledgement,
Identity),
accepted
? Profile.AcceptedAcknowledgement
: Profile.FailedAcknowledgement,
accepted ? Identity : null),
cancellationToken).ConfigureAwait(false);
SetCommunicating(identity);
if (accepted)
SetCommunicating(identity);
return null;
}

Expand Down Expand Up @@ -241,6 +287,20 @@ private void ResetState()
Volatile.Write(ref _onlineState, (int)GemOnlineState.Offline);
}

private void UnregisterCommunicationEstablishmentHandler(
GemCommunicationEstablishmentRegistration registration)
{
lock (_gate)
{
if (ReferenceEquals(
_communicationEstablishmentHandler,
registration))
{
_communicationEstablishmentHandler = null;
}
}
}

private void ThrowIfDisposed()
{
if (Volatile.Read(ref _disposed) != 0)
Expand Down
9 changes: 9 additions & 0 deletions src/SecsFrame.Gem/GemEquipmentServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ public Task<GemIdentity> EstablishCommunicationAsync(
CancellationToken cancellationToken = default)
=> _services.EstablishCommunicationAsync(cancellationToken);

/// <summary>
/// Registers the single application policy for peer-requested communication
/// establishment.
/// </summary>
public GemCommunicationEstablishmentRegistration
RegisterCommunicationEstablishmentHandler(
GemCommunicationEstablishmentHandler handler)
=> _services.RegisterCommunicationEstablishmentHandler(handler);

/// <summary>Queries the Host identity.</summary>
public Task<GemIdentity> AreYouOnlineAsync(
CancellationToken cancellationToken = default)
Expand Down
9 changes: 9 additions & 0 deletions src/SecsFrame.Gem/GemHostServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ public Task<GemIdentity> EstablishCommunicationAsync(
CancellationToken cancellationToken = default)
=> _services.EstablishCommunicationAsync(cancellationToken);

/// <summary>
/// Registers the single application policy for peer-requested communication
/// establishment.
/// </summary>
public GemCommunicationEstablishmentRegistration
RegisterCommunicationEstablishmentHandler(
GemCommunicationEstablishmentHandler handler)
=> _services.RegisterCommunicationEstablishmentHandler(handler);

/// <summary>Queries the Equipment identity.</summary>
public Task<GemIdentity> AreYouOnlineAsync(
CancellationToken cancellationToken = default)
Expand Down
6 changes: 4 additions & 2 deletions src/SecsFrame.Gem/GemMessageCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ internal static GemIdentity DecodeIdentity(SecsItem? root, string operation)

internal static SecsItem EncodeCommunicationReply(
byte acknowledgement,
GemIdentity identity)
GemIdentity? identity)
=> SecsItem.List(
EncodeAcknowledgement(acknowledgement),
EncodeIdentity(identity));
identity is null
? SecsItem.List()
: EncodeIdentity(identity));

internal static (byte Acknowledgement, GemIdentity? Identity)
DecodeCommunicationReply(SecsItem? root)
Expand Down
Loading
Loading