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 @@ -100,6 +100,10 @@
和 Host 全量/按动态标识查询;全量目录保持注册顺序,未知标识被忽略。
- 报警目录严格校验 W-Bit、请求 List、标识唯一性、三字段定义、单字节
Binary 代码与 ASCII 文本,并增加注册释放快照和真实 TCP 查询测试。
- 增加可替换的单报警发送启停消息对与控制码 codec;Host 可控制已注册报警,
Equipment 原子更新注册状态,未知标识使用失败应答拒绝。
- 已注册报警默认允许发送;禁用后通知发送入口按调用时快照阻断该标识,目录
保持可见,释放并重新注册会恢复默认状态。增加严格协议向量和真实 TCP 测试。
- 增加定义、链接、S6F11 动态 Item 向量和真实 TCP 垂直测试,覆盖嵌套值、
空报告、配置拒绝、触发前快照与 Host 拒绝。

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Deselect、Separate、T6/T7 和 Selected 数据门控。内部数据事务 actor
配置、动态消息收发、状态等待和单消费者事件流。
独立 <code>SecsFrame.Gem</code> 已提供可配置 GEM 通用行为,包含通讯建立、
上下线、动态变量/常量、应用托管时钟、报告定义、事件链接和 Collection
Event、报警目录查询,以及最小报警通知和远程命令链路。Item 用法与边界见
Event、报警目录查询、单报警发送启停,以及最小报警通知和远程命令链路。
Item 用法与边界见
[docs/SECS-II-ITEMS.md](docs/SECS-II-ITEMS.md),消息集成见
[docs/SECS-MESSAGES.md](docs/SECS-MESSAGES.md),传输适配边界见
[docs/STREAMFRAME-ADAPTER.md](docs/STREAMFRAME-ADAPTER.md),状态机边界见
Expand Down
54 changes: 36 additions & 18 deletions docs/GEM-FOUNDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<code>SecsFrame.Gem</code> 是只依赖 <code>SecsFrame</code> 的独立程序集。
当前切片提供 Host/Equipment 两侧的通讯建立、在线/离线、状态变量读取、
设备常量读取、应用托管时钟、报告定义、事件链接和 Collection Event,
以及报警目录查询、最小报警通知与远程命令链路,不把 GEM 状态或消息目录
放入 HSMS 状态机。
以及报警目录查询、单报警发送启停、最小报警通知与远程命令链路,不把
GEM 状态或消息目录放入 HSMS 状态机。

<code>GemHostServices</code> 依赖 <code>SecsHost</code>;
<code>GemEquipmentServices</code> 依赖 <code>SecsEquipment</code>。服务不拥有
Expand All @@ -15,8 +15,8 @@
## 工程基线配置

<code>GemMessageProfile</code> 显式配置每个 Primary/Secondary 对、成功和
失败应答值以及时钟文本 codec。<code>CreateEngineeringBaseline()</code>
当前返回以下常见工程配置:
失败应答值、时钟文本 codec 以及报警发送启停控制码 codec。
<code>CreateEngineeringBaseline()</code> 当前返回以下常见工程配置:

| 操作 | Primary | Secondary |
|---|---:|---:|
Expand All @@ -32,13 +32,16 @@
| 事件链接 | S2F35 | S2F36 |
| Collection Event | S6F11 | S6F12 |
| 报警通知 | S5F1 | S5F2 |
| 报警发送启停 | S5F3 | S5F4 |
| 报警目录查询 | S5F5 | S5F6 |
| 远程命令 | S2F41 | S2F42 |

这张表描述仓库自己的默认配置,不是复制的 SEMI 规范表,也不表示已经
核对 E30 的必选能力、状态条件、数据项定义、报告生命周期或应答枚举。
默认成功应答为 Binary 0,失败应答为 Binary 1;时钟使用 UTC 的
<code>yyyyMMddHHmmssff</code> ASCII 工程格式。现场差异应使用独立 profile
默认成功应答为 Binary 0,失败应答为 Binary 1;报警发送允许/禁止码分别
为 Binary <code>0x80</code>/<code>0x00</code>;时钟使用 UTC 的
<code>yyyyMMddHHmmssff</code> ASCII 工程格式。这些值是非规范性工程
基线,仍需使用授权 E30-0526 与目标设备核对。现场差异应使用独立 profile
和 codec,不应修改严格基线或根据 Function 奇偶推断消息含义。

## 事件循环与状态
Expand Down Expand Up @@ -113,22 +116,33 @@ Equipment 使用 <code>GemAlarmNotification</code> 提供一个精确 Binary 代
Host 使用 <code>RegisterAlarmNotificationHandler</code> 注册单一可释放
处理器。处理器返回 <code>true</code> 发送成功应答,返回
<code>false</code> 或没有处理器时发送失败应答。注册快照、释放、异常和
取消语义与 Collection Event 处理器一致。本切片不包含报警启停/屏蔽、
历史、持久化或基于 GEM 状态的发送门控。
取消语义与 Collection Event 处理器一致。本切片不解释报警条件状态,也
不包含历史、持久化或基于 GEM 在线状态的发送门控。

Equipment 使用 <code>RegisterAlarm</code> 注册不可变
<code>GemAlarmDefinition</code>。报警定义保留原始单字节代码、动态报警
标识和七位 ASCII 文本;标识必须唯一。注册对象可精确释放并重新注册,全量
目录按当前注册顺序返回。
标识和七位 ASCII 文本;标识必须唯一。注册对象默认允许发送,并通过
<code>IsSendEnabled</code> 暴露当前发送许可。注册对象可精确释放并重新
注册,全量目录按当前注册顺序返回。

Host 使用 <code>ListAlarmsAsync</code> 查询目录。省略标识或传入空列表会
取得全量快照;传入唯一标识列表时按请求顺序返回已注册的匹配项,未知标识
被忽略。查询回复不携带额外失败应答,因此未知标识不会被转换为 T3 超时。
请求和回复都是一次事务快照;并发注册或释放只影响后续查询。

报警目录与 <code>SendAlarmNotificationAsync</code> 当前保持解耦。目录注册
不会自动限制原始报警通知发送,也没有启用状态。启停/屏蔽及其对通知发送的
影响需要在授权标准与现场行为核对后单独定义。
Host 使用 <code>SetAlarmSendEnabledAsync</code> 控制一个已注册报警。
请求是控制码和动态报警标识组成的二元素 List;控制码由
<code>GemAlarmControlCodec</code> 严格映射。Equipment 在构造成功应答前
原子更新注册状态,未知标识使用 profile 的失败应答拒绝。

禁用只阻断当前注册 ID 后续进入
<code>SendAlarmNotificationAsync</code> 的调用,不修改报警定义、目录或
通知代码位,也不改变报警条件。发送许可在调用入口取得一次快照;已经通过
检查的并发发送会继续完成。未注册 ID 的原始通知保持既有发送行为。释放并
重新注册会恢复默认允许状态,HSMS 会话变化不会重置注册状态。

当前 API 故意不提供“全部报警”启停、批量控制、报警条件屏蔽或历史。
这些语义以及控制码含义仍需依据授权 E30-0526 和目标设备互操作结果核对。

## 远程命令

Expand Down Expand Up @@ -164,6 +178,8 @@ Host 返回完整结果,不把非零代码折叠成异常,也不解释代码
ASCII,报警标识保持为任意非 null 动态 Item;
- 报警目录请求是唯一动态标识组成的 List,空 List 表示全量查询;回复是
报警定义 List,每项必须包含单字节 Binary 代码、唯一标识和 ASCII 文本;
- 报警发送启停请求是二元素 List,控制码必须是恰好一个 Binary 字节且为
profile 配置的允许或禁止码;未知报警标识使用失败应答拒绝;
- 远程命令请求是命令标识和参数列表组成的二元素 List,每个参数是名称与
值组成的二元素 List;回复是整体单字节代码和参数结果列表组成的二元素
List,每个参数结果必须包含唯一名称和单字节 Binary 代码;
Expand All @@ -174,7 +190,7 @@ Host 返回完整结果,不把非零代码折叠成异常,也不解释代码

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

Expand All @@ -183,13 +199,15 @@ Host 返回完整结果,不把非零代码折叠成异常,也不解释代码
公开 SEMI 目录当前标识 GEM 基线为 E30-0526。仍需使用团队合法获得的
副本核对消息条件、COMMACK/ONLACK/OFLACK/TIACK 语义、MDLN 条件结构、
空 SVID/ECID 列表行为、报告定义与链接生命周期、Collection Event 数据
结构和应答、报警通知/目录结构与代码位语义、时间格式选择、状态转换和
错误响应,以及远程命令结构、整体/参数结果代码语义。不得把标准正文、
结构和应答、报警通知/目录/发送启停结构、代码位与控制码语义、时间格式
选择、状态转换和错误响应,以及远程命令结构、整体/参数结果代码语义。
不得把标准正文、
消息表、状态图或 Schema 提交仓库。完整版本和版权边界见
[STANDARDS.md](STANDARDS.md)。

当前证据包括三目标框架的严格输入测试,以及 Host Active / Equipment
Passive 真实 TCP 下的双向通讯建立、上下线、异构动态标识、报告定义、
事件链接、Collection Event 嵌套值与空报告、报警通知接受/拒绝、配置/事件
拒绝、报警目录全量/选择查询与释放快照、远程命令动态参数与结果、无处理器
拒绝、时钟读写和 Linktest。它们是独立工程验证,不是 SEMI 一致性认证。
拒绝、报警目录全量/选择查询与释放快照、单报警发送启停与未知标识拒绝、
远程命令动态参数与结果、无处理器拒绝、时钟读写和 Linktest。它们是独立
工程验证,不是 SEMI 一致性认证。
6 changes: 4 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@
- [x] 报警通知的动态标识、代码透传、Host 处理器和 Equipment 发送链路。
- [x] 远程命令的动态参数/结果、Host 发送和 Equipment 处理链路。
- [x] Equipment 运行期报警目录注册与 Host 全量/按标识查询。
- [ ] 报警启停/历史、命令目录/权限/调度和更完整的状态/错误恢复。
- [x] 单个已注册报警的发送启停、未知标识拒绝和通知发送门控。
- [ ] 报警历史/批量控制、命令目录/权限/调度和更完整的状态/错误恢复。
- [ ] 使用授权 E30-0526 核对基础消息条件、数据项、应答值和状态转换。

当前报告、报警通知和远程命令切片使用可替换工程 profile、动态
<code>SecsItem</code> 标识,并保留报警与命令结果原始代码,不宣称已经
核对 E30 的生命周期、代码语义或应答枚举。报警目录查询同样是非规范性
工程基线;下一优先级是报警启停,再扩展更完整的 GEM 状态/错误恢复。
工程基线;单报警发送启停也使用显式可替换控制码。下一优先级是更完整的
GEM 状态/错误恢复,再评估报警历史与批量控制。

## 阶段 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 @@ -63,6 +63,7 @@ await equipment.WaitUntilSelectedAsync(cancellationToken);

当前 GEM 切片已实现工程配置下的通讯建立、在线状态、变量、设备常量、
时钟、报告定义、事件链接、Collection Event,以及最小报警通知和远程命令;
报警目录查询也支持运行期注册。报警启停/历史、命令目录/权限/调度仍未
实现。相关行为、事件循环要求和未核对边界见
报警目录查询支持运行期注册,Host 也可启停单个已注册报警的通知发送。
报警历史/批量控制、命令目录/权限/调度仍未实现。相关行为、事件循环要求和
未核对边界见
[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 | 通讯建立、上下线、动态变量/常量、时钟、报告定义、事件链接和 Collection Event 已有可替换 profile 的工程实现;规范性条件待核对 |
| GEM | SEMI E30-0526 | 通讯建立、上下线、动态变量/常量、时钟、报告/事件、报警通知/目录/发送启停和远程命令已有可替换 profile 的工程实现;规范性条件待核对 |
| SEDD | SEMI E172-0225 | 规划中 |
| SMN | SEMI E173-0721 | 规划中 |

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

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

/// <summary>Maps alarm-send control states to exact protocol bytes.</summary>
/// <remarks>
/// The mapping is an interoperability configuration and does not interpret
/// alarm notification code bits.
/// </remarks>
public sealed class GemAlarmControlCodec
{
/// <summary>Creates an unambiguous alarm-send control mapping.</summary>
public GemAlarmControlCodec(byte enabledCode, byte disabledCode)
{
if (enabledCode == disabledCode)
{
throw new ArgumentException(
"Enabled and disabled alarm-send codes must differ.",
nameof(disabledCode));
}

EnabledCode = enabledCode;
DisabledCode = disabledCode;
}

/// <summary>Gets the exact code used to enable alarm sending.</summary>
public byte EnabledCode { get; }

/// <summary>Gets the exact code used to disable alarm sending.</summary>
public byte DisabledCode { get; }

internal byte Encode(bool enabled)
=> enabled ? EnabledCode : DisabledCode;

internal bool Decode(byte code)
{
if (code == EnabledCode)
return true;
if (code == DisabledCode)
return false;

throw new GemProtocolException(
$"The alarm-send control code 0x{code:X2} is not configured.");
}
}
7 changes: 7 additions & 0 deletions src/SecsFrame.Gem/GemAlarmRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace SecsFrame.Gem;
public sealed class GemAlarmRegistration : IDisposable
{
private Action<GemAlarmRegistration>? _unregister;
private int _sendEnabled = 1;

internal GemAlarmRegistration(
GemAlarmDefinition definition,
Expand All @@ -16,8 +17,14 @@ internal GemAlarmRegistration(
/// <summary>Gets the exact registered alarm identifier.</summary>
public SecsItem AlarmId => Definition.AlarmId;

/// <summary>Gets whether notifications for this registration may be sent.</summary>
public bool IsSendEnabled => Volatile.Read(ref _sendEnabled) != 0;

internal GemAlarmDefinition Definition { get; }

internal void SetSendEnabled(bool enabled)
=> Volatile.Write(ref _sendEnabled, enabled ? 1 : 0);

/// <summary>Removes this exact registration. Repeated calls are harmless.</summary>
public void Dispose()
=> Interlocked.Exchange(ref _unregister, null)?.Invoke(this);
Expand Down
43 changes: 43 additions & 0 deletions src/SecsFrame.Gem/GemEquipmentServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ public async Task SendAlarmNotificationAsync(
if (notification is null)
throw new ArgumentNullException(nameof(notification));

lock (_gate)
{
ThrowIfDisposed();
if (_alarms.TryGetValue(notification.AlarmId, out var registration) &&
!registration.IsSendEnabled)
{
throw new InvalidOperationException(
$"Alarm sending is disabled for {notification.AlarmId}.");
}
}

var response = await _services.SendRequestAsync(
Profile.AlarmNotification,
GemOperation.AlarmNotification,
Expand Down Expand Up @@ -244,6 +255,7 @@ private void RegisterEquipmentRoutes()
_services.AddRoute(Profile.LinkEventReports, HandleLinkEventReportsAsync);
_services.AddRoute(Profile.RemoteCommand, HandleRemoteCommandAsync);
_services.AddRoute(Profile.ListAlarms, HandleListAlarmsAsync);
_services.AddRoute(Profile.AlarmSendControl, HandleAlarmSendControlAsync);
}

private ValueTask<SecsMessage?> HandleOnlineAsync(
Expand Down Expand Up @@ -456,6 +468,37 @@ links is null
GemMessageCodec.EncodeAlarmDefinitions(definitions)));
}

private ValueTask<SecsMessage?> HandleAlarmSendControlAsync(
HsmsPrimaryContext context,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
GemMessageCodec.RequireReplyExpected(
context,
"alarm-send control request");
var control = GemMessageCodec.DecodeAlarmSendControl(
context.Message.RootItem,
Profile.AlarmControlCodec);
var accepted = false;
lock (_gate)
{
ThrowIfDisposed();
if (_alarms.TryGetValue(control.AlarmId, out var registration))
{
registration.SetSendEnabled(control.Enabled);
accepted = true;
}
}

return new ValueTask<SecsMessage?>(
GemEndpointServices.CreateSecondary(
Profile.AlarmSendControl,
GemMessageCodec.EncodeAcknowledgement(
accepted
? Profile.AcceptedAcknowledgement
: Profile.FailedAcknowledgement)));
}

private GemValueRegistration RegisterValue(
Dictionary<SecsItem, GemValueRegistration> registrations,
SecsItem identifier,
Expand Down
21 changes: 21 additions & 0 deletions src/SecsFrame.Gem/GemHostServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,27 @@ public async Task<IReadOnlyList<GemAlarmDefinition>> ListAlarmsAsync(
response.Message.RootItem);
}

/// <summary>Enables or disables sending for one registered Equipment alarm.</summary>
public async Task SetAlarmSendEnabledAsync(
SecsItem alarmId,
bool enabled,
CancellationToken cancellationToken = default)
{
var response = await _services.SendRequestAsync(
Profile.AlarmSendControl,
GemOperation.SetAlarmSendEnabled,
GemMessageCodec.EncodeAlarmSendControl(
alarmId,
enabled,
Profile.AlarmControlCodec),
cancellationToken).ConfigureAwait(false);
_services.RequireAccepted(
GemOperation.SetAlarmSendEnabled,
GemMessageCodec.DecodeAcknowledgement(
response.Message.RootItem,
"alarm-send control reply"));
}

/// <summary>Registers the single application Collection Event handler.</summary>
public GemCollectionEventRegistration RegisterCollectionEventHandler(
GemCollectionEventHandler handler)
Expand Down
Loading
Loading