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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OpenRobot.Edge.AppSdk" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OpenRobot.Edge.AppSdk" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="OpenRobot.Edge.AppSdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OpenRobot.Edge.AppSdk --version 0.2.0
                    
#r "nuget: OpenRobot.Edge.AppSdk, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OpenRobot.Edge.AppSdk@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OpenRobot.Edge.AppSdk&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=OpenRobot.Edge.AppSdk&version=0.2.0
                    
Install as a Cake Tool

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 读「实际生效」的配置。优先级从低到高:

  1. 声明里的 Default;
  2. Options.ConfigFiles(按顺序,后者赢);
  3. 本地覆盖文件(Options.OverrideFilePath,缺省 <程序目录>/config/apps.<AppId>.override.json);
  4. 平台 app.config.get 快照;
  5. 环境变量(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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 0 11/2/2026
0.1.0 49 10/29/2026