csharp/3RD-Dimension/3RDD-GCode-Sender-Issues/GCodeSender/Communication/Machine.cs

Communication
Machine.cs
using GCodeSender.GCode;
using GCodeSender.GCode.GCodeCommands;
using GCodeSender.Util;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Ports;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
// TODO Send $I to get VER and OPT and Parse
namespace GCodeSender.Communication
{
    enum ConnectionType
    {
        Serial
    }

    clast Machine
    {
        public enum OperatingMode
        {
            Manual,
            SendFile,
            Probe,
            Disconnected,
            SendMacro
        }

        public event Action ProbeFinished;
        public event Action NonFatalException;
        public event Action Info;
        public event Action LineReceived;
        public event Action StatusReceived;
        public event Action LineSent;
        public event Action ConnectionStateChanged;
        public event Action PositionUpdateReceived;
        public event Action StatusChanged;
        public event Action DistanceModeChanged;
        public event Action UnitChanged;
        public event Action PlaneChanged;
        public event Action BufferStateChanged;
        public event Action PinStateChanged;
        public event Action OperatingModeChanged;
        public event Action FileChanged;
        public event Action FilePositionChanged;
        public event Action OverrideChanged;

        public Vector3 MachinePosition { get; private set; } = new Vector3();   //No events here, the parser triggers a single event for both
        public Vector3 WorkOffset { get; private set; } = new Vector3();
        public Vector3 WorkPosition { get { return MachinePosition - WorkOffset; } }

        public Vector3 LastProbePosMachine { get; private set; }
        public Vector3 LastProbePosWork { get; private set; }

        public int FeedOverride { get; private set; } = 100;
        public int RapidOverride { get; private set; } = 100;
        public int SpindleOverride { get; private set; } = 100;

        public bool PinStateProbe { get; private set; } = false;
        public bool PinStateLimitX { get; private set; } = false;
        public bool PinStateLimitY { get; private set; } = false;
        public bool PinStateLimitZ { get; private set; } = false;

        public double FeedRateRealtime { get; private set; } = 0;
        public double SpindleSpeedRealtime { get; private set; } = 0;

        public String GRBL_Version { get; private set; } = "None";

        public double CurrentTLO { get; private set; } = 0;

        private Calculator _calculator;
        public Calculator Calculator { get { return _calculator; } }

        private ReadOnlyCollection _pauselines = new ReadOnlyCollection(new bool[0]);
        public ReadOnlyCollection PauseLines
        {
            get { return _pauselines; }
            private set { _pauselines = value; }
        }

        WorkOffsetsWindow WOST = new WorkOffsetsWindow(); // New WorkOffsetWindow Object

        private ReadOnlyCollection _file = new ReadOnlyCollection(new string[0]);
        public ReadOnlyCollection File
        {
            get { return _file; }
            private set
            {
                _file = value;
                FilePosition = 0;

                RaiseEvent(FileChanged);
            }
        }

        private int _filePosition = 0;
        public int FilePosition
        {
            get { return _filePosition; }
            private set
            {
                _filePosition = value;
            }
        }

        private OperatingMode _mode = OperatingMode.Disconnected;
        public OperatingMode Mode
        {
            get { return _mode; }
            private set
            {
                if (_mode == value)
                    return;

                _mode = value;
                RaiseEvent(OperatingModeChanged);
            }
        }

        #region Status
        private string _status = "Disconnected";
        public string Status
        {
            get { return _status; }
            private set
            {
                if (_status == value)
                    return;
                _status = value;

                RaiseEvent(StatusChanged);
            }
        }

        private ParseDistanceMode _distanceMode = ParseDistanceMode.Absolute;
        public ParseDistanceMode DistanceMode
        {
            get { return _distanceMode; }
            private set
            {
                if (_distanceMode == value)
                    return;
                _distanceMode = value;

                RaiseEvent(DistanceModeChanged);
            }
        }

        private ParseUnit _unit = ParseUnit.Metric;
        public ParseUnit Unit
        {
            get { return _unit; }
            private set
            {
                if (_unit == value)
                    return;
                _unit = value;

                RaiseEvent(UnitChanged);
            }
        }

        private ArcPlane _plane = ArcPlane.XY;
        public ArcPlane Plane
        {
            get { return _plane; }
            private set
            {
                if (_plane == value)
                    return;
                _plane = value;

                RaiseEvent(PlaneChanged);
            }
        }

        private bool _connected = false;
        public bool Connected
        {
            get { return _connected; }
            private set
            {
                if (value == _connected)
                    return;

                _connected = value;

                if (!Connected)
                    Mode = OperatingMode.Disconnected;

                RaiseEvent(ConnectionStateChanged);
            }
        }

        private int _bufferState;
        public int BufferState
        {
            get { return _bufferState; }
            private set
            {
                if (_bufferState == value)
                    return;

                _bufferState = value;

                RaiseEvent(BufferStateChanged);
            }
        }
        #endregion Status

        public bool SyncBuffer { get; set; }

        private Stream Connection;
        private Thread WorkerThread;

        private StreamWriter Log;

        private void RecordLog(string message)
        {
            if (Log != null)
            {
                try
                {
                    Log.WriteLine(message);
                }
                catch { throw; }
            }
        }

        public Machine()
        {
            _calculator = new Calculator(this);
        }

        Queue Sent = Queue.Synchronized(new Queue());
        Queue ToSend = Queue.Synchronized(new Queue());
        Queue ToSendPriority = Queue.Synchronized(new Queue()); //contains characters (for soft reset, feed hold etc)
        Queue ToSendMacro = Queue.Synchronized(new Queue());

        private void Work()
        {
            try
            {
                StreamReader reader = new StreamReader(Connection);
                StreamWriter writer = new StreamWriter(Connection);

                int StatusPollInterval = Properties.Settings.Default.StatusPollInterval;

                int ControllerBufferSize = Properties.Settings.Default.ControllerBufferSize;
                BufferState = 0;

                TimeSpan WaitTime = TimeSpan.FromMilliseconds(0.5);
                DateTime LastStatusPoll = DateTime.Now + TimeSpan.FromSeconds(0.5);
                DateTime StartTime = DateTime.Now;

                DateTime LastFilePosUpdate = DateTime.Now;
                bool filePosChanged = false;

                bool SendMacroStatusReceived = false;

                writer.Write("\n$G\n");
                writer.Write("\n$#\n");
                writer.Flush();

                while (true)
                {
                    Task lineTask = reader.ReadLineAsync();

                    while (!lineTask.IsCompleted)
                    {
                        if (!Connected)
                        {
                            return;
                        }

                        while (ToSendPriority.Count > 0)
                        {
                            writer.Write((char)ToSendPriority.Dequeue());
                            writer.Flush();
                        }
                        if (Mode == OperatingMode.SendFile)
                        {
                            if (File.Count > FilePosition && (File[FilePosition].Length + 1) < (ControllerBufferSize - BufferState))
                            {
                                string send_line = File[FilePosition].Replace(" ", ""); // don't send whitespace to machine

                                writer.Write(send_line);
                                writer.Write('\n');
                                writer.Flush();

                                RecordLog("> " + send_line);

                                RaiseEvent(UpdateStatus, send_line);
                                RaiseEvent(LineSent, send_line);

                                BufferState += send_line.Length + 1;

                                Sent.Enqueue(send_line);

                                if (PauseLines[FilePosition] && Properties.Settings.Default.PauseFileOnHold)
                                {
                                    Mode = OperatingMode.Manual;
                                }

                                if (++FilePosition >= File.Count)
                                {
                                    Mode = OperatingMode.Manual;
                                }

                                filePosChanged = true;
                            }
                        }
                        else if (Mode == OperatingMode.SendMacro)
                        {
                            switch (Status)
                            {
                                case "Idle":
                                    if (BufferState == 0 && SendMacroStatusReceived)
                                    {
                                        SendMacroStatusReceived = false;

                                        string send_line = (string)ToSendMacro.Dequeue();

                                        send_line = Calculator.Evaluate(send_line, out bool success);

                                        if (!success)
                                        {
                                            ReportError("Error while evaluating macro!");
                                            ReportError(send_line);

                                            ToSendMacro.Clear();
                                        }
                                        else
                                        {
                                            send_line = send_line.Replace(" ", "");

                                            writer.Write(send_line);
                                            writer.Write('\n');
                                            writer.Flush();

                                            RecordLog("> " + send_line);

                                            RaiseEvent(UpdateStatus, send_line);
                                            RaiseEvent(LineSent, send_line);

                                            BufferState += send_line.Length + 1;

                                            Sent.Enqueue(send_line);
                                        }
                                    }
                                    break;
                                case "Run":
                                case "Hold":
                                    break;
                                default:    // grbl is in some kind of alarm state
                                    ToSendMacro.Clear();
                                    break;
                            }

                            if (ToSendMacro.Count == 0)
                                Mode = OperatingMode.Manual;
                        }
                        else if (ToSend.Count > 0 && (((string)ToSend.Peek()).Length + 1) < (ControllerBufferSize - BufferState))
                        {
                            string send_line = ((string)ToSend.Dequeue()).Replace(" ", "");

                            writer.Write(send_line);
                            writer.Write('\n');
                            writer.Flush();

                            RecordLog("> " + send_line);

                            RaiseEvent(UpdateStatus, send_line);
                            RaiseEvent(LineSent, send_line);

                            BufferState += send_line.Length + 1;

                            Sent.Enqueue(send_line);
                        }


                        DateTime Now = DateTime.Now;

                        if ((Now - LastStatusPoll).TotalMilliseconds > StatusPollInterval)
                        {
                            writer.Write('?');
                            writer.Flush();
                            LastStatusPoll = Now;
                        }

                        //only update file pos every X ms
                        if (filePosChanged && (Now - LastFilePosUpdate).TotalMilliseconds > 500)
                        {
                            RaiseEvent(FilePositionChanged);
                            LastFilePosUpdate = Now;
                            filePosChanged = false;
                        }

                        Thread.Sleep(WaitTime);
                    }

                    string line = lineTask.Result;

                    RecordLog("< " + line);

                    if (line == "ok")
                    {
                        if (Sent.Count != 0)
                        {
                            BufferState -= ((string)Sent.Dequeue()).Length + 1;
                        }
                        else
                        {
                            MainWindow.Logger.Info("Received OK without anything in the Sent Buffer");
                            BufferState = 0;
                        }
                    }
                    else
                    {
                        if (line.StartsWith("error:"))
                        {
                            if (Sent.Count != 0)
                            {
                                string errorline = (string)Sent.Dequeue();

                                RaiseEvent(ReportError, $"{line}: {errorline}");
                                BufferState -= errorline.Length + 1;
                            }
                            else
                            {
                                if ((DateTime.Now - StartTime).TotalMilliseconds > 200)
                                    RaiseEvent(ReportError, $"Received  without anything in the Sent Buffer");

                                BufferState = 0;
                            }

                            Mode = OperatingMode.Manual;
                        }
                        else if (line.StartsWith("= File.Count || lineNumber < 0)
            {
                RaiseEvent(NonFatalException, "Line Number outside of file length");
                return;
            }

            FilePosition = lineNumber;

            RaiseEvent(FilePositionChanged);
        }

        public void ClearQueue()
        {
            if (Mode != OperatingMode.Manual)
            {
                RaiseEvent(Info, "Not in Manual mode");
                return;
            }

            ToSend.Clear();
        } 

        private static Regex GCodeSplitter = new Regex(@"([GZ])\s*(\-?\d+\.?\d*)", RegexOptions.Compiled);

        /// 
        /// Updates Status info from each line sent
        /// 
        /// 
        private void UpdateStatus(string line)
        {
            if (!Connected)
                return;

            if (line.Contains("$J="))
                return;

            if (line.StartsWith("[TLO:"))
            {
                try
                {
                    CurrentTLO = double.Parse(line.Substring(5, line.Length - 6), Constants.DecimalParseFormat);
                    RaiseEvent(PositionUpdateReceived);
                }
                catch { RaiseEvent(NonFatalException, "Error while Parsing Status Message"); }
                return;
            }

            try
            {
                //we use a Regex here so G91.1 etc don't get recognized as G91
                MatchCollection mc = GCodeSplitter.Matches(line);
                for (int i = 0; i < mc.Count; i++)
                {
                    Match m = mc[i];

                    if (m.Groups[1].Value != "G")
                        continue;

                    double code = double.Parse(m.Groups[2].Value, Constants.DecimalParseFormat);

                    if (code == 17)
                        Plane = ArcPlane.XY;
                    if (code == 18)
                        Plane = ArcPlane.YZ;
                    if (code == 19)
                        Plane = ArcPlane.ZX;

                    if (code == 20)
                        Unit = ParseUnit.Imperial;
                    if (code == 21)
                        Unit = ParseUnit.Metric;

                    if (code == 90)
                        DistanceMode = ParseDistanceMode.Absolute;
                    if (code == 91)
                        DistanceMode = ParseDistanceMode.Incremental;

                    if (code == 49)
                        CurrentTLO = 0;

                    if (code == 43.1)
                    {
                        if (mc.Count > (i + 1))
                        {
                            if (mc[i + 1].Groups[1].Value == "Z")
                            {
                                CurrentTLO = double.Parse(mc[i + 1].Groups[2].Value, Constants.DecimalParseFormat);
                                RaiseEvent(PositionUpdateReceived);
                            }

                            i += 1;
                        }
                    }
                }
            }
            catch { RaiseEvent(NonFatalException, "Error while Parsing Status Message"); }
        }

        private static Regex StatusEx = new Regex(@"(?])", RegexOptions.Compiled);
        /// 
        /// Parses a recevied status report (answer to '?')
        /// 
        private void ParseStatus(string line)
        {
            MatchCollection statusMatch = StatusEx.Matches(line);

            if (statusMatch.Count == 0)
            {
                NonFatalException.Invoke(string.Format("Received Bad Status: '{0}'", line));
                return;
            }

            bool posUpdate = false;
            bool overrideUpdate = false;
            bool pinStateUpdate = false;
            bool resetPins = true;

            foreach (Match m in statusMatch)
            {
                if (m.Index == 1)
                {
                    Status = m.Groups[1].Value;
                    continue;
                }

                if (m.Groups[1].Value == "Ov")
                {
                    try
                    {
                        string[] parts = m.Groups[2].Value.Split(',');
                        FeedOverride = int.Parse(parts[0]);
                        RapidOverride = int.Parse(parts[1]);
                        SpindleOverride = int.Parse(parts[2]);
                        overrideUpdate = true;
                    }
                    catch { NonFatalException.Invoke(string.Format("Received Bad Status: '{0}'", line)); }
                }

                else if (m.Groups[1].Value == "WCO")
                {
                    try
                    {
                        string OffsetString = m.Groups[2].Value;

                        if (Properties.Settings.Default.IgnoreAdditionalAxes)
                        {
                            string[] parts = OffsetString.Split(',');
                            if (parts.Length > 3)
                            {
                                Array.Resize(ref parts, 3);
                                OffsetString = string.Join(",", parts);
                            }
                        }

                        WorkOffset = Vector3.Parse(OffsetString);
                        posUpdate = true;
                    }
                    catch { NonFatalException.Invoke(string.Format("Received Bad Status: '{0}'", line)); }
                }

                else if (SyncBuffer && m.Groups[1].Value == "Bf")
                {
                    try
                    {
                        int availableBytes = int.Parse(m.Groups[2].Value.Split(',')[1]);
                        int used = Properties.Settings.Default.ControllerBufferSize - availableBytes;

                        if (used < 0)
                            used = 0;

                        BufferState = used;
                        RaiseEvent(Info, $"Buffer State Synced ({availableBytes} bytes free)");
                    }
                    catch { NonFatalException.Invoke(string.Format("Received Bad Status: '{0}'", line)); }
                }

                else if (m.Groups[1].Value == "Pn")
                {
                    resetPins = false;

                    string states = m.Groups[2].Value;

                    bool stateX = states.Contains("X");
                    if (stateX != PinStateLimitX)
                        pinStateUpdate = true;
                    PinStateLimitX = stateX;

                    bool stateY = states.Contains("Y");
                    if (stateY != PinStateLimitY)
                        pinStateUpdate = true;
                    PinStateLimitY = stateY;

                    bool stateZ = states.Contains("Z");
                    if (stateZ != PinStateLimitZ)
                        pinStateUpdate = true;
                    PinStateLimitZ = stateZ;

                    bool stateP = states.Contains("P");
                    if (stateP != PinStateProbe)
                        pinStateUpdate = true;
                    PinStateProbe = stateP;
                }

                else if (m.Groups[1].Value == "F")
                {
                    try
                    {
                        FeedRateRealtime = double.Parse(m.Groups[2].Value, Constants.DecimalParseFormat);
                        posUpdate = true;
                    }
                    catch { NonFatalException.Invoke(string.Format("Received Bad Status: '{0}'", line)); }
                }

                else if (m.Groups[1].Value == "FS")
                {
                    try
                    {
                        string[] parts = m.Groups[2].Value.Split(',');
                        FeedRateRealtime = double.Parse(parts[0], Constants.DecimalParseFormat);
                        SpindleSpeedRealtime = double.Parse(parts[1], Constants.DecimalParseFormat);
                        posUpdate = true;
                    }
                    catch { NonFatalException.Invoke(string.Format("Received Bad Status: '{0}'", line)); }
                }
            }

            SyncBuffer = false; //only run this immediately after button press

            //run this later to catch work offset changes before parsing position
            Vector3 NewMachinePosition = MachinePosition;

            foreach (Match m in statusMatch)
            {
                if (m.Groups[1].Value == "MPos" || m.Groups[1].Value == "WPos")
                {
                    try
                    {
                        string PositionString = m.Groups[2].Value;

                        if (Properties.Settings.Default.IgnoreAdditionalAxes)
                        {
                            string[] parts = PositionString.Split(',');
                            if (parts.Length > 3)
                            {
                                Array.Resize(ref parts, 3);
                                PositionString = string.Join(",", parts);
                            }
                        }

                        NewMachinePosition = Vector3.Parse(PositionString);

                        if (m.Groups[1].Value == "WPos")
                            NewMachinePosition += WorkOffset;

                        if (NewMachinePosition != MachinePosition)
                        {
                            posUpdate = true;
                            MachinePosition = NewMachinePosition;
                        }
                    }
                    catch { NonFatalException.Invoke(string.Format("Received Bad Status: '{0}'", line)); }
                }

            }

            if (posUpdate && Connected && PositionUpdateReceived != null)
                PositionUpdateReceived.Invoke();

            if (overrideUpdate && Connected && OverrideChanged != null)
                OverrideChanged.Invoke();

            if (resetPins)  //no pin state received in status -> all zero
            {
                pinStateUpdate = PinStateLimitX | PinStateLimitY | PinStateLimitZ | PinStateProbe;  //was any pin set before

                PinStateLimitX = false;
                PinStateLimitY = false;
                PinStateLimitZ = false;
                PinStateProbe = false;
            }

            if (pinStateUpdate && Connected && PinStateChanged != null)
                PinStateChanged.Invoke();

            if (Connected && StatusReceived != null)
                StatusReceived.Invoke(line);
        }

        private static Regex ProbeEx = new Regex(@"\[PRB:(?'Pos'\-?[0-9\.]*(?:,\-?[0-9\.]*)+):(?'Success'0|1)\]", RegexOptions.Compiled);

        /// 
        /// Parses a recevied probe report
        /// 
        private void ParseProbe(string line)
        {
            if (ProbeFinished == null)
                return;

            Match probeMatch = ProbeEx.Match(line);

            Group pos = probeMatch.Groups["Pos"];
            Group success = probeMatch.Groups["Success"];

            if (!probeMatch.Success || !(pos.Success & success.Success))
            {
                NonFatalException.Invoke($"Received Bad Probe: '{line}'");
                return;
            }

            string PositionString = pos.Value;

            if (Properties.Settings.Default.IgnoreAdditionalAxes)
            {
                string[] parts = PositionString.Split(',');
                if (parts.Length > 3)
                {
                    Array.Resize(ref parts, 3);
                    PositionString = string.Join(",", parts);
                }
            }

            Vector3 ProbePos = Vector3.Parse(PositionString);
            LastProbePosMachine = ProbePos;

            ProbePos -= WorkOffset;

            LastProbePosWork = ProbePos;

            bool ProbeSuccess = success.Value == "1";

            ProbeFinished.Invoke(ProbePos, ProbeSuccess);
        }

        private static Regex StartupRegex = new Regex("([0-9]).([0-9])([a-z])");
        private void ParseStartup(string line)
        {     // TODO Kinda Successfull, still need to find a way to ask for it just after recieving OK message on Connection
            Match m = StartupRegex.Match(line);

            int major, minor;
            char rev;

            if (!m.Success ||
                !int.TryParse(m.Groups[1].Value, out major) ||
                !int.TryParse(m.Groups[2].Value, out minor) ||
                !char.TryParse(m.Groups[3].Value, out rev))
            {
                RaiseEvent(Info, "Could not read GRBL Version.");
                return;
            }
            // TODO Display current GRBL version in the Connection or About box
            Version v = new Version(major, minor, (int)rev);
            GRBL_Version = v.ToString();
            Console.WriteLine("GRBL Version is " + GRBL_Version);
            if (v < Constants.MinimumGrblVersion)
            {
                ReportError("Outdated version of grbl detected!");
                ReportError($"Please upgrade to at least grbl v{Constants.MinimumGrblVersion.Major}.{Constants.MinimumGrblVersion.Minor}{(char)Constants.MinimumGrblVersion.Build}");
            }

        }

        /// 
        /// Reports error. This is there to offload the ExpandError function from the "Real-Time" worker thread to the application thread
        /// also used for alarms
        /// 
        private void ReportError(string error)
        {
            if (NonFatalException != null)
                NonFatalException.Invoke(GrblCodeTranslator.ExpandError(error));
        }

        private void RaiseEvent(Action action, string param)
        {
            if (action == null)
                return;

            Application.Current.Dispatcher.BeginInvoke(action, param);
        }

        private void RaiseEvent(Action action)
        {
            if (action == null)
                return;

            Application.Current.Dispatcher.BeginInvoke(action);
        }
    }
}