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 @@ -92,6 +92,10 @@
和可释放的 Host 单处理器注册;报警代码按原始字节透传,不推断位语义。
- 报警通知严格校验 W-Bit、三字段正文、单字节 Binary 代码、ASCII 文本、
Secondary 和应答值,并增加协议向量及真实 TCP 接受/拒绝测试。
- 增加可替换的远程命令消息对、不可变动态命令/参数/结果模型、Host 发送
API 和可释放的 Equipment 单处理器注册;整体与参数结果码原样返回。
- 远程命令严格校验 W-Bit、二字段请求/回复、参数形状、名称唯一性和
Binary 结果码,并增加动态嵌套 Item、无处理器拒绝及真实 TCP 往返测试。
- 增加定义、链接、S6F11 动态 Item 向量和真实 TCP 垂直测试,覆盖嵌套值、
空报告、配置拒绝、触发前快照与 Host 拒绝。

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ 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
41 changes: 34 additions & 7 deletions docs/GEM-FOUNDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +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 @@ -31,6 +32,7 @@
| 事件链接 | S2F35 | S2F36 |
| Collection Event | S6F11 | S6F12 |
| 报警通知 | S5F1 | S5F2 |
| 远程命令 | S2F41 | S2F42 |

这张表描述仓库自己的默认配置,不是复制的 SEMI 规范表,也不表示已经
核对 E30 的必选能力、状态条件、数据项定义、报告生命周期或应答枚举。
Expand Down Expand Up @@ -113,6 +115,24 @@ Host 使用 <code>RegisterAlarmNotificationHandler</code> 注册单一可释放
取消语义与 Collection Event 处理器一致。本切片不包含报警目录查询、
启停/屏蔽、历史、持久化或基于 GEM 状态的发送门控。

## 远程命令

Host 使用 <code>GemRemoteCommand</code> 携带动态命令标识和有序参数,再
通过 <code>ExecuteRemoteCommandAsync</code> 发起请求。命令、参数名称和
参数值均保持为任意非 null <code>SecsItem</code>;参数名称必须唯一,值可
包含嵌套 List。库不把现场命令限制为预生成枚举或 ASCII 字符串。

Equipment 使用 <code>RegisterRemoteCommandHandler</code> 注册单一可释放
处理器。处理器返回 <code>GemRemoteCommandResult</code>,其中包含一个原始
整体完成码和有序参数结果;每个参数结果保留动态名称与原始单字节代码。
Host 返回完整结果,不把非零代码折叠成异常,也不解释代码枚举。没有处理器
时 Equipment 使用 profile 的失败应答值和空参数结果立即回复。

处理器在锁外执行,注册快照、释放、异常和取消语义与报警处理器一致。当前
切片只要求请求参数名和结果参数名各自唯一,不推断结果必须覆盖哪些请求
参数。本切片不包含命令目录、参数 Schema、权限、排队、重试、幂等策略、
执行历史或基于 GEM 状态的命令门控。

## 严格失败边界

当前实现严格要求:
Expand All @@ -127,25 +147,32 @@ Host 使用 <code>RegisterAlarmNotificationHandler</code> 注册单一可释放
的标识和值列表形状必须完整且标识不能产生歧义;
- 报警通知是三元素 List,代码必须是恰好一个 Binary 字节,文本必须为
ASCII,报警标识保持为任意非 null 动态 Item;
- 远程命令请求是命令标识和参数列表组成的二元素 List,每个参数是名称与
值组成的二元素 List;回复是整体单字节代码和参数结果列表组成的二元素
List,每个参数结果必须包含唯一名称和单字节 Binary 代码;
- 报告配置请求和 Collection Event 必须设置 W-Bit,Secondary 的消息对和
应答字节必须与 profile 匹配;报警通知遵循相同要求。
应答字节必须与 profile 匹配;报警通知遵循相同要求;
- 远程命令必须设置 W-Bit 且 Secondary 消息对完全匹配;整体和参数结果码
保持原始字节,不限制为 profile 的成功/失败两个值。

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

## 标准与验证边界

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

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

当前报告和报警通知切片使用可替换工程 profile、动态
<code>SecsItem</code> 标识,并保留报警代码原始字节,不宣称已经核对
E30 的报告生命周期、报警代码位语义或应答枚举。下一优先级是远程命令,
再扩展报警管理以及更完整的 GEM 状态和错误恢复。
当前报告、报警通知和远程命令切片使用可替换工程 profile、动态
<code>SecsItem</code> 标识,并保留报警与命令结果原始代码,不宣称已经
核对 E30 的生命周期、代码语义或应答枚举。下一优先级是报警管理和更完整
的 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 @@ -62,6 +62,7 @@ await equipment.WaitUntilSelectedAsync(cancellationToken);
而不是修改 HSMS 状态机或根据 Active/Passive 猜测角色。

当前 GEM 切片已实现工程配置下的通讯建立、在线状态、变量、设备常量、
时钟、报告定义、事件链接、Collection Event 和最小报警通知;报警目录、
启停/历史与远程命令仍未实现。相关行为、事件循环要求和未核对边界见
时钟、报告定义、事件链接、Collection Event,以及最小报警通知和远程命令;
报警目录、启停/历史、命令目录/权限/调度仍未实现。相关行为、事件循环要求
和未核对边界见
[GEM-FOUNDATION.md](GEM-FOUNDATION.md),项目不声明 GEM 合规。
61 changes: 61 additions & 0 deletions src/SecsFrame.Gem/GemEquipmentServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public sealed class GemEquipmentServices : IDisposable
private readonly Dictionary<SecsItem, GemValueRegistration> _equipmentConstants = new();
private readonly Dictionary<SecsItem, GemReportDefinition> _reportDefinitions = new();
private readonly Dictionary<SecsItem, GemEventReportLink> _eventReportLinks = new();
private GemRemoteCommandRegistration? _remoteCommandHandler;
private int _disposed;

/// <summary>Creates Equipment GEM services without taking ownership of the endpoint.</summary>
Expand Down Expand Up @@ -89,6 +90,30 @@ public GemValueRegistration RegisterEquipmentConstant(
provider,
"equipment constant");

/// <summary>Registers the single application remote-command handler.</summary>
public GemRemoteCommandRegistration RegisterRemoteCommandHandler(
GemRemoteCommandHandler handler)
{
if (handler is null)
throw new ArgumentNullException(nameof(handler));

lock (_gate)
{
ThrowIfDisposed();
if (_remoteCommandHandler is not null)
{
throw new InvalidOperationException(
"A remote-command handler is already registered.");
}

var registration = new GemRemoteCommandRegistration(
handler,
UnregisterRemoteCommandHandler);
_remoteCommandHandler = registration;
return registration;
}
}

/// <summary>Collects and sends one linked Collection Event to the Host.</summary>
public async Task SendCollectionEventAsync(
SecsItem dataId,
Expand Down Expand Up @@ -174,6 +199,7 @@ public void Dispose()
_equipmentConstants.Clear();
_reportDefinitions.Clear();
_eventReportLinks.Clear();
_remoteCommandHandler = null;
}
_services.Dispose();
}
Expand All @@ -188,6 +214,7 @@ private void RegisterEquipmentRoutes()
_services.AddRoute(Profile.SetClock, HandleSetClockAsync);
_services.AddRoute(Profile.DefineReports, HandleDefineReportsAsync);
_services.AddRoute(Profile.LinkEventReports, HandleLinkEventReportsAsync);
_services.AddRoute(Profile.RemoteCommand, HandleRemoteCommandAsync);
}

private ValueTask<SecsMessage?> HandleOnlineAsync(
Expand Down Expand Up @@ -340,6 +367,30 @@ links is null
return null;
}

private async ValueTask<SecsMessage?> HandleRemoteCommandAsync(
HsmsPrimaryContext context,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
GemMessageCodec.RequireReplyExpected(context, "remote-command request");
var command = GemMessageCodec.DecodeRemoteCommand(
context.Message.RootItem);
GemRemoteCommandHandler? handler;
lock (_gate)
handler = _remoteCommandHandler?.Handler;

var result = handler is null
? new GemRemoteCommandResult(
Profile.FailedAcknowledgement,
Array.Empty<GemRemoteCommandParameterResult>())
: await handler(command, cancellationToken).ConfigureAwait(false) ??
throw new GemProtocolException(
"The remote-command handler returned null.");
return GemEndpointServices.CreateSecondary(
Profile.RemoteCommand,
GemMessageCodec.EncodeRemoteCommandResult(result));
}

private GemValueRegistration RegisterValue(
Dictionary<SecsItem, GemValueRegistration> registrations,
SecsItem identifier,
Expand Down Expand Up @@ -562,6 +613,16 @@ private void ThrowIfDisposed()
throw new ObjectDisposedException(nameof(GemEquipmentServices));
}

private void UnregisterRemoteCommandHandler(
GemRemoteCommandRegistration registration)
{
lock (_gate)
{
if (ReferenceEquals(_remoteCommandHandler, registration))
_remoteCommandHandler = null;
}
}

private sealed class ReportExecution
{
internal ReportExecution(
Expand Down
17 changes: 17 additions & 0 deletions src/SecsFrame.Gem/GemHostServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,23 @@ public async Task LinkEventReportsAsync(
"event-report-link reply"));
}

/// <summary>Sends a remote command and returns its uninterpreted result.</summary>
public async Task<GemRemoteCommandResult> ExecuteRemoteCommandAsync(
GemRemoteCommand command,
CancellationToken cancellationToken = default)
{
if (command is null)
throw new ArgumentNullException(nameof(command));

var response = await _services.SendRequestAsync(
Profile.RemoteCommand,
GemOperation.RemoteCommand,
GemMessageCodec.EncodeRemoteCommand(command),
cancellationToken).ConfigureAwait(false);
return GemMessageCodec.DecodeRemoteCommandResult(
response.Message.RootItem);
}

/// <summary>Registers the single application Collection Event handler.</summary>
public GemCollectionEventRegistration RegisterCollectionEventHandler(
GemCollectionEventHandler handler)
Expand Down
89 changes: 89 additions & 0 deletions src/SecsFrame.Gem/GemMessageCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,95 @@ internal static GemAlarmNotification DecodeAlarmNotification(SecsItem? root)
RequireAscii(body[2], operation, "text"));
}

internal static SecsItem EncodeRemoteCommand(GemRemoteCommand command)
{
if (command is null)
throw new ArgumentNullException(nameof(command));

var parameters = new SecsItem[command.Parameters.Count];
for (var index = 0; index < command.Parameters.Count; index++)
{
var parameter = command.Parameters[index];
parameters[index] = SecsItem.List(parameter.Name, parameter.Value);
}

return SecsItem.List(command.Command, SecsItem.List(parameters));
}

internal static GemRemoteCommand DecodeRemoteCommand(SecsItem? root)
{
const string operation = "remote-command request";
var body = RequireList(root, 2, operation);
var encoded = DecodeList(body[1], operation);
var parameters = new GemRemoteCommandParameter[encoded.Count];
var names = new HashSet<SecsItem>();
for (var index = 0; index < encoded.Count; index++)
{
var fields = RequireList(encoded[index], 2, operation);
if (!names.Add(fields[0]))
{
throw new GemProtocolException(
$"The {operation} contains duplicate parameter name at index {index}.");
}

parameters[index] = new GemRemoteCommandParameter(
fields[0],
fields[1]);
}

return new GemRemoteCommand(body[0], parameters);
}

internal static SecsItem EncodeRemoteCommandResult(
GemRemoteCommandResult result)
{
if (result is null)
throw new ArgumentNullException(nameof(result));

var parameters = new SecsItem[result.ParameterResults.Count];
for (var index = 0; index < result.ParameterResults.Count; index++)
{
var parameter = result.ParameterResults[index];
parameters[index] = SecsItem.List(
parameter.Name,
SecsItem.Binary(parameter.Acknowledgement));
}

return SecsItem.List(
SecsItem.Binary(result.Acknowledgement),
SecsItem.List(parameters));
}

internal static GemRemoteCommandResult DecodeRemoteCommandResult(
SecsItem? root)
{
const string operation = "remote-command reply";
var body = RequireList(root, 2, operation);
var encoded = DecodeList(body[1], operation);
var parameters = new GemRemoteCommandParameterResult[encoded.Count];
var names = new HashSet<SecsItem>();
for (var index = 0; index < encoded.Count; index++)
{
var fields = RequireList(encoded[index], 2, operation);
if (!names.Add(fields[0]))
{
throw new GemProtocolException(
$"The {operation} contains duplicate parameter-result name " +
$"at index {index}.");
}

parameters[index] = new GemRemoteCommandParameterResult(
fields[0],
RequireBinaryByte(
fields[1],
$"remote-command parameter result at index {index}"));
}

return new GemRemoteCommandResult(
RequireBinaryByte(body[0], "remote-command acknowledgement"),
parameters);
}

internal static SecsItem EncodeClock(DateTimeOffset value, GemClockCodec codec)
=> SecsItem.Ascii(codec.Encode(value));

Expand Down
Loading
Loading