01 · 内核与工程模型 —— Hmi.Core / Hmi.Project

内核层。所有上层程序集都从这两个开始:Hmi.Core 管"运行时内存里的实时数据",Hmi.Project 管"磁盘上的工程文件"。


Hmi.Core —— 实时库 RTDB

职责:进程内实时数据库、点模型、发布/订阅变化分发。这是每个上层都要读写的内核。无任何 Hmi.* 依赖。

入口类型 RtdbCore : IDisposable

src/Hmi.Core/Rtdb/RtdbCore.cs(public sealed partial class)。构造:RtdbCore(IReadOnlyList<Tag> tags)。

常用方法:

方法 作用
bool TryGetId(string name, out int id) 点名 → 内部整型 id(热路径都用 id 寻址)
Tag GetTag(int id) 取点定义
TagChange Read(int id) 读当前值(seqlock 无锁乐观读)
NonScalarSnapshot ReadNonScalar(int id) 读非标量(字符串/数组/结构)旁路值
bool UpdateFromDriver(int id, TagValue v, Quality q, long ts) 驱动侧写入
bool UpdateNonScalar(...) / bool SetQuality(int,Quality,long) 写非标量 / 只改质量戳
IDisposable Subscribe(IEnumerable<int>, ITagSubscriber, DeliveryMode) 订阅一批点
IDisposable SubscribeAll(...) / int Count 订阅全部 / 点数

读用 seqlock 乐观读 + 分片锁;变化分发在独立线程上,分 Droppable(只要最新)/Lossless(绝不丢)两条投递车道。这就是"报警历史不丢、界面刷新不卡"的机制根源。

订阅契约 ITagSubscriber

src/Hmi.Core/Subscription/ITagSubscriber.cs —— void OnTagChanged(ReadOnlySpan<TagChange> batch)。这是 RTDB 唯一的消费者接口,TagSubscriptionBridge、HistorianRecorder、AlarmEngine、LinkedVariableMirror 都实现它。你想接一个新的"实时数据消费者",实现这个接口再 Subscribe 即可。

Hmi.Core.Branding:可变的 ProductName / DefaultProductName,运行期品牌名切换用。


Hmi.Project —— 工程模型与读写

职责:磁盘工程模型 + JSON 加载/保存(点、通道、画面、脚本、报警/历史配置、配方、多语言、通知、报表、样式),外加绑定/动作抽象。依赖 Hmi.Core、Drivers.Abstractions。

聚合根 LoadedProject

src/Hmi.Project/Persistence/LoadedProject.cs —— 一个工程在内存里的全部:ProjectModel Project、List<TagDef> Tags、List<ChannelDef> Channels、Dictionary<string,ScreenModel> Screens、ProjectScripts、Dictionary<string,ScriptModule> Modules、List<RecipeModel> Recipes、LanguageTable Languages、NotifyConfig Notify、List<AlarmConfigModel> Alarms、List<HistoryConfigModel> History、List<ReportModel> Reports、List<CustomButtonSkin> ButtonSkins。

加载器 ProjectLoader(静态)

src/Hmi.Project/Persistence/ProjectLoader.cs:

  • LoadedProject Load(string projectDir) —— 从目录加载;缺 project.json 抛 FileNotFoundException。
  • LoadedProject Load(IProjectFileSource src) —— 从文件源加载。目录源 DirectoryFileSource 或内存 zip 源 ZipFileSource;加密运行包解密成 ZipFileSource,明文永不落盘。
  • void Save(LoadedProject, string projectDir)。

邮件密码故意不进 notify.json,由 MailSecretStore(DPAPI)另存。

写路径抽象 IActionDispatcher

src/Hmi.Project/Binding/IActionDispatcher.cs —— WriteTag(name,value) / OpenScreen(name) / RunScript(name)。脚本、AI 共用这条写路径;运行态由 Hmi.Backend 提供实现。


小结

  • 想操作内存实时值 → RtdbCore;想接一个新消费者 → 实现 ITagSubscriber。
  • 想读写磁盘工程 → ProjectLoader.Load/Save + LoadedProject。
  • 大多数集成不会直接碰 LoadedProject 的字段,而是通过第 06 章的 Hmi.Automation 间接改 —— 那一层带校验和审计。