添加模型缩减和项目打开功能的API支持

- 在HttpServer中实现了新的API端点:/api/project/open和/api/model/shrinkwrap
- 添加了ShrinkwrapModel和OpenProject命令的处理逻辑
- 在PdmsManager中实现了ShrinkwrapModel和OpenProject方法,支持相应请求的处理
- 更新了项目文件以包含新的命令和模型请求类

此更新增强了插件的功能,允许用户通过API进行模型缩减和项目打开操作。
This commit is contained in:
sladro 2026-02-05 08:22:42 +08:00
parent bf35d98365
commit 8f5bcc0c98
13 changed files with 1495 additions and 1 deletions

View File

@ -1,7 +1,9 @@
using Aveva.ApplicationFramework;
using System;
using System.Windows.Forms;
using Aveva.ApplicationFramework;
using TellmePdmsPluging.Network;
using TellmePdmsPluging.Core;
namespace TellmePdmsPluging
{
@ -35,10 +37,14 @@ namespace TellmePdmsPluging
{
// 记录启动日志
LogMessage("开始启动TellmePdms插件...");
SafeQueue.Init();
// 启动HTTP服务器
_httpServer = new HttpServer(9001);
_httpServer.Start();
Application.Idle += OnIdle;
// 弹出成功提示
MessageBox.Show(
@ -69,6 +75,8 @@ namespace TellmePdmsPluging
try
{
LogMessage("开始停止TellmePdms插件...");
Application.Idle -= OnIdle;
// 停止HTTP服务器
if (_httpServer != null)
@ -86,6 +94,21 @@ namespace TellmePdmsPluging
}
}
private void OnIdle(object sender, EventArgs e)
{
try
{
while (SafeQueue.TryDequeue(out ICommand cmd))
{
cmd.Execute();
}
}
catch (Exception ex)
{
LogMessage($"Idle执行命令时出错: {ex.Message}");
}
}
private void LogMessage(string message)
{
try

View File

@ -0,0 +1,41 @@
using System;
using TellmePdmsPluging.Core;
using TellmePdmsPluging.Models;
namespace TellmePdmsPluging.Commands
{
public class OpenMdbCommand : ICommand
{
public OpenMdbCommand(OpenMdbRequest request)
{
Request = request ?? new OpenMdbRequest();
CommandId = Guid.NewGuid().ToString("N");
}
public string CommandId { get; }
public string CommandType
{
get { return "OpenMDB"; }
}
public bool CanCancel
{
get { return false; }
}
public OpenMdbRequest Request { get; }
public object Execute()
{
Request.ApplyDefaults();
return PdmsManager.Instance.OpenMdb(Request);
}
public void Cancel()
{
throw new NotSupportedException("OpenMdbCommand 不支持取消");
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using TellmePdmsPluging.Core;
using TellmePdmsPluging.Models;
namespace TellmePdmsPluging.Commands
{
public class OpenProjectCommand : ICommand
{
public OpenProjectCommand(OpenProjectRequest request)
{
Request = request ?? new OpenProjectRequest();
CommandId = Guid.NewGuid().ToString("N");
}
public string CommandId { get; }
public string CommandType
{
get { return "OpenProject"; }
}
public bool CanCancel
{
get { return false; }
}
public OpenProjectRequest Request { get; }
public object Execute()
{
Request.ApplyDefaults();
return PdmsManager.Instance.OpenProject(Request);
}
public void Cancel()
{
throw new NotSupportedException("OpenProjectCommand 不支持取消");
}
}
}

View File

@ -0,0 +1,39 @@
using TellmePdmsPluging.Core;
using TellmePdmsPluging.Models;
namespace TellmePdmsPluging.Commands
{
public class ShrinkwrapModelCommand : ICommand
{
public ShrinkwrapModelCommand(ShrinkwrapModelRequest request)
{
Request = request ?? new ShrinkwrapModelRequest();
CommandId = System.Guid.NewGuid().ToString("N");
}
public string CommandId { get; }
public string CommandType
{
get { return "ShrinkwrapModel"; }
}
public bool CanCancel
{
get { return false; }
}
public ShrinkwrapModelRequest Request { get; }
public object Execute()
{
Request.ApplyDefaults();
return PdmsManager.Instance.ShrinkwrapModel(Request);
}
public void Cancel()
{
throw new System.NotSupportedException("ShrinkwrapModelCommand 不支持取消");
}
}
}

108
Core/MainThreadInvoker.cs Normal file
View File

@ -0,0 +1,108 @@
using System;
using System.Threading;
namespace TellmePdmsPluging.Core
{
public static class MainThreadInvoker
{
public static InvokeResult Invoke(ICommand command, int timeoutMs)
{
if (command == null)
{
return new InvokeResult(false, null, new ArgumentNullException("command"), "command为空");
}
var wrapper = new ResultCommandWrapper(command);
SafeQueue.Enqueue(wrapper);
bool signaled = wrapper.Wait(timeoutMs);
if (!signaled)
{
return new InvokeResult(false, null, null, "等待PDMS主线程执行超时");
}
if (wrapper.Error != null)
{
return new InvokeResult(false, null, wrapper.Error, wrapper.Error.Message);
}
return new InvokeResult(true, wrapper.Result, null, null);
}
private class ResultCommandWrapper : IResultCommand
{
private readonly ICommand _inner;
private readonly ManualResetEvent _done;
public ResultCommandWrapper(ICommand inner)
{
_inner = inner;
_done = new ManualResetEvent(false);
CommandId = inner.CommandId;
}
public string CommandId { get; }
public string CommandType
{
get { return _inner.CommandType; }
}
public bool CanCancel
{
get { return _inner.CanCancel; }
}
public object Result { get; private set; }
public Exception Error { get; private set; }
public bool IsCompleted { get; private set; }
public object Execute()
{
try
{
Result = _inner.Execute();
return Result;
}
catch (Exception ex)
{
Error = ex;
return null;
}
finally
{
IsCompleted = true;
_done.Set();
}
}
public void Cancel()
{
_inner.Cancel();
}
public bool Wait(int timeoutMs)
{
return _done.WaitOne(timeoutMs);
}
}
}
public class InvokeResult
{
public InvokeResult(bool success, object result, Exception error, string message)
{
Success = success;
Result = result;
Error = error;
Message = message;
}
public bool Success { get; }
public object Result { get; }
public Exception Error { get; }
public string Message { get; }
}
}

View File

@ -4,6 +4,7 @@ using TellmePdmsPluging.Models;
using Aveva.ApplicationFramework;
using Aveva.Pdms.Database;
using System.Linq;
using System;
namespace TellmePdmsPluging.Core
{
@ -120,6 +121,95 @@ namespace TellmePdmsPluging.Core
}
}
public ShrinkwrapModelResult ShrinkwrapModel(ShrinkwrapModelRequest request)
{
var effectiveRequest = request ?? new ShrinkwrapModelRequest();
effectiveRequest.ApplyDefaults();
var result = new ShrinkwrapModelResult
{
DryRun = effectiveRequest.DryRun,
Padding = effectiveRequest.Padding,
StartedAt = DateTime.Now
};
try
{
if (!IsPdmsConnected())
{
result.Success = false;
result.CompletedAt = DateTime.Now;
result.Message = "PDMS 未连接";
result.Errors.Add("PDMS 未连接");
return result;
}
var currentMdb = MDB.CurrentMDB;
var designDb = currentMdb?.GetFirstDB(DbType.Design);
if (designDb == null || designDb.World == null)
{
result.Success = false;
result.CompletedAt = DateTime.Now;
result.Message = "未找到有效的设计数据库";
result.Errors.Add("未找到有效的设计数据库");
return result;
}
var context = new ShrinkwrapContext(effectiveRequest, result);
var sites = designDb.World.Members();
bool processed = false;
if (sites != null)
{
foreach (var site in sites)
{
if (!IsElementValid(site) || !string.Equals(GetElementTypeName(site), "SITE", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var zones = site.Members();
if (zones == null)
{
continue;
}
foreach (var zone in zones)
{
if (!IsElementValid(zone) || !string.Equals(GetElementTypeName(zone), "ZONE", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!context.ShouldProcessZone(zone))
{
continue;
}
processed = true;
context.EnterZone(zone);
context.ProcessZone(zone);
}
}
}
result.Success = processed && result.Errors.Count == 0;
result.Message = processed
? (result.DryRun ? "外壳保留干跑完成" : "外壳保留完成")
: "未找到符合过滤条件的Zone";
result.CompletedAt = DateTime.Now;
return result;
}
catch (Exception ex)
{
result.Success = false;
result.Errors.Add(ex.Message);
result.Message = "外壳保留失败";
result.CompletedAt = DateTime.Now;
return result;
}
}
public ModelStatusResponse GetModelStatus()
{
try
@ -152,6 +242,253 @@ namespace TellmePdmsPluging.Core
}
}
public OpenProjectResult OpenProject(OpenProjectRequest request)
{
var effectiveRequest = request ?? new OpenProjectRequest();
effectiveRequest.ApplyDefaults();
var result = new OpenProjectResult
{
ProjectName = effectiveRequest.ProjectName,
CompletedAt = DateTime.Now
};
try
{
var currentProject = Project.CurrentProject;
if (currentProject == null)
{
result.Success = false;
result.Message = "Project.CurrentProject 为空,无法打开项目";
return result;
}
bool alreadyOpen = false;
try
{
alreadyOpen = currentProject.IsOpen();
}
catch
{
alreadyOpen = false;
}
result.WasAlreadyOpen = alreadyOpen;
// 如果已经打开且未指定目标项目名,则直接返回成功
if (alreadyOpen && string.IsNullOrEmpty(effectiveRequest.ProjectName))
{
result.Success = true;
result.Message = "项目已打开";
return result;
}
if (string.IsNullOrEmpty(effectiveRequest.ProjectName))
{
result.Success = false;
result.Message = "ProjectName 不能为空";
return result;
}
bool opened = currentProject.Open(
effectiveRequest.ProjectName,
effectiveRequest.UserName ?? string.Empty,
effectiveRequest.Password ?? string.Empty);
result.Success = opened;
result.Message = opened ? "项目打开成功" : "项目打开失败Open 返回 false";
result.CompletedAt = DateTime.Now;
return result;
}
catch (Exception ex)
{
result.Success = false;
result.Message = "项目打开异常: " + ex.Message;
result.CompletedAt = DateTime.Now;
return result;
}
}
public OpenMdbResult OpenMdb(OpenMdbRequest request)
{
var effectiveRequest = request ?? new OpenMdbRequest();
effectiveRequest.ApplyDefaults();
var result = new OpenMdbResult
{
MdbName = effectiveRequest.MdbName,
ReadOnly = effectiveRequest.ReadOnly,
DefaultType = effectiveRequest.DefaultType,
CompletedAt = DateTime.Now
};
try
{
if (string.IsNullOrEmpty(effectiveRequest.MdbName))
{
result.Success = false;
result.Message = "MdbName 不能为空";
return result;
}
// 如果当前MDB已经是目标则直接返回成功
try
{
var current = MDB.CurrentMDB;
if (current != null && string.Equals(current.Name, effectiveRequest.MdbName, StringComparison.OrdinalIgnoreCase))
{
result.Success = true;
result.WasAlreadyOpen = true;
result.Message = "MDB 已经打开";
result.CompletedAt = DateTime.Now;
return result;
}
}
catch
{
// ignore, continue to open
}
var setup = MDBSetup.CreateMDBSetup(effectiveRequest.MdbName);
if (setup == null)
{
result.Success = false;
result.Message = "创建 MDBSetup 失败";
return result;
}
setup.ReadOnly = effectiveRequest.ReadOnly;
if (!string.IsNullOrEmpty(effectiveRequest.Stamp))
{
setup.Stamp = effectiveRequest.Stamp;
}
if (effectiveRequest.Subtype.HasValue)
{
setup.Subtype = effectiveRequest.Subtype.Value;
}
if (!string.IsNullOrEmpty(effectiveRequest.DefaultType))
{
DbType defaultType;
if (!TryParseDbType(effectiveRequest.DefaultType, out defaultType))
{
result.Success = false;
result.Message = "DefaultType 无效: " + effectiveRequest.DefaultType;
return result;
}
setup.DefaultType = defaultType;
}
if (effectiveRequest.ReadTypes != null && effectiveRequest.ReadTypes.Count > 0)
{
DbType[] readTypes;
string error;
if (!TryParseDbTypes(effectiveRequest.ReadTypes, out readTypes, out error))
{
result.Success = false;
result.Message = "ReadTypes 无效: " + error;
return result;
}
setup.ReadTypes = readTypes;
}
if (effectiveRequest.WriteTypes != null && effectiveRequest.WriteTypes.Count > 0)
{
DbType[] writeTypes;
string error;
if (!TryParseDbTypes(effectiveRequest.WriteTypes, out writeTypes, out error))
{
result.Success = false;
result.Message = "WriteTypes 无效: " + error;
return result;
}
setup.WriteTypes = writeTypes;
}
var openedMdb = Project.OpenMDB(setup);
if (openedMdb == null)
{
result.Success = false;
result.Message = "打开 MDB 失败OpenMDB 返回 null";
result.CompletedAt = DateTime.Now;
return result;
}
result.Success = true;
result.WasAlreadyOpen = false;
result.MdbName = openedMdb.Name;
result.Message = "MDB 打开成功";
result.CompletedAt = DateTime.Now;
return result;
}
catch (Exception ex)
{
result.Success = false;
result.Message = "打开 MDB 异常: " + ex.Message;
result.CompletedAt = DateTime.Now;
return result;
}
}
private static bool TryParseDbTypes(IEnumerable<string> values, out DbType[] types, out string error)
{
var list = new List<DbType>();
var invalid = new List<string>();
foreach (var raw in values)
{
if (string.IsNullOrEmpty(raw))
{
continue;
}
DbType parsed;
if (TryParseDbType(raw, out parsed))
{
if (!list.Contains(parsed))
{
list.Add(parsed);
}
}
else
{
invalid.Add(raw);
}
}
if (invalid.Count > 0)
{
types = null;
error = string.Join(",", invalid.ToArray());
return false;
}
types = list.ToArray();
error = null;
return true;
}
private static bool TryParseDbType(string value, out DbType dbType)
{
dbType = DbType.Design;
if (string.IsNullOrEmpty(value))
{
return false;
}
try
{
dbType = (DbType)Enum.Parse(typeof(DbType), value.Trim(), true);
return true;
}
catch
{
return false;
}
}
private bool IsPdmsConnected()
{
try
@ -567,5 +904,266 @@ namespace TellmePdmsPluging.Core
}
}
}
private class ShrinkwrapContext
{
private const int MaxRemovedSnapshots = 200;
public ShrinkwrapContext(ShrinkwrapModelRequest request, ShrinkwrapModelResult result)
{
Request = request;
Result = result;
_keepTypes = new HashSet<string>(request.KeepTypes ?? new List<string>());
}
public ShrinkwrapModelRequest Request { get; }
public ShrinkwrapModelResult Result { get; }
public string CurrentZoneName { get; private set; }
private readonly HashSet<string> _keepTypes;
private float[] _innerBox;
public bool ShouldProcessZone(DbElement zone)
{
if (Request.ZoneFilters == null || Request.ZoneFilters.Count == 0)
{
return true;
}
var zonePath = BuildElementPath(zone).ToUpperInvariant();
return Request.ZoneFilters.Any(filter => zonePath.Contains(filter));
}
public void EnterZone(DbElement zone)
{
CurrentZoneName = BuildElementPath(zone);
if (!Result.ZoneSummaries.Contains(CurrentZoneName))
{
Result.ZoneSummaries.Add(CurrentZoneName);
}
}
public void ProcessZone(DbElement zone)
{
float[] zoneBox;
if (!TryGetLimitsBox(zone, out zoneBox))
{
Result.Errors.Add($"Zone包围盒计算失败: {BuildElementPath(zone)}");
return;
}
_innerBox = BuildInnerBox(zoneBox, (float)Request.Padding);
ProcessChildren(zone);
}
private void ProcessChildren(DbElement parent)
{
var members = parent.Members();
if (members == null)
{
return;
}
foreach (var child in members)
{
ProcessElement(child);
}
}
private void ProcessElement(DbElement element)
{
if (!IsElementValid(element))
{
return;
}
Result.TotalVisited++;
var typeName = GetElementTypeName(element);
var normalizedType = string.IsNullOrEmpty(typeName) ? string.Empty : typeName.ToUpperInvariant();
if (_keepTypes.Contains(normalizedType))
{
Result.KeptCount++;
ProcessChildren(element);
return;
}
float[] box;
if (!TryGetLimitsBox(element, out box))
{
Result.KeptCount++;
return;
}
if (IsInsideInnerBox(box, _innerBox, (float)Request.TouchTolerance))
{
RemoveElement(element, normalizedType);
return;
}
Result.ShellKeptCount++;
Result.KeptCount++;
ProcessChildren(element);
}
private void RemoveElement(DbElement element, string typeName)
{
var elementPath = BuildElementPath(element);
if (Request.DryRun)
{
SnapshotRemoval(elementPath, typeName, true);
return;
}
try
{
element.Delete();
SnapshotRemoval(elementPath, typeName, false);
}
catch (Exception ex)
{
Result.Errors.Add($"删除元素 {elementPath} 失败: {ex.Message}");
}
}
private void SnapshotRemoval(string elementPath, string typeName, bool dryRun)
{
Result.RemovedCount++;
if (Result.RemovedElements.Count < MaxRemovedSnapshots)
{
var marker = dryRun ? "DRY" : "DEL";
Result.RemovedElements.Add($"[{marker}] {elementPath} ({typeName})");
}
}
private static bool TryGetLimitsBox(DbElement element, out float[] box)
{
box = null;
try
{
var spatial = Spatial.Instance;
if (spatial == null)
{
return false;
}
Aveva.Pdms.Geometry.LimitsBox limits;
var ok = spatial.LimitsBox(element, out limits);
if (!ok)
{
return false;
}
box = ConvertLimitsBox(limits);
return box != null && box.Length >= 6;
}
catch
{
return false;
}
}
private static float[] ConvertLimitsBox(Aveva.Pdms.Geometry.LimitsBox limits)
{
// 兼容不同版本 PDMS LimitsBox 字段/属性命名
// 期望输出: [xmin, ymin, zmin, xmax, ymax, zmax]
try
{
var type = typeof(Aveva.Pdms.Geometry.LimitsBox);
var boxed = (object)limits;
float xmin = ReadFloat(boxed, type, new[] { "XMIN", "XMin", "MinX", "Xmin" });
float ymin = ReadFloat(boxed, type, new[] { "YMIN", "YMin", "MinY", "Ymin" });
float zmin = ReadFloat(boxed, type, new[] { "ZMIN", "ZMin", "MinZ", "Zmin" });
float xmax = ReadFloat(boxed, type, new[] { "XMAX", "XMax", "MaxX", "Xmax" });
float ymax = ReadFloat(boxed, type, new[] { "YMAX", "YMax", "MaxY", "Ymax" });
float zmax = ReadFloat(boxed, type, new[] { "ZMAX", "ZMax", "MaxZ", "Zmax" });
return new[] { xmin, ymin, zmin, xmax, ymax, zmax };
}
catch
{
return null;
}
}
private static float ReadFloat(object boxed, System.Type type, string[] names)
{
foreach (var name in names)
{
var prop = type.GetProperty(name);
if (prop != null)
{
var v = prop.GetValue(boxed, null);
if (v != null)
{
return Convert.ToSingle(v);
}
}
var field = type.GetField(name);
if (field != null)
{
var v = field.GetValue(boxed);
if (v != null)
{
return Convert.ToSingle(v);
}
}
}
throw new InvalidOperationException("LimitsBox缺少必要字段/属性");
}
private static float[] BuildInnerBox(float[] outerBox, float padding)
{
var inner = new float[6];
inner[0] = outerBox[0] + padding;
inner[1] = outerBox[1] + padding;
inner[2] = outerBox[2] + padding;
inner[3] = outerBox[3] - padding;
inner[4] = outerBox[4] - padding;
inner[5] = outerBox[5] - padding;
if (inner[3] < inner[0])
{
inner[0] = outerBox[0];
inner[3] = outerBox[3];
}
if (inner[4] < inner[1])
{
inner[1] = outerBox[1];
inner[4] = outerBox[4];
}
if (inner[5] < inner[2])
{
inner[2] = outerBox[2];
inner[5] = outerBox[5];
}
return inner;
}
private static bool IsInsideInnerBox(float[] box, float[] innerBox, float tolerance)
{
if (box == null || innerBox == null || box.Length < 6 || innerBox.Length < 6)
{
return false;
}
// box完全在innerBox内部留tolerance避免贴边误删
return (box[0] > innerBox[0] + tolerance) &&
(box[1] > innerBox[1] + tolerance) &&
(box[2] > innerBox[2] + tolerance) &&
(box[3] < innerBox[3] - tolerance) &&
(box[4] < innerBox[4] - tolerance) &&
(box[5] < innerBox[5] - tolerance);
}
}
}
}

67
Core/SafeQueue.cs Normal file
View File

@ -0,0 +1,67 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace TellmePdmsPluging.Core
{
public static class SafeQueue
{
private static readonly object _sync = new object();
private static Queue<ICommand> _queue;
public static void Init()
{
if (_queue != null)
{
return;
}
lock (_sync)
{
if (_queue == null)
{
_queue = new Queue<ICommand>();
}
}
}
public static void Enqueue(ICommand command)
{
if (command == null)
{
return;
}
Init();
lock (_sync)
{
_queue.Enqueue(command);
}
}
public static bool TryDequeue(out ICommand command)
{
Init();
lock (_sync)
{
if (_queue.Count > 0)
{
command = _queue.Dequeue();
return true;
}
}
command = null;
return false;
}
}
public interface IResultCommand : ICommand
{
object Result { get; }
Exception Error { get; }
bool IsCompleted { get; }
}
}

View File

@ -0,0 +1,125 @@
# Tellme PDMS 插件技术架构文档
## 1. 项目概述
- **目标**:为单个 AVEVA PDMS 12.1 SP4 会话提供远程监控与操作能力,实现前端客户端通过 HTTP 接口安全地获取模型状态并下达任务。
- **运行环境**.NET Framework 3.5x86遵循 PDMS 插件加载约束;前端与插件运行在同一主机,通信默认局限于本地回环接口。
- **核心原则**:主线程执行 PDMS API、命令模式分离网络与业务、无硬编码数据、兼顾大模型性能与资源占用。
## 2. 系统总体架构
```mermaid
graph LR
A[前端客户端] -->|HTTP/JSON| B[HttpServer]
B -->|入队| C[SafeQueue]
C -->|出队| D[PDMS 主线程]
D --> E[Aveva PDMS API]
D -->|结果| B
B -->|响应| A
```
- **入口插件 (`TellmePdmsAddin`)**:实现 `IAddin` 接口,在 PDMS 启动时加载,负责生命周期管理和日志记录。
- **网络层 (`Network.HttpServer`)**:基于 `System.Net.HttpListener` 自托管 HTTP 服务,路由健康检查与业务接口。
- **命令层 (`Core.ICommand` 系列)**:每个请求解析为命令对象入队,支持可取消性扩展。
- **业务层 (`Core.PdmsManager`)**:封装对 `MDB.CurrentMDB`、`Project.CurrentProject`、`DbElement` 等 API 的访问,生成结构化数据。
- **模型层 (`Models.*`)**:定义 `ModelStatusResponse`、`ProjectInfo`、`ModelStatistics`、`SessionInfo` 等 DTO 以便序列化。
- **工具层**:提供线程安全队列、日志、序列化和配置扩展点(`Utils` 目录预留)。
## 3. 关键技术栈
| 分类 | 选型 | 理由 |
| --- | --- | --- |
| 插件框架 | `Aveva.ApplicationFramework.IAddin` | 满足 PDMS 扩展规范,允许在主线程注册 Idle 事件 |
| 语言与平台 | C# + .NET Framework 3.5 (x86) | 与 PDMS 12.1 SP4 兼容,仅 32 位可加载 |
| 网络通信 | `System.Net.HttpListener` | BCL 自带、无第三方依赖、支持本地自托管 |
| 序列化 | 自定义轻量 JSON 序列化 | 兼容 .NET 3.5,避免外部依赖,可替换为 `System.Web.Script.Serialization` |
| 队列实现 | `Queue<T>` + `lock` 封装 | .NET 3.5 无 `ConcurrentQueue`,需手动保证线程安全 |
| 日志 | 文本文件 (`C:\temp\*.txt`) | 易于部署与排查,可替换为 log4net |
| 单元测试(预留) | NUnit 2.x | 支持 .NET 3.5,便于未来补充测试 |
## 4. 主要模块说明
### 4.1 插件入口 `TellmePdmsAddin`
- `Start(ServiceManager)`:初始化 `HttpServer`、记录启动日志、提示监听端口,并可在此处加载配置或注册 UI 菜单。
- `Stop()`:释放服务器资源并记录停止日志。
- `Application.Idle` 事件在其他模块注册,用于在 PDMS 主线程执行命令队列。
### 4.2 网络服务 `Network.HttpServer`
- 构造函数注入端口(默认 9001仅允许 `http://localhost:<port>/`
- `Start()`/`Stop()` 控制 `HttpListener` 生命周期,利用 `BeginGetContext` 异步接受请求。
- `ProcessRequest` 完成路由分发、CORS 头设置、异常捕获与响应写入。
- 当前实现的路由:`/health`、`/test`、`/api/status/model`,可扩展 `switch` 和命令解析以支持更多端点。
### 4.3 命令抽象 `Core.ICommand`
- 统一定义 `CommandId`、`CommandType`、`Execute()`,保留 `CanCancel/Cancel()` 扩展位。
- 推荐为每个 HTTP 接口实现对应命令类,确保执行逻辑仅运行在主线程环境。
### 4.4 业务管理 `Core.PdmsManager`
- 单例模式管理 PDMS 状态查询,避免重复初始化。
- `GetModelStatus()`:集中调度连接检测、项目信息、模型统计、会话信息。
- **连接检测**`MDB.CurrentMDB` 是否返回非空。
- **项目信息**:通过 `CurrentMDB.GetFirstDB(DbType.Design)` 获取设计库名称、MDS 名称、PDMS 版本。
- **模型统计**:递归 `DbElement.Members()`,使用 `element.GetActualType().Name` 累加 SITE、ZONE、PIPE 等关键元素数量。
- **Zone 统计**:遍历 SITE → ZONE 层级,利用 `DbElement.GetValidString(DbAttributeInstance.NAME, ref zoneName)` 采集激活区域名称。
- **会话信息**:从 `DesignDb.CurrentSession` 读取用户、启动时间,计算持续分钟数。
- 所有 API 调用包裹 try/catch 并输出调试日志以保证稳定性。
## 5. 线程模型
- HttpListener 线程仅负责解析请求并将命令入队,禁止直接调用 PDMS API。
- 主线程通过 `Application.Idle` 循环 `SafeQueue.TryDequeue(out ICommand cmd)`,顺序执行。
- 可在命令执行过程中更新共享的进度字典,以供 `/task/{id}` 查询(待实现)。
- 长任务需在执行逻辑内部分批处理,并在批次间调用 `GC.Collect()` 控制内存。
## 6. HTTP 接口约定
| 路径 | 方法 | 描述 | 响应示例 |
| --- | --- | --- | --- |
| `/health` | GET | 进程存活检测,返回时间戳与内存占用 | `{ "code":0, "message":"成功", "data":{ "status":"OK", "timestamp":"2025-09-21 12:00:00", "memoryMB":123 } }` |
| `/test` | GET | 轻量连通性测试,检测是否运行于 PDMS 环境 | `{ "success":true, "data":{ "running":true, "message":"TellmePdms 与 PDMS 连接正常" }, "error":null }` |
| `/api/status/model` | GET | 返回模型加载状态、项目信息、元素统计、会话信息 | `{ "code":0, "message":"成功", "data":{ ...ModelStatusResponse... } }` |
- 响应以 `ApiResponse<T>` 模式封装:`code` 为 0 表示成功,`message` 给出提示,`data` 为业务数据。
- 自定义错误码示例:`1001` 表示 PDMS 模型未加载,`500` 表示内部异常。
- 未来规划接口:`POST /command`、`POST /task`、`GET /task/{id}`、`GET /stats`、`POST /export/ifc` 等,可复用命令与模型层。
## 7. 数据模型结构
### 7.1 `ModelStatusResponse`
- `ModelLoaded`PDMS 是否加载模型。
- `ProjectInfo`:包含 `ProjectName`、`MdsName`、`PdmsVersion`。
- `ModelStatistics``TotalElements`、`ElementCounts (Dictionary<string,int>)`、`ZoneCount`、`ActiveZones`。
- `SessionInfo`:会话 `UserName`、`StartTime`、`DurationMinutes`。
### 7.2 序列化策略
- 对字典、列表使用定制序列化,确保 .NET 3.5 环境兼容。
- 日期统一格式 `yyyy-MM-ddTHH:mm:ssZ`,便于前端解析。
- 主线程执行结束后返回 JSON 字符串,由 HttpServer 写入响应流。
## 8. 性能与资源控制
- **Large Address Aware**:建议为 32 位进程设置 LAA 标志,在 64 位 Windows 上提升可用内存上限至 3 GB。
- **分批遍历**对大模型递归时可按逻辑域SITE、ZONE分批处理减少单次内存占用。
- **流式响应**长列表数据可拆分分页或使用压缩GZip + Base64后流式传输避免一次性加载。
- **GC 管理**:在长任务各阶段主动调用 `GC.Collect()` 并输出当前内存占用,防止内存峰值。
- **并发限制**:当前设计为单客户端单任务,无任务队列优先级;后续可引入任务管理表实现调度与取消。
## 9. 日志与错误处理
- 插件与服务器分别记录至 `C:\temp\pdms_plugin_log.txt`、`C:\temp\pdms_http_log.txt`。
- 网络层捕获异常后返回 500 错误并写入日志;业务层捕获异常后返回自定义错误码。
- 建议引入滚动日志策略log4net RollingFileAppender以限制文件大小。
## 10. 部署与配置
1. 以 x86、.NET 3.5 配置编译 `TellmePdmsPluging.sln`
2. 将输出 DLL 复制到 PDMS 安装目录,更新 `DesignAddin.xml` 注册插件。
3. 确保 Windows 防火墙允许本地回环端口 9001外部访问需额外代理。
4. 可在 `Config` 目录引入自定义配置(端口、日志路径、批处理大小等),启动时读取。
5. 部署后通过 `http://localhost:9001/health` 验证服务是否监听。
## 11. 后续扩展建议
- **任务管理**:实现 `/task` 接口与任务表持久化,支持长任务进度查询与取消。
- **安全加固**:加入鉴权(如基于令牌或双向证书),限制访问来源。
- **前端工具**:提供 Web 或桌面客户端,集成状态展示与命令触发。
- **协议升级**:考虑使用 WebSocket 推送进度、MessagePack/Protobuf 提升带宽效率。
- **监控告警**:将健康检查与日志集成到企业监控平台,追踪内存与任务状态。
---
本文件概述了 Tellme PDMS 远程控制插件的架构设计、核心组件、关键算法与部署要点,可作为后续开发迭代、测试验证及运维交付的基础参考资料。

127
Models/OpenMdbRequest.cs Normal file
View File

@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
namespace TellmePdmsPluging.Models
{
public class OpenMdbRequest
{
/// <summary>
/// MDB name to open
/// </summary>
public string MdbName { get; set; }
/// <summary>
/// If true then all DBs will be opened in read
/// </summary>
public bool ReadOnly { get; set; }
/// <summary>
/// Default database type. e.g. Design
/// </summary>
public string DefaultType { get; set; }
/// <summary>
/// DbTypes to open in read (string form, e.g. ["Design","Catalog"])
/// </summary>
public List<string> ReadTypes
{
get { return _readTypes; }
set { _readTypes = NormalizeList(value); }
}
/// <summary>
/// DbTypes to open in write (string form)
/// </summary>
public List<string> WriteTypes
{
get { return _writeTypes; }
set { _writeTypes = NormalizeList(value); }
}
/// <summary>
/// Stamp at which to open the MDB (optional)
/// </summary>
public string Stamp { get; set; }
/// <summary>
/// Subtype, i.e. marine or not (optional)
/// </summary>
public int? Subtype { get; set; }
private List<string> _readTypes;
private List<string> _writeTypes;
public void ApplyDefaults()
{
MdbName = Normalize(MdbName);
DefaultType = Normalize(DefaultType);
Stamp = Normalize(Stamp);
}
private static string Normalize(string value)
{
if (IsNullOrWhiteSpace(value))
{
return null;
}
return value.Trim();
}
private static List<string> NormalizeList(IEnumerable<string> source)
{
if (source == null)
{
return null;
}
var normalized = new List<string>();
foreach (var item in source)
{
if (IsNullOrWhiteSpace(item))
{
continue;
}
var formatted = item.Trim();
if (!normalized.Contains(formatted))
{
normalized.Add(formatted);
}
}
return normalized;
}
private static bool IsNullOrWhiteSpace(string value)
{
if (value == null)
{
return true;
}
for (int i = 0; i < value.Length; i++)
{
if (!char.IsWhiteSpace(value[i]))
{
return false;
}
}
return true;
}
}
public class OpenMdbResult
{
public bool Success { get; set; }
public string Message { get; set; }
public string MdbName { get; set; }
public bool WasAlreadyOpen { get; set; }
public bool ReadOnly { get; set; }
public string DefaultType { get; set; }
public DateTime CompletedAt { get; set; }
}
}

View File

@ -0,0 +1,67 @@
using System;
namespace TellmePdmsPluging.Models
{
public class OpenProjectRequest
{
/// <summary>
/// PDMS Project name
/// </summary>
public string ProjectName { get; set; }
/// <summary>
/// PDMS login user name
/// </summary>
public string UserName { get; set; }
/// <summary>
/// PDMS login password
/// </summary>
public string Password { get; set; }
public void ApplyDefaults()
{
ProjectName = Normalize(ProjectName);
UserName = Normalize(UserName);
Password = Password ?? string.Empty;
}
private static string Normalize(string value)
{
if (IsNullOrWhiteSpace(value))
{
return null;
}
return value.Trim();
}
private static bool IsNullOrWhiteSpace(string value)
{
if (value == null)
{
return true;
}
for (int i = 0; i < value.Length; i++)
{
if (!char.IsWhiteSpace(value[i]))
{
return false;
}
}
return true;
}
}
public class OpenProjectResult
{
public bool Success { get; set; }
public string Message { get; set; }
public string ProjectName { get; set; }
public bool WasAlreadyOpen { get; set; }
public DateTime CompletedAt { get; set; }
}
}

View File

@ -0,0 +1,102 @@
using System.Collections.Generic;
namespace TellmePdmsPluging.Models
{
public class ShrinkwrapModelRequest
{
public bool DryRun { get; set; } = true;
public double Padding { get; set; } = 500.0;
public double TouchTolerance { get; set; } = 1.0;
public List<string> ZoneFilters
{
get { return _zoneFilters; }
set { _zoneFilters = NormalizeList(value); }
}
public List<string> KeepTypes
{
get { return _keepTypes; }
set { _keepTypes = NormalizeList(value); }
}
private List<string> _zoneFilters;
private List<string> _keepTypes;
public void ApplyDefaults()
{
if (_keepTypes == null || _keepTypes.Count == 0)
{
_keepTypes = new List<string> { "SITE", "ZONE", "STRU", "FRAME", "SHELL", "PLAT", "WALL" };
}
if (_zoneFilters == null)
{
_zoneFilters = new List<string>();
}
}
private static List<string> NormalizeList(IEnumerable<string> source)
{
if (source == null)
{
return null;
}
var normalized = new List<string>();
foreach (var item in source)
{
if (IsNullOrWhiteSpace(item))
{
continue;
}
var formatted = item.Trim().ToUpperInvariant();
if (!normalized.Contains(formatted))
{
normalized.Add(formatted);
}
}
return normalized;
}
private static bool IsNullOrWhiteSpace(string value)
{
if (value == null)
{
return true;
}
for (int i = 0; i < value.Length; i++)
{
if (!char.IsWhiteSpace(value[i]))
{
return false;
}
}
return true;
}
}
public class ShrinkwrapModelResult
{
public bool Success { get; set; }
public bool DryRun { get; set; }
public double Padding { get; set; }
public int TotalVisited { get; set; }
public int RemovedCount { get; set; }
public int KeptCount { get; set; }
public int ShellKeptCount { get; set; }
public List<string> RemovedElements { get; set; } = new List<string>();
public List<string> Errors { get; set; } = new List<string>();
public List<string> ZoneSummaries { get; set; } = new List<string>();
public System.DateTime StartedAt { get; set; }
public System.DateTime CompletedAt { get; set; }
public string Message { get; set; }
}
}

View File

@ -123,9 +123,18 @@ namespace TellmePdmsPluging.Network
case "/api/status/model":
responseJson = HandleModelStatus();
break;
case "/api/project/open":
responseJson = HandleProjectOpen(request);
break;
case "/api/mdb/open":
responseJson = HandleMdbOpen(request);
break;
case "/api/model/simplify":
responseJson = HandleModelSimplify(request);
break;
case "/api/model/shrinkwrap":
responseJson = HandleModelShrinkwrap(request);
break;
default:
response.StatusCode = 404;
responseJson = CreateErrorResponse(404, "接口不存在");
@ -251,6 +260,142 @@ namespace TellmePdmsPluging.Network
}
}
private string HandleModelShrinkwrap(HttpListenerRequest request)
{
try
{
var payload = ReadRequestBody(request);
if (string.IsNullOrEmpty(payload))
{
return CreateErrorResponse(400, "请求体不能为空");
}
var serializer = new JavaScriptSerializer();
var shrinkwrapRequest = serializer.Deserialize<ShrinkwrapModelRequest>(payload) ?? new ShrinkwrapModelRequest();
var command = new ShrinkwrapModelCommand(shrinkwrapRequest);
var invokeResult = MainThreadInvoker.Invoke(command, 600000);
if (!invokeResult.Success)
{
var msg = string.IsNullOrEmpty(invokeResult.Message) ? "外壳保留失败" : invokeResult.Message;
return CreateErrorResponse(500, msg);
}
var result = invokeResult.Result as ShrinkwrapModelResult;
if (result == null)
{
return CreateErrorResponse(500, "外壳保留结果为空");
}
if (!result.Success)
{
var message = string.IsNullOrEmpty(result.Message) ? "外壳保留失败" : result.Message;
return CreateErrorResponse(500, message);
}
return CreateSuccessResponse(result);
}
catch (Exception ex)
{
return CreateErrorResponse(500, $"外壳保留失败: {ex.Message}");
}
}
private string HandleProjectOpen(HttpListenerRequest request)
{
try
{
if (!string.Equals(request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
{
return CreateErrorResponse(405, "仅支持POST");
}
var payload = ReadRequestBody(request);
if (string.IsNullOrEmpty(payload))
{
return CreateErrorResponse(400, "请求体不能为空");
}
var serializer = new JavaScriptSerializer();
var openRequest = serializer.Deserialize<OpenProjectRequest>(payload) ?? new OpenProjectRequest();
var command = new OpenProjectCommand(openRequest);
var invokeResult = MainThreadInvoker.Invoke(command, 600000);
if (!invokeResult.Success)
{
var msg = string.IsNullOrEmpty(invokeResult.Message) ? "打开项目失败" : invokeResult.Message;
return CreateErrorResponse(500, msg);
}
var result = invokeResult.Result as OpenProjectResult;
if (result == null)
{
return CreateErrorResponse(500, "打开项目结果为空");
}
if (!result.Success)
{
var message = string.IsNullOrEmpty(result.Message) ? "打开项目失败" : result.Message;
return CreateErrorResponse(500, message);
}
return CreateSuccessResponse(result);
}
catch (Exception ex)
{
return CreateErrorResponse(500, $"打开项目失败: {ex.Message}");
}
}
private string HandleMdbOpen(HttpListenerRequest request)
{
try
{
if (!string.Equals(request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
{
return CreateErrorResponse(405, "仅支持POST");
}
var payload = ReadRequestBody(request);
if (string.IsNullOrEmpty(payload))
{
return CreateErrorResponse(400, "请求体不能为空");
}
var serializer = new JavaScriptSerializer();
var openRequest = serializer.Deserialize<OpenMdbRequest>(payload) ?? new OpenMdbRequest();
var command = new OpenMdbCommand(openRequest);
var invokeResult = MainThreadInvoker.Invoke(command, 600000);
if (!invokeResult.Success)
{
var msg = string.IsNullOrEmpty(invokeResult.Message) ? "打开MDB失败" : invokeResult.Message;
return CreateErrorResponse(500, msg);
}
var result = invokeResult.Result as OpenMdbResult;
if (result == null)
{
return CreateErrorResponse(500, "打开MDB结果为空");
}
if (!result.Success)
{
var message = string.IsNullOrEmpty(result.Message) ? "打开MDB失败" : result.Message;
return CreateErrorResponse(500, message);
}
return CreateSuccessResponse(result);
}
catch (Exception ex)
{
return CreateErrorResponse(500, $"打开MDB失败: {ex.Message}");
}
}
private string ReadRequestBody(HttpListenerRequest request)
{
if (request == null || request.InputStream == null)

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -59,6 +59,9 @@
<Reference Include="Aveva.Pdms.Database">
<HintPath>..\..\..\..\..\AVEVA\Plant\PDMS12.1.SP4\Aveva.Pdms.Database.dll</HintPath>
</Reference>
<Reference Include="Aveva.Pdms.Geometry">
<HintPath>..\..\..\..\..\AVEVA\Plant\PDMS12.1.SP4\Aveva.Pdms.Geometry.dll</HintPath>
</Reference>
<Reference Include="PMLNet">
<HintPath>..\..\..\..\..\AVEVA\Plant\PDMS12.1.SP4\PMLNet.dll</HintPath>
</Reference>
@ -73,11 +76,19 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Class1.cs" />
<Compile Include="Commands\OpenMdbCommand.cs" />
<Compile Include="Commands\OpenProjectCommand.cs" />
<Compile Include="Commands\ShrinkwrapModelCommand.cs" />
<Compile Include="Commands\SimplifyModelCommand.cs" />
<Compile Include="Core\ICommand.cs" />
<Compile Include="Core\ApiResponse.cs" />
<Compile Include="Core\MainThreadInvoker.cs" />
<Compile Include="Core\PdmsManager.cs" />
<Compile Include="Core\SafeQueue.cs" />
<Compile Include="Models\ModelStatusResponse.cs" />
<Compile Include="Models\OpenMdbRequest.cs" />
<Compile Include="Models\OpenProjectRequest.cs" />
<Compile Include="Models\ShrinkwrapModelRequest.cs" />
<Compile Include="Models\SimplifyModelRequest.cs" />
<Compile Include="Network\HttpServer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />