Сразу честно признаюсь, у этой статьи просто — «броский многообещающий» заголовок :)
Но на самом деле, на этой неделе у меня было мало свободного времени и поэтому в этот раз заметка будет короткой и совсем уж концептуальной. Поэтому до настоящей SCADA естественно дело не дойдет.
В прошлой статье мы разбирались как сделать свой пользовательский объект в отечественной САПР NanoCAD с помощью MultiCAD.NET API (как для платной, так и для бесплатной версии программы), в этот раз мы воспользуемся ранее разработанным объектом (дверью) и научим его отслеживать состояние текстового файла.
Зачем нам это надо? Милости прошу под кат.
Содержание:
Часть I: Введение.
Часть II: Пишем код под NanoCAD 8.5
Часть III: Пробуем адаптировать под бесплатный NanoCAD 5.1.
Часть IV: Заключение
В начале честно признаюсь, идея использовать NanoCAD как базу для простенькой SCADA системы по большей части принадлежит DRZugrik, просто у меня наконец-то дошли руки её проверить. Чтобы не расстроить чьи-нибудь ожидания еще раз повторюсь, в этой статье мы только приоткроем завесу и рассмотрим на мой взгляд немного нестандартную концепцию применения NanoCAD, не более.
Поскольку ковыряние в MultiCAD.NET API, это мое новое хобби, то мы продолжим работу именно с ним. Если вы совсем не знакомы с этим API, можно посмотреть первую статью по этой теме.
Ну а мы будем использовать наработки из второй статьи в частности нашу псевдотрехмерную дверь.
Вкратце напомню, в прошлый раз мы учились создавать пользовательские примитивы в NanoCAD, и в итоге создали стенку и дверь, наша дверь была сделана в виде псевдотрехмерного объекта (просто линии без свойств твердого тела) и умела открываться и закрываться в зависимости от выбранного значения в свойствах. Чтобы сэкономить время и не выдумывать ничего нового воспользуемся именно ей.
Вернемся к вопросу: «Зачем же нам читать данные из текстового файла?». Отвечаю — чтение из файла в некотором роде будет симуляцией работы с оборудованием. Теоретически можно было бы читать данные с web сервера или как-то приладить чтение с реальной «железки», но мы остановимся на самой простой концепции.
Ниже под спойлером представлен полный код обновленного класса для NanoCAD 8.5, также данный код доступен на GitHub, если вдруг вы забыли как самостоятельно создать проект для NanoCAD API в MS Visual Studio, то вот краткая инструкция для NC 8.5 и для NC 5.1 Free
На всякий случай, предупреждаю я — не программист, да и NanoCAD API я только начал осваивать, так что в коде могут быть ошибки ну или кривые реализации чего-либо.
Одна из критических ошибок, иногда возникает при копировании объектов и попытке переназначить функции мониторинга (впрочем, ошибки с копированием всплывали и в прошлой версии), а иногда ошибки просто вылетают на ровном месте. Если кто-то более опытный подскажет в чем причина буду признателен.
Также предупрежу, что двери нормально работают только если для каждой двери назначен свой уникальный файл, в противном случае могут быть сбои.
// Version 1.1
//Use Microsoft .NET Framework 4 and MultiCad.NET API 7.0
//Class for demonstrating the capabilities of MultiCad.NET
//Assembly for the Nanocad 8.5 SDK is recommended (however, it is may be possible in the all 8.Х family)
//Link imapimgd, mapimgd.dll and mapibasetypes.dll from SDK
//Link System.Windows.Forms and System.Drawing
//The commands: draws a pseudo 3D door.
//This code in the part of non-infringing rights Nanosoft can be used and distributed in any accessible ways.
//For the consequences of the code application, the developer is not responsible.
// V 1.0. More detailed - https://habrahabr.ru/post/342680/
// V 1.1. More detailed - https://habrahabr.ru/post/343772/
// P.S. A big thanks to Alexander Vologodsky for help in developing a method for pivoting object.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;
//added in V 1.1. for monitoring
using System.Security.Permissions;
using System.IO;
using Multicad.AplicationServices;
namespace nanodoor2
{
//change "8b0986c0-4163-42a4-b005-187111b499d7" for your Guid from Assembly.
// Be careful GUID for door and wall classes must be different!
// Otherwise there will be problems with saving and moving
[CustomEntity(typeof(DoorPseudo3D), "8b0986c0-4163-42a4-b005-187111b499d9", "DoorPseudo3D", "DoorPseudo3D Sample Entity")]
[Serializable]
public class DoorPseudo3D : McCustomBase
{
// First and second vertices of the box
private Point3d _pnt1 = new Point3d(0, 0, 0);
private double _h = 2085;
private Vector3d _vecStraightDirection = new Vector3d(1, 0, 0);
private Vector3d _vecDirectionClosed = new Vector3d(1, 0, 0);
public enum Status {closed , middle, open };
private Status _dStatus = Status.closed;
//added in V 1.1. (monitor fileds)
private bool _monitor = false;
private string _monFilePath = @"E:test.txt";
// if it's Serialized you can't copy the object in CAD editor
[NonSerialized]
private FileSystemWatcher _watcher ;
[NonSerialized]
private FileSystemEventHandler _watchHandler;
[CommandMethod("DrawDoor", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
public void DrawDoor() {
DoorPseudo3D door = new DoorPseudo3D();
door.PlaceObject();
}
public override void OnDraw(GeometryBuilder dc)
{
dc.Clear();
// Define the basic points for drawing
Point3d pnt1 = new Point3d(0, 0, 0);
Point3d pnt2 = new Point3d(pnt1.X + 984, pnt1.Y, 0);
Point3d pnt3 = new Point3d(pnt2.X + 0, pnt1.Y+50, 0);
Point3d pnt4 = new Point3d(pnt1.X , pnt3.Y, 0);
// Set the color to ByObject value
dc.Color = McDbEntity.ByObject;
Vector3d hvec = new Vector3d(0, 0, _h);
// Draw the upper and lower sides
dc.DrawPolyline(new Point3d[] { pnt1, pnt2, pnt3, pnt4, pnt1 });
dc.DrawPolyline(new Point3d[] { pnt1.Add(hvec),
pnt2.Add(hvec), pnt3.Add(hvec), pnt4.Add(hvec), pnt1.Add(hvec)});
// Draw the edges
dc.DrawLine(pnt1, pnt1.Add(hvec));
dc.DrawLine(pnt2, pnt2.Add(hvec));
dc.DrawLine(pnt3, pnt3.Add(hvec));
dc.DrawLine(pnt4, pnt4.Add(hvec));
// Drawing a Door Handle
dc.DrawLine(pnt2.Add(new Vector3d( - 190, -0, _h*0.45)),
pnt2.Add(new Vector3d(-100, 0, _h * 0.45)));
dc.DrawLine(pnt3.Add(new Vector3d(-190, 0, _h * 0.45)),
pnt3.Add(new Vector3d(-100, 0, _h * 0.45)));
// Create contours for the front and rear sides and hatch them
// In this demo, we hatch only two sides, you can tailor the others yourself
List<Polyline3d> c1 = new List<Polyline3d>();
c1.Add(new Polyline3d(
new List<Point3d>() { pnt1, pnt1.Add(hvec), pnt2.Add(hvec), pnt2, pnt1, }));
List<Polyline3d> c2 = new List<Polyline3d>();
c2.Add(new Polyline3d(
new List<Point3d>() { pnt4, pnt4.Add(hvec), pnt3.Add(hvec), pnt3, pnt4, }));
dc.DrawGeometry(new Hatch(c1, "JIS_WOOD", 0, 170, false, HatchStyle.Normal, PatternType.PreDefined, 500), 1);
dc.DrawGeometry(new Hatch(c2, "JIS_WOOD", 0, 170, false, HatchStyle.Normal, PatternType.PreDefined, 500), 1);
}
public override hresult PlaceObject(PlaceFlags lInsertType)
{
InputJig jig = new InputJig();
// Get the first box point from the jig
InputResult res = jig.GetPoint("Select first point:");
if (res.Result != InputResult.ResultCode.Normal)
return hresult.e_Fail;
_pnt1 = res.Point;
// Add the object to the database
DbEntity.AddToCurrentDocument();
return hresult.s_Ok;
}
/// <summary>
/// Method for changing the object's SC (the graph is built at the origin of coordinates).
/// </ summary>
/// <param name = "tfm"> The matrix for changing the position of the object. </ param>
/// <returns> True - if the matrix is passed, False - if not. </ returns>
public override bool GetECS(out Matrix3d tfm)
{
// Create a matrix that transforms the object.
// The object is drawn in coordinates(0.0), then it is transformed with the help of this matrix.
tfm = Matrix3d.Displacement(this._pnt1.GetAsVector()) * Matrix3d.Rotation
(-this._vecStraightDirection.GetAngleTo(Vector3d.XAxis, Vector3d.ZAxis), Vector3d.ZAxis, Point3d.Origin);
return true;
}
public override void OnTransform(Matrix3d tfm)
{
// To be able to cancel(Undo)
McUndoPoint undo = new McUndoPoint();
undo.Start();
// Get the coordinates of the base point and the rotation vector
this.TryModify();
this._pnt1 = this._pnt1.TransformBy(tfm);
this.TryModify();
this._vecStraightDirection = this._vecStraightDirection.TransformBy(tfm);
// We move the door only when it is closed if not - undo
if (_dStatus == Status.closed) _vecDirectionClosed = _vecStraightDirection;
else
{
MessageBox.Show("Please transform only closed door");
undo.Undo();
}
undo.Stop();
}
//Define the custom properties of the object
[DisplayName("Height")]
[Description("Height of door")]
[Category("Door options")]
public double HDoor
{
get
{
return _h;
}
set
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_h = value;
}
}
[DisplayName("Door status")]
[Description("Door may be: closed, middle, open")]
[Category("Door options")]
public Status Stat
{
get
{
return _dStatus;
}
set
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
// Change the rotation vector for each of the door states
switch (value)
{
case Status.closed:
_vecStraightDirection = _vecDirectionClosed;
break;
case Status.middle:
_vecStraightDirection = _vecDirectionClosed.Add(_vecDirectionClosed.GetPerpendicularVector().Negate() * 0.575) ;
break;
case Status.open:
_vecStraightDirection = _vecDirectionClosed.GetPerpendicularVector()*-1;
break;
default:
_vecStraightDirection = _vecDirectionClosed;
break;
}
_dStatus = value;
}
}
// Create a grip for the base point of the object
public override bool GetGripPoints(GripPointsInfo info)
{
info.AppendGrip(new McSmartGrip<DoorPseudo3D>(_pnt1, (obj, g, offset) => { obj.TryModify(); obj._pnt1 += offset; }));
return true;
}
//Define the monitoring custom properties , added v. 1.1:
// added in v. 1.1
[DisplayName("Monitoring")]
[Description("Monitoring of file for door")]
[Category("Monitoring")]
public bool Monitor
{
get
{
return _monitor;
}
set
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_monitor = value;
if (_monitor)
{
StartMonitoring();
}
else StopMonitoring();
}
}
// added in v. 1.1
[DisplayName("File path for Monitoring")]
[Description("Monitoring of file for door")]
[Category("Monitoring")]
public string MonitoringFilPath
{
get
{
return _monFilePath;
}
set
{
//for hot change filename
if (Monitor)
{
StopMonitoring();
if (!TryModify())
return;
_monFilePath = value;
StartMonitoring();
McContext.ShowNotification("Monitored file is changed");
}
else
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_monFilePath = value;
}
}
}
//Define the methods, added v. 1.1:
// added in v. 1.1
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public void StartMonitoring()
{
_watcher = new FileSystemWatcher();
if (File.Exists(_monFilePath))
{
_watcher.Path = Path.GetDirectoryName(_monFilePath);
_watcher.Filter = Path.GetFileName(_monFilePath);
_watchHandler = new FileSystemEventHandler(OnChanged);
_watcher.Changed += _watchHandler;
_watcher.EnableRaisingEvents = true;
}
else McContext.ShowNotification("File: " + _monFilePath + " " + "not Exists");
}
// added in v. 1.1
public void StopMonitoring()
{
if (_watcher != null & _watchHandler != null)
{
_watcher.Changed -= _watchHandler;
_watcher.EnableRaisingEvents = false;
}
}
// added in v. 1.1
private void OnChanged(object source, FileSystemEventArgs e)
{
McContext.ShowNotification("File: " + e.FullPath + " " + e.ChangeType);
//read new value from file
try
{
if (File.Exists(_monFilePath))
{
int mStatus = -1;
McContext.ShowNotification("File exists ");
using (StreamReader sr = new StreamReader(new FileStream(_monFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
if (sr.BaseStream.CanRead)
{
McContext.ShowNotification("can read ");
if (int.TryParse(sr.ReadLine(), out mStatus))
{
McContext.ShowNotification("parse correct ");
if (Enum.IsDefined(typeof(Status), mStatus))
{
if (!TryModify()) return;
Stat = (Status) mStatus;
if (!TryModify()) return;
if (!DbEntity.Update()) return;
McContext.ShowNotification("Door state is changed");
McContext.ExecuteCommand("REGENALL");
}
else McContext.ShowNotification("Incorrect data in the file. Should be in diapason: 0, 1, 2 ");
}
}
else McContext.ShowNotification("Can't read file ");
}
}
else McContext.ShowNotification("File not exists ");
_watcher.EnableRaisingEvents = false; // disable tracking
}
finally
{
_watcher.EnableRaisingEvents = true; // reconnect tracking
}
}
}
// TODO: There are many shortcomings in this code.
// Including failures when working with copying, moving objects and saving files, you can improve it if you want.
}
Кратко разберем некоторые новые моменты (относительно прошлой статьи).
//added in V 1.1. for monitoring
using System.Security.Permissions;
using System.IO;
using Multicad.AplicationServices;
Новые пространства имен. Будут нам нужны для мониторинга файлов, чтения из файла, а также просто для вывода сообщений в консоль программы.
//added in V 1.1. for monitoring
//added in V 1.1. (monitor fileds)
private bool _monitor = false;
private string _monFilePath = @"E:test.txt";
FileSystemEventHandler _watchHandler;
FileSystemWatcher _watcher;
Новые типы и поля класса для свойств, позволяющих отслеживать состояние двери.
//Define the monitoring custom properties , added v. 1.1:
// added in v. 1.1
[DisplayName("Monitoring")]
[Description("Monitoring of file for door")]
[Category("Monitoring")]
public bool Monitor
{
get
{
return _monitor;
}
set
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_monitor = value;
if (_monitor)
{
StartMonitoring();
}
else StopMonitoring();
}
}
// added in v. 1.1
[DisplayName("File path for Monitoring")]
[Description("Monitoring of file for door")]
[Category("Monitoring")]
public string MonitoringFilPath
{
get
{
return _monFilePath;
}
set
{
//for hot change filename
if (Monitor)
{
StopMonitoring();
if (!TryModify())
return;
_monFilePath = value;
StartMonitoring();
McContext.ShowNotification("Monitored file is changed");
}
else
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_monFilePath = value;
}
}
}
Первое свойство отвечает за включение и отключение режима отслеживания файлов.
Внимание! Я не стал реализовывать считывание данных из файла при включении этой опции, реальная синхронизация объекта начнется после первого изменения считываемого файла.
Второе свойство содержит в себе адрес до файла назначения (мы отслеживаем для одного объекта один файл, причем уникальный или будут сбои). Если мониторинг файлов включен, то при изменении этого свойства мониторинг на новый файл переключается автоматически.
Пойдем дальше
//Define the methods, added v. 1.1:
// added in v. 1.1
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public void StartMonitoring()
{
_watcher = new FileSystemWatcher();
if (File.Exists(_monFilePath))
{
_watcher.Path = Path.GetDirectoryName(_monFilePath);
_watcher.Filter = Path.GetFileName(_monFilePath);
_watchHandler = new FileSystemEventHandler(OnChanged);
_watcher.Changed += _watchHandler;
_watcher.EnableRaisingEvents = true;
}
else McContext.ShowNotification("File: " + _monFilePath + " " + "not Exists");
}
// added in v. 1.1
public void StopMonitoring()
{
if (_watcher != null & _watchHandler != null)
{
_watcher.Changed -= _watchHandler;
_watcher.EnableRaisingEvents = false;
}
}
Первый метод с помощью обычной .NET библиотеки позволяет нам подписаться на информацию системы об изменении файла.
Второй метод – отписывает нас от слежения.
[PermissionSet(SecurityAction.Demand, Name = «FullTrust»)] как и большая часть кода метода взяты из Микрософтовского примера. Решил не трогать.
Остался последний кусочек обновленного кода.
// added in v. 1.1
private void OnChanged(object source, FileSystemEventArgs e)
{
McContext.ShowNotification("File: " + e.FullPath + " " + e.ChangeType);
//read new value from file
try
{
if (File.Exists(_monFilePath))
{
int mStatus = -1;
McContext.ShowNotification("File exists ");
using (StreamReader sr = new StreamReader(new FileStream(_monFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
if (sr.BaseStream.CanRead)
{
if (int.TryParse(sr.ReadLine(), out mStatus))
{
if (Enum.IsDefined(typeof(Status), mStatus))
{
if (!this.TryModify()) return;
this.Stat = (Status)mStatus;
if (!TryModify()) return;
this.DbEntity.Update();
McContext.ExecuteCommand("REGENALL");
McContext.ShowNotification("Door state is changed");
}
else McContext.ShowNotification("Incorrect data in the file. Should be in diapason: 0, 1, 2 ");
}
}
else McContext.ShowNotification("Can't read file ");
}
}
else McContext.ShowNotification("File not exists ");
_watcher.EnableRaisingEvents = false; // disable tracking
}
finally
{
_watcher.EnableRaisingEvents = true; // reconnect tracking
}
}
Метод для реакции на событие. Вначале проверяем существует ли наш файл, потом пытаемся вытащить из него первую строку и если она совпадает со значением перечисления (в диапазоне от 0 до 2), то изменяем наше свойство управляющее состоянием двери, после чего выполняем стандартную команду обновления экрана, потому что без нее изменения отображаются не всегда.
Для бесплатной версии Nanocad 5.1, код почти такой же.
// Version 1.1
//Use Microsoft .NET Framework 3.5 and old version of MultiCad.NET (for NC 5.1)
//Class for demonstrating the capabilities of MultiCad.NET
//Assembly for the Nanocad 5.1
//Link mapimgd.dll from Nanocad SDK
//Link System.Windows.Forms and System.Drawing
//upd: for version 1.1 also link .NET API: hostdbmg.dll, hostmgd.dll
//The commands: draws a pseudo 3D door.
//This code in the part of non-infringing rights Nanosoft can be used and distributed in any accessible ways.
//For the consequences of the code application, the developer is not responsible.
// V 1.0. More detailed - https://habrahabr.ru/post/342680/
// V 1.1. More detailed - https://habrahabr.ru/post/343772/
// P.S. A big thanks to Alexander Vologodsky for help in developing a method for pivoting object.
using System;
using System.ComponentModel;
using System.Windows.Forms;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;
//added in V 1.1. for monitoring
using System.Security.Permissions;
using System.IO;
using Multicad.AplicationServices;
using HostMgd.ApplicationServices;
using HostMgd.EditorInput;
namespace nanodoor2
{
//change "8b0986c0-4163-42a4-b005-187111b499d7" for your Guid from Assembly.
// Be careful GUID for door and wall classes must be different!
// Otherwise there will be problems with saving and moving
[CustomEntity(typeof(DoorPseudo3D_nc51), "b4edac1f-7978-483f-91b1-10503d20735b", "DoorPseudo3D_nc51", "DoorPseudo3D_nc51 Sample Entity")]
[Serializable]
public class DoorPseudo3D_nc51 : McCustomBase
{
// First and second vertices of the box
private Point3d _pnt1 = new Point3d(0, 0, 0);
private double _scale = 1000;
private double _h = 2085;
private Vector3d _vecStraightDirection = new Vector3d(1, 0, 0);
private Vector3d _vecDirectionClosed = new Vector3d(1, 0, 0);
public enum Status {closed , middle, open};
private Status _dStatus = Status.closed;
//added in V 1.1. (monitor fileds)
public enum Mon { off, on};
private Mon _monitor = Mon.off;
private string _monFilePath = @"E:test.txt";
// if it is serialized, you may not be able to copy the object in the CAD editor
[NonSerialized]
private FileSystemWatcher _watcher;
[NonSerialized]
private FileSystemEventHandler _watchHandler;
[CommandMethod("DrawDoor", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
public void DrawDoor() {
DoorPseudo3D_nc51 door = new DoorPseudo3D_nc51();
door.PlaceObject();
this.TryModify();
// this.Monitor = false;
}
public override void OnDraw(GeometryBuilder dc)
{
dc.Clear();
// Define the basic points for drawing
Point3d pnt1 = new Point3d(0, 0, 0);
Point3d pnt2 = new Point3d(pnt1.X + (984 * _scale), pnt1.Y, 0);
Point3d pnt3 = new Point3d(pnt2.X + 0, pnt1.Y+(50 * _scale), 0);
Point3d pnt4 = new Point3d(pnt1.X , pnt3.Y, 0) ;
// Set the color to ByObject value
dc.Color = McDbEntity.ByObject;
Vector3d hvec = new Vector3d(0, 0, _h * _scale) ;
// Draw the upper and lower sides
dc.DrawPolyline(new Point3d[] { pnt1, pnt2, pnt3, pnt4, pnt1 });
dc.DrawPolyline(new Point3d[] { pnt1.Add(hvec),
pnt2.Add(hvec), pnt3.Add(hvec), pnt4.Add(hvec), pnt1.Add(hvec)});
// Draw the edges
dc.DrawLine(pnt1, pnt1.Add(hvec));
dc.DrawLine(pnt2, pnt2.Add(hvec));
dc.DrawLine(pnt3, pnt3.Add(hvec));
dc.DrawLine(pnt4, pnt4.Add(hvec));
// Drawing a Door Handle
dc.DrawLine(pnt2.Add(new Vector3d( -190 * _scale, -0, _h*0.45 * _scale)),
pnt2.Add(new Vector3d(-100 * _scale, 0, _h * 0.45 * _scale)));
dc.DrawLine(pnt3.Add(new Vector3d(-190 * _scale, 0, _h * 0.45 * _scale)),
pnt3.Add(new Vector3d(-100 * _scale, 0, _h * 0.45 * _scale)));
}
public override hresult PlaceObject(PlaceFlags lInsertType)
{
InputJig jig = new InputJig();
// Get the first box point from the jig
InputResult res = jig.GetPoint("Select first point:");
if (res.Result != InputResult.ResultCode.Normal)
return hresult.e_Fail;
_pnt1 = res.Point;
Stat = Status.closed;
// Add the object to the database
DbEntity.AddToCurrentDocument();
// added in v.1.
return hresult.s_Ok;
}
/// <summary>
/// Method for changing the object's SC (the graph is built at the origin of coordinates).
/// </ summary>
/// <param name = "tfm"> The matrix for changing the position of the object. </ param>
/// <returns> True - if the matrix is passed, False - if not. </ returns>
public override bool GetECS(out Matrix3d tfm)
{
// Create a matrix that transforms the object.
// The object is drawn in coordinates(0.0), then it is transformed with the help of this matrix.
tfm = Matrix3d.Displacement(this._pnt1.GetAsVector()) * Matrix3d.Rotation
(-this._vecStraightDirection.GetAngleTo(Vector3d.XAxis, Vector3d.ZAxis), Vector3d.ZAxis, Point3d.Origin);
return true;
}
public override void OnTransform(Matrix3d tfm)
{
// To be able to cancel(Undo)
McUndoPoint undo = new McUndoPoint();
undo.Start();
// Get the coordinates of the base point and the rotation vector
this.TryModify();
this._pnt1 = this._pnt1.TransformBy(tfm);
this.TryModify();
this._vecStraightDirection = this._vecStraightDirection.TransformBy(tfm);
// We move the door only when it is closed if not - undo
if (_dStatus == Status.closed) _vecDirectionClosed = _vecStraightDirection;
else
{
MessageBox.Show("Please transform only closed door (when its status = 0)");
undo.Undo();
}
undo.Stop();
}
//Define the custom properties of the object
[DisplayName("Height")]
[Description("Height of door")]
[Category("Door options")]
public double HDoor
{
get
{
return _h;
}
set
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_h = value;
}
}
[DisplayName("DScale")]
[Description("Door Scale")]
[Category("Door options")]
public double DScale
{
get
{
return _scale;
}
set
{
if (!TryModify())
return;
_scale = value;
}
}
[DisplayName("Door status")]
[Description("0-closed, 1-midle, 2-open")]
[Category("Door options")]
public Status Stat
{
get
{
return _dStatus;
}
set
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
// Change the rotation vector for each of the door states
switch (value)
{
case Status.closed:
_vecStraightDirection = _vecDirectionClosed;
break;
case Status.middle:
_vecStraightDirection = _vecDirectionClosed.Add(_vecDirectionClosed.GetPerpendicularVector().Negate() * 0.575) ;
break;
case Status.open:
_vecStraightDirection = _vecDirectionClosed.GetPerpendicularVector()*-1;
break;
default:
break;
}
_dStatus = value;
}
}
// Create a grip for the base point of the object
public override bool GetGripPoints(GripPointsInfo info)
{
info.AppendGrip(new McSmartGrip<DoorPseudo3D_nc51>(_pnt1, (obj, g, offset) => { obj.TryModify(); obj._pnt1 += offset; }));
return true;
}
//Define the monitoring custom properties , added v. 1.1:
// added in v. 1.1
[DisplayName("Monitoring")]
[Description("Monitoring of file for door")]
[Category("Monitoring")]
public Mon Monitor
{
get
{
return _monitor;
}
set
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_monitor = value;
if (_monitor==Mon.on)
{
StartMonitoring();
}
else StopMonitoring();
// if (_monitor)
// {
// StartMonitoring();
// }// Get the command line editor
// else StopMonitoring();
}
}
// added in v. 1.1
[DisplayName("File path for Monitoring")]
[Description("Monitoring of file for door")]
[Category("Monitoring")]
public string MonitoringFilPath
{
get
{
return _monFilePath;
}
set
{
// Get the command line editor
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
//for hot change filename
if (Monitor==Mon.on)
{
StopMonitoring();
if (!TryModify())
return;
_monFilePath = value;
StartMonitoring();
ed.WriteMessage("Monitored file is changed");
}
else
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_monFilePath = value;
}
}
}
//Define the methods, added v. 1.1:
// added in v. 1.1
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public void StartMonitoring()
{
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
_watcher = new FileSystemWatcher();
if (File.Exists(_monFilePath))
{
_watcher.Path = Path.GetDirectoryName(_monFilePath);
_watcher.Filter = Path.GetFileName(_monFilePath);
_watchHandler = new FileSystemEventHandler(OnChanged);
_watcher.Changed += _watchHandler;
_watcher.EnableRaisingEvents = true;
}
else ed.WriteMessage("File: " + _monFilePath + " " + "not Exists");
}
// added in v. 1.1
public void StopMonitoring()
{
if (_watcher != null & _watchHandler != null)
{
_watcher.Changed -= _watchHandler;
_watcher.EnableRaisingEvents = false;
}
}
// added in v. 1.1
private void OnChanged(object source, FileSystemEventArgs e)
{
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
ed.WriteMessage("File: " + e.FullPath + " " + e.ChangeType);
//read new value from file
try
{
if (File.Exists(_monFilePath))
{
int mStatus = -1;
ed.WriteMessage("File exists ");
using (StreamReader sr = new StreamReader(new FileStream(_monFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
if (sr.BaseStream.CanRead)
{
if (int.TryParse(sr.ReadLine(), out mStatus))
{
if (Enum.IsDefined(typeof(Status), mStatus))
{
if (!TryModify()) return;
Stat = (Status)mStatus; // преобразование
if (!TryModify()) return;
DbEntity.Update();
McContext.ExecuteCommand("REGENALL");
ed.WriteMessage("Door state is changed");
}
else ed.WriteMessage("Incorrect data in the file. Should be in diapason: 0, 1, 2 ");
}
}
else ed.WriteMessage("Can't read file ");
}
}
else ed.WriteMessage("File not exists ");
_watcher.EnableRaisingEvents = false; // disable tracking
}
finally
{
_watcher.EnableRaisingEvents = true; // reconnect tracking
}
}
}
// TODO: There are many shortcomings in this code.
// Including failures when working with copying, moving objects and saving files, you can improve it if you want.
}
Разница в основном заключается в том, что в старой версии MultiCAD.NET API еще не был реализован McContext.ShowNotification(), поэтому мы подключаем обычные .NET API (библиотеки hostdbmg.dll и hostmgd.dll). После чего заменяем этот метод связкой:
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
ed.WriteMessage("File: " + e.FullPath + " " + e.ChangeType);
Где первые две строки позволяют получить доступ к консоли ввода, а последняя точно также выводит в неё текстовое сообщение.
В результате получим, то что на картинке.
Верхние рисунки для версии NC 8.5 нижние для NC 5.1 объекты показаны как в псевдотрехмерном так и в двухмерном виде.
Пример рассмотренный здесь жутко сбоит с одной стороны, с другой стороны не учитывает взаимодействие с сервером, но так или иначе данный простенький пример концептуально показывает потенциал NanoCAD как платформы для простенькой SCADA.
Можно сделать еще много чего, добавить диалоги для квитирования изменения объектов, работу с железом, разграничении уровней доступа и так далее. Благодаря тонким возможностям настройки пользовательской среды теоретически платформу можно использовать и как редактор для отображения SCADA системы, и как пользовательскую среду, то есть убрать все чертежные инструменты и оставить только окно для мониторинга и управления.
Я безусловно не призываю делать на базе NanoCAD конкурента Wonderware InTouch, но вот помню лет пять назад была простенькая SCADA «Алгоритм» от Болида, (она собственно и сейчас есть, просто я не в курсе как она за 5 лет развилась ) и думаю, что аналог подобной вещи (той версии, что была 5 лет назад) по функционалу можно было бы написать опираясь на платформу NanoCAD.
Возможно я к теме SCADA на базе NanoCADa еще вернусь, а пока буду рад почитать конструктивные комментарии и конечно же пожелать всем хорошей трудовой недели!
Автор: Роман