«Я слежу за тобой» или как из CADa сделать SCADA (MultiCAD.NET API)

в 19:22, , рубрики: .net, api, C#, c#.net, CAD/CAM, nanoCAD, scada, новичкам, сапр

Сразу честно признаюсь, у этой статьи просто — «броский многообещающий» заголовок :)
Но на самом деле, на этой неделе у меня было мало свободного времени и поэтому в этот раз заметка будет короткой и совсем уж концептуальной. Поэтому до настоящей SCADA естественно дело не дойдет.

В прошлой статье мы разбирались как сделать свой пользовательский объект в отечественной САПР NanoCAD с помощью MultiCAD.NET API (как для платной, так и для бесплатной версии программы), в этот раз мы воспользуемся ранее разработанным объектом (дверью) и научим его отслеживать состояние текстового файла.

Зачем нам это надо? Милости прошу под кат.
«Я слежу за тобой» или как из CADa сделать SCADA (MultiCAD.NET API) - 1


Содержание:
Часть 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 я только начал осваивать, так что в коде могут быть ошибки ну или кривые реализации чего-либо.

Одна из критических ошибок, иногда возникает при копировании объектов и попытке переназначить функции мониторинга (впрочем, ошибки с копированием всплывали и в прошлой версии), а иногда ошибки просто вылетают на ровном месте. Если кто-то более опытный подскажет в чем причина буду признателен.

Также предупрежу, что двери нормально работают только если для каждой двери назначен свой уникальный файл, в противном случае могут быть сбои.

Полный код для двери под NC 8.5

// 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, код почти такой же.

Полный код для двери под NC 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);

Где первые две строки позволяют получить доступ к консоли ввода, а последняя точно также выводит в неё текстовое сообщение.

В результате получим, то что на картинке.

«Я слежу за тобой» или как из CADa сделать SCADA (MultiCAD.NET API) - 2

Верхние рисунки для версии NC 8.5 нижние для NC 5.1 объекты показаны как в псевдотрехмерном так и в двухмерном виде.


Подведём итоги.

Пример рассмотренный здесь жутко сбоит с одной стороны, с другой стороны не учитывает взаимодействие с сервером, но так или иначе данный простенький пример концептуально показывает потенциал NanoCAD как платформы для простенькой SCADA.

Можно сделать еще много чего, добавить диалоги для квитирования изменения объектов, работу с железом, разграничении уровней доступа и так далее. Благодаря тонким возможностям настройки пользовательской среды теоретически платформу можно использовать и как редактор для отображения SCADA системы, и как пользовательскую среду, то есть убрать все чертежные инструменты и оставить только окно для мониторинга и управления.

Я безусловно не призываю делать на базе NanoCAD конкурента Wonderware InTouch, но вот помню лет пять назад была простенькая SCADA «Алгоритм» от Болида, (она собственно и сейчас есть, просто я не в курсе как она за 5 лет развилась ) и думаю, что аналог подобной вещи (той версии, что была 5 лет назад) по функционалу можно было бы написать опираясь на платформу NanoCAD.

Возможно я к теме SCADA на базе NanoCADa еще вернусь, а пока буду рад почитать конструктивные комментарии и конечно же пожелать всем хорошей трудовой недели!

Автор: Роман

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js