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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@

### 新增

- 增加本机 <code>SecsFrame.CommunicationDemo</code> 通讯测试工具,提供
Active/Passive 显式连接配置、真实 TCP 回环、严格 SML 编辑与样例、
数据/控制消息操作,以及最多 500 条的会话内活动和结构化诊断视图。
- 增加独立 <code>SecsFrame.GuidedDemo</code> 分步成果演示,以固定输入逐步
执行真实 Select、动态 Item/SML、Primary/Secondary、Linktest 和 Separate
后新会话恢复,并保留每一步的可见证据。
- 增加 Demo 注意力模型、工程边界、每会话最多两轮的迭代账本和跨会话接续
约定;工程回环与固定脚本均不声明设备 Profile 或 SEMI 一致性。
- 增加独立 <code>SecsFrame.Soak</code> 可执行程序,在真实 TCP 待处理事务中
按显式随机种子选择双方 Separate 或端点销毁重建,恢复 Selected 后立即
验证新事务;普通 <code>dotnet test</code> 不执行该长时负载。
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ Item 用法与边界见
[docs/HSMS-PRIMARY-ROUTING.md](docs/HSMS-PRIMARY-ROUTING.md),
Host/Equipment 端点见
[docs/SECS-ENDPOINT-ROLES.md](docs/SECS-ENDPOINT-ROLES.md),
本机通讯工具、分步成果演示及跨会话迭代接续见
[docs/DEMOS.md](docs/DEMOS.md),
独立 GEM 基础行为见
[docs/GEM-FOUNDATION.md](docs/GEM-FOUNDATION.md),
SML 调试文本见
Expand Down Expand Up @@ -124,6 +126,24 @@ dotnet build SecsFrame.slnx -c Release
dotnet test SecsFrame.slnx -c Release --no-build
```

## Demo

通讯测试工具:

```bash
dotnet run --project demo/SecsFrame.CommunicationDemo/SecsFrame.CommunicationDemo.csproj
```

分步成果演示:

```bash
dotnet run --project demo/SecsFrame.GuidedDemo/SecsFrame.GuidedDemo.csproj
```

两个 Demo 均为本机 Interactive Server 应用。默认分别使用
<http://localhost:5080> 与 <http://localhost:5081>;工程回环和固定脚本
不代表设备消息 Profile、标准合规或生产就绪。

## 许可证

[MIT](LICENSE)
Expand Down
4 changes: 4 additions & 0 deletions SecsFrame.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<Solution>
<Folder Name="/demo/">
<Project Path="demo/SecsFrame.CommunicationDemo/SecsFrame.CommunicationDemo.csproj" />
<Project Path="demo/SecsFrame.GuidedDemo/SecsFrame.GuidedDemo.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/SecsFrame.Gem/SecsFrame.Gem.csproj" />
<Project Path="src/SecsFrame.Sml/SecsFrame.Sml.csproj" />
Expand Down
14 changes: 14 additions & 0 deletions demo/SecsFrame.CommunicationDemo/Components/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="app.css" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@inherits LayoutComponentBase

@Body
272 changes: 272 additions & 0 deletions demo/SecsFrame.CommunicationDemo/Components/Pages/Home.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
@page "/"
@implements IDisposable
@inject CommunicationWorkspace Workspace

<PageTitle>SecsFrame 通讯测试工具</PageTitle>

<div class="tool-shell">
<header class="tool-header">
<div class="brand-block">
<span class="brand-mark" aria-hidden="true">SF</span>
<div>
<h1>通讯测试工具</h1>
<p>SecsFrame 工程演示</p>
</div>
</div>
<div class="connection-summary" aria-live="polite">
<span class="state-dot state-@Workspace.State.ToString().ToLowerInvariant()"></span>
<strong>@Workspace.State</strong>
<span>@Workspace.Endpoint</span>
</div>
</header>

<main class="tool-main">
<section class="tool-pane connection-pane @(Workspace.IsConnected ? null : "is-focus")">
<div class="pane-heading">
<div>
<span class="step-label">01</span>
<h2>连接</h2>
</div>
</div>

<EditForm Model="_draft" OnValidSubmit="ConnectAsync">
<DataAnnotationsValidator />
<ValidationSummary class="validation-summary" />

<label class="check-row">
<InputCheckbox @bind-Value="_draft.UseLoopbackPeer"
disabled="@Workspace.IsConnected" />
<span>
<strong>本机回环端</strong>
<small>无需外部设备</small>
</span>
</label>

<div class="field-grid">
<label class="field field-wide">
<span>IP 地址</span>
<InputText @bind-Value="_draft.IpAddress"
disabled="@(Workspace.IsConnected || _draft.UseLoopbackPeer)" />
<ValidationMessage For="@(() => _draft.IpAddress)" />
</label>
<label class="field">
<span>端口</span>
<InputNumber @bind-Value="_draft.Port"
disabled="@Workspace.IsConnected" />
</label>
<label class="field">
<span>模式</span>
<InputSelect @bind-Value="_draft.ConnectionMode"
disabled="@(Workspace.IsConnected || _draft.UseLoopbackPeer)">
<option value="@HsmsConnectionMode.Active">Active</option>
<option value="@HsmsConnectionMode.Passive">Passive</option>
</InputSelect>
</label>
<label class="field field-wide">
<span>Session ID</span>
<InputNumber @bind-Value="_draft.SessionId"
disabled="@Workspace.IsConnected" />
</label>
</div>

<details class="timer-settings">
<summary>计时参数</summary>
<div class="field-grid timer-grid">
<label class="field">
<span>T3 秒</span>
<InputNumber @bind-Value="_draft.T3Seconds" disabled="@Workspace.IsConnected" />
</label>
<label class="field">
<span>T5 秒</span>
<InputNumber @bind-Value="_draft.T5Seconds" disabled="@Workspace.IsConnected" />
</label>
<label class="field">
<span>T6 秒</span>
<InputNumber @bind-Value="_draft.T6Seconds" disabled="@Workspace.IsConnected" />
</label>
<label class="field">
<span>T7 秒</span>
<InputNumber @bind-Value="_draft.T7Seconds" disabled="@Workspace.IsConnected" />
</label>
<label class="field">
<span>T8 秒</span>
<InputNumber @bind-Value="_draft.T8Seconds" disabled="@Workspace.IsConnected" />
</label>
</div>
</details>

<div class="primary-actions">
@if (Workspace.IsConnected)
{
<button type="button"
class="button button-secondary button-block"
disabled="@Workspace.IsBusy"
@onclick="DisconnectAsync">断开</button>
}
else
{
<button type="submit"
class="button button-primary button-block"
disabled="@Workspace.IsBusy">连接</button>
}
</div>
</EditForm>
</section>

<section class="tool-pane message-pane @(Workspace.State == HsmsSessionState.Selected ? "is-focus" : null)">
<div class="pane-heading">
<div>
<span class="step-label">02</span>
<h2>消息</h2>
</div>
<span class="editor-status">@EditorStatus</span>
</div>

<label class="field template-field">
<span>消息样例</span>
<select value="@_selectedTemplate" @onchange="SelectTemplate">
@foreach (var template in MessageTemplate.All)
{
<option value="@template.Id">@template.Name</option>
}
</select>
</label>

<label class="field message-editor">
<span>SML</span>
<textarea @bind="_sml"
spellcheck="false"
aria-label="SML 消息编辑器"></textarea>
</label>

@if (!string.IsNullOrEmpty(_operationError))
{
<p class="operation-error" role="alert">@_operationError</p>
}

<div class="message-actions">
<button type="button"
class="button button-primary"
disabled="@(!CanOperate)"
@onclick="SendAsync">发送</button>
<button type="button"
class="button button-secondary"
disabled="@(!CanOperate)"
@onclick="LinktestAsync">Linktest</button>
<button type="button"
class="button button-secondary"
disabled="@(!CanOperate)"
@onclick="SeparateAsync">Separate</button>
</div>
</section>

<section class="tool-pane activity-pane">
<div class="pane-heading">
<div>
<span class="step-label">03</span>
<h2>活动与诊断</h2>
</div>
<button type="button"
class="text-button"
disabled="@(Workspace.Activities.Count == 0)"
@onclick="Workspace.ClearActivities">清空</button>
</div>

<div class="activity-feed" aria-live="polite">
@if (Workspace.Activities.Count == 0)
{
<div class="empty-state">
<strong>暂无活动</strong>
<span>连接状态、消息和诊断将在这里出现</span>
</div>
}
else
{
<ol>
@foreach (var entry in Workspace.Activities)
{
<li class="activity-item tone-@entry.Tone.ToString().ToLowerInvariant()"
@key="entry.Id">
<div class="activity-meta">
<span>@entry.Timestamp.ToString("HH:mm:ss.fff")</span>
<span>@entry.Category</span>
</div>
<strong>@entry.Title</strong>
<p>@entry.Summary</p>
@if (!string.IsNullOrEmpty(entry.Detail))
{
<details>
<summary>查看详情</summary>
<pre>@entry.Detail</pre>
</details>
}
</li>
}
</ol>
}
</div>
</section>
</main>
</div>

@code {
private readonly ConnectionDraft _draft = new();
private string _selectedTemplate = MessageTemplate.All[0].Id;
private string _sml = MessageTemplate.All[0].Sml;
private string? _operationError;

private bool CanOperate =>
Workspace.State == HsmsSessionState.Selected &&
!Workspace.IsBusy;

private string EditorStatus => Workspace.State == HsmsSessionState.Selected
? "可发送"
: "等待 Selected";

protected override void OnInitialized()
=> Workspace.Changed += HandleWorkspaceChanged;

public void Dispose()
=> Workspace.Changed -= HandleWorkspaceChanged;

private async Task ConnectAsync()
=> await RunUiOperationAsync(() => Workspace.ConnectAsync(_draft));

private async Task DisconnectAsync()
=> await RunUiOperationAsync(Workspace.DisconnectAsync);

private async Task SendAsync()
=> await RunUiOperationAsync(() => Workspace.SendAsync(_sml));

private async Task LinktestAsync()
=> await RunUiOperationAsync(Workspace.LinktestAsync);

private async Task SeparateAsync()
=> await RunUiOperationAsync(Workspace.SeparateAsync);

private void SelectTemplate(ChangeEventArgs args)
{
_selectedTemplate = args.Value?.ToString() ??
MessageTemplate.All[0].Id;
var template = MessageTemplate.All
.Single(item => item.Id == _selectedTemplate);
_sml = template.Sml;
_operationError = null;
}

private async Task RunUiOperationAsync(Func<Task> operation)
{
_operationError = null;
try
{
await operation();
}
catch (Exception error)
{
_operationError = error.Message;
}
}

private void HandleWorkspaceChanged(object? sender, EventArgs args)
=> _ = InvokeAsync(StateHasChanged);
}
14 changes: 14 additions & 0 deletions demo/SecsFrame.CommunicationDemo/Components/Routes.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(MainLayout)">
<main class="not-found">
<h1>页面不存在</h1>
<a href="/">返回通讯工具</a>
</main>
</LayoutView>
</NotFound>
</Router>
11 changes: 11 additions & 0 deletions demo/SecsFrame.CommunicationDemo/Components/_Imports.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using SecsFrame
@using SecsFrame.CommunicationDemo
@using SecsFrame.CommunicationDemo.Components
@using SecsFrame.CommunicationDemo.Components.Layout
@using SecsFrame.CommunicationDemo.Models
@using SecsFrame.CommunicationDemo.Services
10 changes: 10 additions & 0 deletions demo/SecsFrame.CommunicationDemo/Models/ActivityEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace SecsFrame.CommunicationDemo.Models;

internal sealed record ActivityEntry(
long Id,
DateTimeOffset Timestamp,
ActivityEntryTone Tone,
string Category,
string Title,
string Summary,
string? Detail);
9 changes: 9 additions & 0 deletions demo/SecsFrame.CommunicationDemo/Models/ActivityEntryTone.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SecsFrame.CommunicationDemo.Models;

internal enum ActivityEntryTone
{
Neutral,
Success,
Warning,
Error,
}
Loading
Loading