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);
}
}
}