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 @@ -102,6 +102,10 @@
在线状态和已解码命令决定是否交给执行处理器,未注册策略时保持现有行为。
- 策略拒绝使用 profile 失败应答和空参数结果,且不调用执行处理器;应用
改变决策后可在同一会话重试。增加注册生命周期和真实 TCP 状态门控测试。
- 增加可释放的 Equipment Collection Event 发送策略;应用可依据当前
通讯/在线状态、DATAID 和 CEID 决定是否采值并发送,未注册时保持现有行为。
- Collection Event 策略拒绝在状态变量提供器和 S6F11 事务之前短路;应用
改变决策后可在同一会话显式重试。增加注册生命周期和真实 TCP 状态测试。
- 增加可替换的报警目录查询消息对、不可变报警定义、Equipment 运行期注册
和 Host 全量/按动态标识查询;全量目录保持注册顺序,未知标识被忽略。
- 报警目录严格校验 W-Bit、请求 List、标识唯一性、三字段定义、单字节
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Deselect、Separate、T6/T7 和 Selected 数据门控。内部数据事务 actor
配置、动态消息收发、状态等待和单消费者事件流。
独立 <code>SecsFrame.Gem</code> 已提供可配置 GEM 通用行为,包含双向通讯
建立及应用接受策略、同会话显式重试恢复、上下线与应用转换策略、动态变量/
常量、应用托管时钟、报告定义、事件链接和 Collection Event、报警目录查询、
单报警发送启停,以及最小报警通知、远程命令链路和应用状态接受策略。
常量、应用托管时钟、报告定义、事件链接和带 Equipment 应用发送策略的
Collection Event、报警目录查询、单报警发送启停,以及最小报警通知、
远程命令链路和应用状态接受策略。
Item 用法与边界见
[docs/SECS-II-ITEMS.md](docs/SECS-II-ITEMS.md),消息集成见
[docs/SECS-MESSAGES.md](docs/SECS-MESSAGES.md),传输适配边界见
Expand Down
36 changes: 26 additions & 10 deletions docs/GEM-FOUNDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<code>SecsFrame.Gem</code> 是只依赖 <code>SecsFrame</code> 的独立程序集。
当前切片提供 Host/Equipment 两侧的通讯建立及应用接受策略、在线/离线及
应用转换策略、状态变量读取、设备常量读取、应用托管时钟、报告定义、事件
链接和 Collection Event,以及报警目录查询、单报警发送启停、最小报警
通知、远程命令链路与应用状态接受策略,不把 GEM 状态或消息目录放入 HSMS
状态机。
链接和带应用发送策略的 Collection Event,以及报警目录查询、单报警发送
启停、最小报警通知、远程命令链路与应用状态接受策略,不把 GEM 状态或消息
目录放入 HSMS 状态机。

<code>GemHostServices</code> 依赖 <code>SecsHost</code>;
<code>GemEquipmentServices</code> 依赖 <code>SecsEquipment</code>。服务不拥有
Expand Down Expand Up @@ -94,9 +94,10 @@ Equipment 可使用 <code>RegisterOnlineStateTransitionHandler</code> 注册
应用应遵守角色端点的单消费者事件循环约束,使决策和状态转换按请求顺序
执行。

当前切片仍没有把变量、常量、时钟、报告、报警或命令硬性门控在某个 GEM
状态上,也不自动重新建立通讯。这样可在尚未获得授权 E30 状态表前避免把
未经核对的状态条件固化为公共行为。
除前述通讯建立/在线转换策略外,后文还为远程命令和 Collection Event
提供显式应用策略。当前仍不把变量读取、常量、时钟、报警通知等其他操作
硬性门控在某个 GEM 状态上,也不自动重新建立通讯。这样可在尚未获得授权
E30 状态表前避免把未经核对的状态条件固化为公共行为。

## 动态数据与时钟

Expand Down Expand Up @@ -130,6 +131,20 @@ Host 使用 <code>DefineReportsAsync</code> 和
注册不会改变正在生成的这一条事件;提供器仍可能按应用取消或失败。值保持
任意动态 Item,包括嵌套 List 和空报告。

Equipment 可另用 <code>RegisterCollectionEventSendPolicyHandler</code>
注册单一可释放发送策略。配置有效的每次发送会把当前
<code>CommunicationState</code>、<code>OnlineState</code>、DATAID 和
CEID 交给策略。返回 <code>false</code> 时
<code>SendCollectionEventAsync</code> 抛出本地
<code>InvalidOperationException</code>;状态变量提供器不会执行,也不会
创建 S6F11 事务,因此 Host 不会收到消息。

没有注册策略时继续采值并发送,保持此前行为。策略、状态和报告执行计划在
Equipment 锁内一起快照,策略在锁外执行;释放注册只影响后续发送,已取得
的快照会完成。策略异常和取消直接传播给调用方。应用可以改变策略结果并在
同一 Selected 会话再次显式调用发送;库不自动重试,也不内置未经授权
E30-0526 核对的通讯/在线状态矩阵。

Host 使用 <code>RegisterCollectionEventHandler</code> 注册单一可释放处理器。
处理器返回 <code>true</code> 发送成功应答,返回 <code>false</code> 发送失败
应答;没有处理器时明确失败。释放注册只影响后续事件,已经取得处理器快照
Expand Down Expand Up @@ -255,7 +270,8 @@ Schema、权限、排队、自动重试、幂等策略或执行历史。
Passive 真实 TCP 下的双向通讯建立、首次建立拒绝后同会话恢复、已通讯
重新建立拒绝时的状态/身份保持与恢复、在线转换接受/拒绝与状态保持、异构
动态标识、报告定义、
事件链接、Collection Event 嵌套值与空报告、报警通知接受/拒绝、配置/事件
拒绝、报警目录全量/选择查询与释放快照、单报警发送启停与未知标识拒绝、
远程命令动态参数与结果、无处理器拒绝、应用状态拒绝短路与同会话重试、
时钟读写和 Linktest。它们是独立工程验证,不是 SEMI 一致性认证。
事件链接、Collection Event 嵌套值与空报告、应用发送策略拒绝零采值/零
事务和同会话重试、报警通知接受/拒绝、配置/事件拒绝、报警目录全量/选择
查询与释放快照、单报警发送启停与未知标识拒绝、远程命令动态参数与结果、
无处理器拒绝、应用状态拒绝短路与同会话重试、时钟读写和 Linktest。它们
是独立工程验证,不是 SEMI 一致性认证。
14 changes: 10 additions & 4 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ epoch 收口与 Passive 监听加固。后续切片继续扩展 GEM 通用行为
- [x] Equipment 在线/离线转换的应用决策、显式拒绝与状态保持。
- [x] Host/Equipment 双向通讯建立应用决策、显式拒绝与同会话重试恢复。
- [x] Equipment 入站远程命令的应用状态接受策略、拒绝短路与同会话重试。
- [ ] Collection Event、报警通知等其余业务消息的应用状态门控。
- [x] Equipment Collection Event 发送的应用状态策略、拒绝零采值/零事务
与同会话重试。
- [ ] 报警通知等其余业务消息的应用状态门控。
- [ ] 报警历史/批量控制、命令目录/权限/调度和更完整的状态/错误恢复。
- [ ] 使用授权 E30-0526 核对基础消息条件、数据项、应答值和状态转换。

Expand All @@ -58,9 +60,13 @@ epoch 收口与 Passive 监听加固。后续切片继续扩展 GEM 通用行为
不注册;拒绝不改变既有通讯状态或对端身份,恢复由应用在同一 Selected
会话显式重试,库不启动后台循环。远程命令接受策略也默认不注册;应用可
依据当前通讯/在线状态决定是否执行命令,库不内置未经 E30 核对的状态矩阵。
下一优先级是把相同的默认关闭模式扩展到 Equipment 主动业务上报,最小
切片先覆盖 Collection Event 发送入口,再评估报警通知、自动恢复、报警
历史和批量控制。
Collection Event 发送策略沿用相同的默认不注册模式;拒绝发生在动态值
采集和 S6F11 事务之前,应用改变决策后可在同一会话显式重试。

下一优先级是把此模式扩展到报警通知发送入口。最小切片应让应用依据当前
通讯/在线状态和通知内容决策,在既有单报警发送启停检查之后、创建 S5F1
事务之前拒绝,并继续保持默认不注册;随后再评估自动恢复、报警历史和批量
控制。

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

Expand Down
11 changes: 11 additions & 0 deletions src/SecsFrame.Gem/GemCollectionEventSendPolicyHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SecsFrame.Gem;

/// <summary>
/// Decides whether one configured Collection Event may collect values and be sent.
/// </summary>
public delegate ValueTask<bool> GemCollectionEventSendPolicyHandler(
GemCommunicationState communicationState,
GemOnlineState onlineState,
SecsItem dataId,
SecsItem eventId,
CancellationToken cancellationToken);
21 changes: 21 additions & 0 deletions src/SecsFrame.Gem/GemCollectionEventSendPolicyRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace SecsFrame.Gem;

/// <summary>Owns one Equipment Collection Event send-policy registration.</summary>
public sealed class GemCollectionEventSendPolicyRegistration : IDisposable
{
private Action<GemCollectionEventSendPolicyRegistration>? _unregister;

internal GemCollectionEventSendPolicyRegistration(
GemCollectionEventSendPolicyHandler handler,
Action<GemCollectionEventSendPolicyRegistration> unregister)
{
Handler = handler;
_unregister = unregister;
}

internal GemCollectionEventSendPolicyHandler Handler { get; }

/// <summary>Removes this exact policy registration. Repeated calls are harmless.</summary>
public void Dispose()
=> Interlocked.Exchange(ref _unregister, null)?.Invoke(this);
}
68 changes: 66 additions & 2 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 GemCollectionEventSendPolicyRegistration?
_collectionEventSendPolicyHandler;
private GemRemoteCommandAcceptanceRegistration?
_remoteCommandAcceptanceHandler;
private GemRemoteCommandRegistration? _remoteCommandHandler;
Expand Down Expand Up @@ -159,6 +161,34 @@ public GemRemoteCommandAcceptanceRegistration
}
}

/// <summary>
/// Registers the single application policy evaluated before Collection Event
/// value collection and sending.
/// </summary>
public GemCollectionEventSendPolicyRegistration
RegisterCollectionEventSendPolicyHandler(
GemCollectionEventSendPolicyHandler handler)
{
if (handler is null)
throw new ArgumentNullException(nameof(handler));

lock (_gate)
{
ThrowIfDisposed();
if (_collectionEventSendPolicyHandler is not null)
{
throw new InvalidOperationException(
"A Collection Event send-policy handler is already registered.");
}

var registration = new GemCollectionEventSendPolicyRegistration(
handler,
UnregisterCollectionEventSendPolicyHandler);
_collectionEventSendPolicyHandler = registration;
return registration;
}
}

/// <summary>Registers the single application remote-command handler.</summary>
public GemRemoteCommandRegistration RegisterRemoteCommandHandler(
GemRemoteCommandHandler handler)
Expand Down Expand Up @@ -218,7 +248,23 @@ public async Task SendCollectionEventAsync(
if (eventId is null)
throw new ArgumentNullException(nameof(eventId));

var executions = GetReportExecutions(eventId);
var executions = GetReportExecutions(
eventId,
out var sendPolicyHandler,
out var communicationState,
out var onlineState);
if (sendPolicyHandler is not null &&
!await sendPolicyHandler(
communicationState,
onlineState,
dataId,
eventId,
cancellationToken).ConfigureAwait(false))
{
throw new InvalidOperationException(
$"The application policy rejected Collection Event {eventId}.");
}

var reports = new GemCollectedReport[executions.Count];
for (var reportIndex = 0; reportIndex < executions.Count; reportIndex++)
{
Expand Down Expand Up @@ -306,6 +352,7 @@ public void Dispose()
_alarms.Clear();
_alarmCatalog.Clear();
_onlineStateTransitionHandler = null;
_collectionEventSendPolicyHandler = null;
_remoteCommandAcceptanceHandler = null;
_remoteCommandHandler = null;
}
Expand Down Expand Up @@ -775,7 +822,11 @@ private void ApplyEventReportLinks(
}
}

private IReadOnlyList<ReportExecution> GetReportExecutions(SecsItem eventId)
private IReadOnlyList<ReportExecution> GetReportExecutions(
SecsItem eventId,
out GemCollectionEventSendPolicyHandler? sendPolicyHandler,
out GemCommunicationState communicationState,
out GemOnlineState onlineState)
{
lock (_gate)
{
Expand Down Expand Up @@ -815,6 +866,9 @@ private IReadOnlyList<ReportExecution> GetReportExecutions(SecsItem eventId)
Array.AsReadOnly(providers));
}

sendPolicyHandler = _collectionEventSendPolicyHandler?.Handler;
communicationState = CommunicationState;
onlineState = OnlineState;
return Array.AsReadOnly(executions);
}
}
Expand Down Expand Up @@ -849,6 +903,16 @@ private void UnregisterRemoteCommandAcceptanceHandler(
}
}

private void UnregisterCollectionEventSendPolicyHandler(
GemCollectionEventSendPolicyRegistration registration)
{
lock (_gate)
{
if (ReferenceEquals(_collectionEventSendPolicyHandler, registration))
_collectionEventSendPolicyHandler = null;
}
}

private void UnregisterOnlineStateTransitionHandler(
GemOnlineStateTransitionRegistration registration)
{
Expand Down
26 changes: 26 additions & 0 deletions test/SecsFrame.Tests/GemEquipmentServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,32 @@ public async Task Remote_command_acceptance_handler_is_exact_disposable_and_repl
services.RegisterRemoteCommandAcceptanceHandler(handler));
}

[Fact]
public async Task Collection_event_send_policy_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));
GemCollectionEventSendPolicyHandler handler = static (_, _, _, _, _) =>
new ValueTask<bool>(true);

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

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