OpenRobot.Edge.AppSdk
0.2.0
dotnet add package OpenRobot.Edge.AppSdk --version 0.2.0
NuGet\Install-Package OpenRobot.Edge.AppSdk -Version 0.2.0
<PackageReference Include="OpenRobot.Edge.AppSdk" Version="0.2.0" />
<PackageVersion Include="OpenRobot.Edge.AppSdk" Version="0.2.0" />
<PackageReference Include="OpenRobot.Edge.AppSdk" />
paket add OpenRobot.Edge.AppSdk --version 0.2.0
#r "nuget: OpenRobot.Edge.AppSdk, 0.2.0"
#:package OpenRobot.Edge.AppSdk@0.2.0
#addin nuget:?package=OpenRobot.Edge.AppSdk&version=0.2.0
#tool nuget:?package=OpenRobot.Edge.AppSdk&version=0.2.0
OpenRobot.Edge.AppSdk
边缘应用接入 SDK:连接 EdgeWebApi、自动注册(版本 / 日志路径)、树形配置声明与热更新、 运行时声明注入、业务状态声明、本地配置读取与覆盖、NLog 文件日志与高频日志节流、 状态上报、业务数据写入。
应用只写业务逻辑,配置界面、数据存储、状态展示、控制面板交给平台。
快速开始
var builder = ModuleHostBuilder.Create(args);
builder.Services.AddLogging();
var edgeOptions = new EdgeAppOptions
{
AppId = "EdgeCDS", // = IPC ClientId = 平台侧身份,只配这一处
DisplayName = "边缘数据服务",
Version = "26.10.09.1",
Ipc = { Transport = IpcTransportKind.NamedPipe },
LogDirectory = "/var/log/openrobot/edgecds",
};
edgeOptions.Variables.Add(new AppVariableDeclarationEntry
{
Key = "apps.EdgeCDS.collect.interval",
Title = "采集周期",
Type = AppVariableTypes.Int,
Default = 1000,
Min = 100,
Max = 60000,
Editable = true,
});
edgeOptions.Collections.Add(new AppCollectionDeclaration
{
Name = "alarms",
Mode = AppCollectionModes.Log,
TtlSeconds = 30 * 24 * 3600,
Indexed = ["$.level"],
});
// 本地配置 → 自动读取,按优先级叠加(见「配置读取与覆盖」)
edgeOptions.ConfigFiles.Add("config/Uspeed.json");
builder.Services.AddEdgeApp(edgeOptions);
// 文件日志(显式启用;不调用就只走宿主现有的日志)
builder.Services.AddEdgeAppLogging(edgeOptions);
// 让宿主替我们管连接生命周期:PostInitialize 连、Shutdown 断
builder.AddModule<EdgeAppModule>();
await using var host = builder.Build();
await host.RunAsync();
AddEdgeApp 只注册、不连接,与 Communication 库同一约定。连接由宿主发起,两种接法去下面挑一种。
选择宿主接法
一、Modularity(推荐)
[DependsOn(typeof(EdgeAppModule))] // 声明依赖,保证拓扑排序里排在 SDK 之后
public sealed class EdgeCdsModule : StartupModule
{
private readonly EdgeAppClient _edge;
public EdgeCdsModule(EdgeAppClient edge) => _edge = edge;
public override void PostInitialize()
{
// 注册回调要在连接之前 —— 重连后平台可能立刻补发,晚注册就漏了第一条
_edge.ConfigChanged = OnConfigChanged;
}
private Task OnConfigChanged(AppConfigChangedPayload payload)
{
// SDK 已经在回调之前把新值刷进缓存了,这里直接读就是新值。
// payload.ChangedKeys 告诉你是哪些键变了(null = 整份重载),可用来做增量应用。
var interval = _edge.Config.Get<int>("apps.EdgeCDS.collect.interval");
RestartCollector(interval);
return Task.CompletedTask;
}
}
EdgeAppModule 在 PostInitialize 发起连接、在 WaitReadyAsync 最多等 5 秒。
连不上不算启动失败:超时只记一条 Warning,应用按离线模式继续跑,后台会持续重连。
二、Generic Host
services.AddEdgeApp(...);
public sealed class EdgeAppHostedService : IHostedService
{
private readonly EdgeAppRunner _runner;
public EdgeAppHostedService(EdgeAppRunner runner) => _runner = runner;
public Task StartAsync(CancellationToken ct) => _runner.StartAsync(ct);
public Task StopAsync(CancellationToken ct) => _runner.StopAsync();
}
需要知道「到底连上没有」时用 _runner.WaitForConnectedAsync(TimeSpan);它超时返回 false 而不是抛异常。
业务代码
注入 EdgeAppClient:
await edge.ReportStateAsync(new { running = true, throughput = 128 });
await edge.UpsertAsync("devices", "dev-01", device);
await edge.AppendAsync("alarms", new { level = "error", code = "E1024" });
// 注册控制点(在 PostInitialize 里做,别等指令到了再注册)
edge.OnControl("restartCollector", async (cmd, ct) =>
{
if (_collector.IsBusy) return new AppControlResultPayload { Ok = false, Message = "正在采集中,稍后再试" };
await _collector.RestartAsync(ct);
return new AppControlResultPayload { Ok = true, Message = "已重启" };
});
控制点要先在 EdgeAppOptions.Controls 里声明(界面才有按钮),
再在运行时用 OnControl 挂执行器 —— 声明说「有什么按钮」,执行器说「按下去做什么」。
执行器自己判断能不能做:声明不是许可。
| 方法 | 用途 | 失败行为 |
|---|---|---|
ReportStateAsync(state, controls) |
状态上报,建议 2–5 秒一次 | 只记日志 |
UpsertAsync(collection, key, payload) |
按唯一键写入 | 只记日志 |
AppendAsync(collection, payload) |
追加一条(键由平台生成) | 只记日志 |
WriteAsync(collection, records) |
批量写入 | 只记日志 |
GetConfigAsync() |
拉一次生效配置并刷新 Config |
返回 null,保留旧缓存 |
RegisterAsync() / DeclareVariablesAsync() |
手动重发声明 | 抛给调用方 |
读配置是同步的,走本地缓存:
int interval = edge.Config.Get<int>("apps.EdgeCDS.collect.interval");
if (edge.Config.TryGet<string>("apps.EdgeCDS.mode", out var mode)) { ... }
缓存由 SDK 自动维护:连上时拉一次,收到 app.config.changed 时先刷新再回调。
读不到(键不存在 / 类型对不上)返回 default 而不是抛 —— 配置读取散落在业务各处,
为一个漏写的键打断采集循环不值得。
失败不阻断业务:这条链路是管理面旁路,发送失败只记日志。唯一的硬失败是构造期
AppId 为空、以及往未声明的集合里写数据 —— 这两件事写下去只会变成孤儿数据,早抛比晚查好。
启动元数据
EdgeAppMetadata 在启动时解析一次:文件版本、产品版本、安装包名、程序目录、日志路径。
随 app.register 上报,不需要手工填。Version 显式配置优先,否则用文件版本。
日志目录解析顺序:Options.LogDirectory → 环境变量 EDGE_APP_LOG_DIR(只接受绝对路径)
→ 平台默认(Linux /var/log/openrobot/<AppId>,Windows %ProgramData%\OpenRobot\Logs\<AppId>)。
相对路径一律不采用 —— pm2 的工作目录与程序目录不保证一致。
配置读取与覆盖
注入 EdgeAppConfigStore 读「实际生效」的配置。优先级从低到高:
- 声明里的
Default; Options.ConfigFiles(按顺序,后者赢);- 本地覆盖文件(
Options.OverrideFilePath,缺省<程序目录>/config/apps.<AppId>.override.json); - 平台
app.config.get快照; - 环境变量(
Options.EnvironmentVariablePrefix,如EDGEAPP__Logging__Level=Debug)。
var store = sp.GetRequiredService<EdgeAppConfigStore>();
store.Get<int>("Logging.Level"); // 相对键:apps.<App> 之下
store.Get<int>("apps.EdgeUspeed.Logging.Level"); // 绝对键同样接受
store.GetEffectiveConfig(); // 应用子树(嵌套形状)
store.GetFlatValues(); // 绝对键 → JsonElement
store.GetSources("Logging.Level"); // 这个值最终来自哪几层
await store.SetLocalOverrideAsync("Logging.Level", "Debug"); // 写本机覆盖层
await store.RemoveLocalOverrideAsync("Logging.Level");
await store.ReloadAsync(); // 手动重读本地文件
平台值高于本地覆盖是刻意的:线下改的旧值不应该盖掉平台上的权威配置。 本地文件损坏只跳过该层,记 Error,不影响启动。
第一版不监听文件变更:本地文件 / 环境变量改了之后,调一次 ReloadAsync()。
平台快照更新会自动重算,不需要手动刷新。
树形声明与运行时注入
AppVariableDeclarationEntry 支持 object / array 节点(Fields / Item),
足以表达 EdgeUspeed 的嵌套配置与对象数组。可以从 JSON 直接生成:
var variables = EdgeConfigSchemaBuilder.FromJsonFile("config/Uspeed.json", "EdgeUspeed");
edgeOptions.Variables.AddRange(variables);
生成的 Title 默认是路径段名,调用方应覆盖成业务文案。
旧平台不支持树形 schema 时用 EdgeConfigSchemaBuilder.Flatten(entries) 摊平成叶子(数组保持整体)。
业务模块在 PostInitialize 里补声明,不必回到 AddEdgeApp:
edge.AddVariables(new AppVariableDeclarationEntry { ... });
edge.AddControls(new AppControlDeclaration { ... });
edge.AddCollections(new AppCollectionDeclaration { ... });
edge.AddStates(stateMachine);
edge.RegisterControl(declaration, executor); // 声明 + 执行器一次挂好
await edge.ReannounceAsync(); // 已连接时立即重发注册 / 变量 / 状态声明
运行时增删请走这些方法,不要直接改 Options 的 List —— 直接改没有锁,与重连快照存在竞态。
状态声明
新增 topic app.state.declare,声明「有哪些状态、怎么迁移」;迁移算法仍在业务代码里。
edgeOptions.States.Add(new AppStateMachineDeclaration
{
MachineId = "ips",
Title = "提效状态",
Initial = "Idle",
States =
[
new AppStateDeclaration { Name = "Idle", Title = "待机", Severity = AppStateSeverities.Info },
new AppStateDeclaration { Name = "Fault", Title = "故障", Severity = AppStateSeverities.Error, Terminal = true },
],
Transitions =
[
new AppStateTransitionDeclaration { From = ["Idle"], To = "Fault", Event = "fault.raise" },
],
});
await edge.ReportStateAsync(new AppStateReport
{
Summary = "3 路采集正常",
MachineId = "ips",
StateName = "Optimizing",
State = new { channels = 3 },
});
Guard 是表达式文本,SDK 不执行、不校验。
文件日志与高频日志节流
AddEdgeAppLogging 只加文件 target(<LogDirectory>/<AppId>-yyyy-MM-dd.log),
控制台仍由 pm2 收集,一条日志只落一处。默认只把 Debug 写文件,
Information 及以上已经在 pm2 里;需要完整文件时往 EdgeLoggingOptions.FileLevels 里加。
默认同步写(AutoFlush),崩溃时最后几条不丢。
高频日志按调用方给的常量键收量,和 EdgeCDS / Uspeed 同一口径:
// 静默:键在 QuietKeys 里时每个周期只放行首条
logger.Throttled(throttle, EdgeLogKeys.IpcTopicMissing, LogLevel.Debug,
"收到未注册 topic:{Topic}", topic);
// 限频:窗口内最多 N 条
logger.Throttled(throttle, EdgeLogKeys.CollectSummary, LogLevel.Debug,
1, "采集摘要:{Channels} 路,缓存 {Buffered} 条", channels, buffered);
- 先判定、再格式化:被抑制的调用不拼字符串,只付一次字典查找;
Warning默认不节流,Error永不节流(EdgeLogThrottleOptions.ThrottleWarnings可显式打开 Warning);- 键必须低基数:
collect.summary可以,collect.<设备ID>等于没节流; EdgeAppModule周期调用Drain()输出汇总:静默 ipc.topic-missing 汇总:本周期 287 条(明细只打首条)。零流量不输出;- 键表上限默认 1024,按最久未使用淘汰。
协议
topic 常量见 EdgeAppProtocol。前缀固定 app.,点分、无版本前缀、不带应用名 ——
IPC 信封的 target 负责寻址,路由器按 topic 精确匹配且没有通配符。
| 方向 | topic | 载荷类型 |
|---|---|---|
| 应用 → 平台 | app.register |
AppRegisterPayload |
| 应用 → 平台 | app.vars.declare |
AppVariableDeclarationPayload |
| 应用 → 平台 | app.state.declare |
AppStateDeclarationPayload |
| 应用 → 平台 | app.state |
AppStatePayload |
| 应用 → 平台 | app.data |
AppDataPayload |
| 应用 → 平台(请求-响应) | app.config.get |
AppConfigRequestPayload → AppConfigSnapshotPayload |
| 平台 → 应用 | app.config.changed |
AppConfigChangedPayload |
| 平台 → 应用(请求-响应) | app.control.invoke |
AppControlInvokePayload → AppControlResultPayload |
注册与声明在每次连上时自动重发(AutoRegisterOnConnect,默认开),应用不必自己订阅连接事件。
声明内容会算 SHA-256 摘要,平台据此判断「没变就不重复落盘」,所以重复发送是安全的。
当前范围
已实现:注册(含版本 / 日志路径)、树形变量声明、运行时声明注入、状态声明与上报、
业务数据写入(IPC)、配置变更订阅、配置读取与分层覆盖(app.config.get + 本地文件 /
覆盖 / 环境变量)、控制点声明与执行(app.control.invoke)、
NLog 文件日志与高频日志节流统计。
后续(平台侧 / 其它仓库):app.state.declare 处理器与注册元数据落库、界面按树形 schema 渲染、
配置写入平台链路、YAML 配置解析。
平台侧已接入
app.register(应用台账:注册信息落库 + 在线 / pm2 进程状态) 与app.config.get;控制点的平台侧(下发入口、界面按钮)尚未接入 —— 协议与应用侧都就绪,RequestToAsync一接就能用。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
- NLog (>= 6.2.0)
- NLog.Extensions.Logging (>= 6.2.0)
- OpenRobot.Framework.Communication (>= 1.2.3)
- OpenRobot.Framework.Modularity (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.