diff --git a/SerialMonitor/Functions/BuiltInFunctions.cs b/SerialMonitor/Functions/BuiltInFunctions.cs new file mode 100644 index 0000000..63c1f15 --- /dev/null +++ b/SerialMonitor/Functions/BuiltInFunctions.cs @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------------- +// +// Name: BuiltInFunctions.cs +// Author: Vita Tucek +// Created: 23.5.2024 +// License: MIT +// Description: System functions +// +//--------------------------------------------------------------------------- + +namespace SerialMonitor.Functions +{ + internal class BuiltInFunctions + { + public static readonly string[] Available = [nameof(Crc16)]; + + public static bool IsAvailable(string functionName) + { + return Available.Any(a => a.Equals(functionName, StringComparison.OrdinalIgnoreCase)); + } + + public static IFunction Get(string functionName) + { + var f = Available.First(x => x.Equals(functionName, StringComparison.OrdinalIgnoreCase)); + + if (f.Equals("Crc16")) + return Crc16.Instance; + + throw new NotImplementedException("Function {functionName} is not implemented."); + } + } +} diff --git a/SerialMonitor/Functions/Crc16.cs b/SerialMonitor/Functions/Crc16.cs new file mode 100644 index 0000000..20ea4c7 --- /dev/null +++ b/SerialMonitor/Functions/Crc16.cs @@ -0,0 +1,111 @@ +//--------------------------------------------------------------------------- +// +// Name: Crc16.cs +// Author: Vita Tucek +// Created: 23.5.2024 +// License: MIT +// Description: CRC16 (Modbus) calculation +// +//--------------------------------------------------------------------------- + +namespace SerialMonitor.Functions +{ + public class Crc16 : IFunction + { + private static readonly ushort[] crc_table = { + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 + }; + + public int Size => 2; + + private static IFunction? _instance; + public static IFunction Instance => _instance ?? (_instance = new Crc16()); + + /// + /// Compute over specified length + /// + /// + /// + /// + public static ushort ComputeCrc(byte[] data, int length) + { + int datalen = length > data.Length ? data.Length : length; + ushort temp; + ushort crc = 0xFFFF; + + for (int i = 0; i < datalen; i++) + { + temp = (ushort)((data[i] ^ crc) & 0x00FF); + crc >>= 8; + crc ^= crc_table[temp]; + } + + return crc; + } + + public void Compute(byte[] data, int position) + { + var crc = Crc16.ComputeCrc(data, position); + data[position] = (byte)(crc & 0xFF); + data[position+1] = (byte)(crc >> 8 & 0xFF); + } + + /// + /// Verify data with CRC included + /// + /// + /// + /// + public static bool Verify(byte[] data) + { + return Verify(data, data.Length); + } + + /// + /// Verify data with CRC included + /// + /// + /// + /// + public static bool Verify(byte[] data, int length) + { + if (length < 3) + return false; + ushort computed = ComputeCrc(data, length - 2); + ushort received = (ushort)((ushort)(data[length - 1] << 8) + data[length - 2]); + + return computed == received; + } + } +} diff --git a/SerialMonitor/Functions/IFunction.cs b/SerialMonitor/Functions/IFunction.cs new file mode 100644 index 0000000..b8e93b8 --- /dev/null +++ b/SerialMonitor/Functions/IFunction.cs @@ -0,0 +1,28 @@ +//--------------------------------------------------------------------------- +// +// Name: IFunction.cs +// Author: Vita Tucek +// Created: 23.5.2024 +// License: MIT +// Description: Functions interface +// +//--------------------------------------------------------------------------- + +namespace SerialMonitor.Functions +{ + public interface IFunction + { + /// + /// Data space needed in packet + /// + int Size { get; } + /// + /// Compute function over data in source from 0 to position. Result is put at position. + /// + /// + /// + void Compute(byte[] data, int position); + + static IFunction Instance { get; } + } +} diff --git a/SerialMonitor/HexData.cs b/SerialMonitor/HexData.cs new file mode 100644 index 0000000..9008457 --- /dev/null +++ b/SerialMonitor/HexData.cs @@ -0,0 +1,330 @@ +//--------------------------------------------------------------------------- +// +// Name: HexData.cs +// Author: Vita Tucek +// Created: 23.5.2024 +// License: MIT +// Description: Hexadecimal repeat data definition +// +//--------------------------------------------------------------------------- + +using SerialMonitor.Functions; +using System.Collections; +using System.Globalization; +using System.Linq.Expressions; +using System.Text.RegularExpressions; + +namespace SerialMonitor +{ + internal class HexData + { + private static readonly Regex regWhite = new Regex("\\s+"); + /// + /// Properties + /// + public ushort[]? MyProperty { get; private set; } + /// + /// Variable ID and Index in MyProperty array collection + /// + public Dictionary? Variables { get; private set; } + /// + /// Index in Property array and function ID collection + /// + public Dictionary? Functions { get; private set; } + + private HexData() + { + + } + + /// + /// Create data instance + /// + /// + /// + public static HexData Create(string data) + { + ArgumentNullException.ThrowIfNull(data); + + var hexData = new HexData(); + var trimmed = data.Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase); + + // delimited + if (regWhite.IsMatch(trimmed)) + { + var bytes = regWhite.Split(trimmed); + + var functions = bytes.Select((x, i) => new { Index = i, Item = x }) + .Where(x => x.Item.StartsWith('@')).Select(x => new { x.Index, Item = x.Item[1..] }); + + if (!functions.Any()) + { + hexData.MyProperty = bytes.Select(x => GetSingleByte(x)).ToArray(); + } + else + { + foreach (var f in functions) + { + if (!BuiltInFunctions.IsAvailable(f.Item)) + throw new RepeatFileException($"Function {f.Item} is not supported"); + } + // create functions + hexData.Functions = functions.ToDictionary(x => x.Index, x => BuiltInFunctions.Get(x.Item)); + // alocate properties (depend on function space needed) + hexData.MyProperty = new ushort[bytes.Length - hexData.Functions.Count + hexData.Functions.Sum(x => x.Value.Size)]; + // fill properties + int move = 1; + for (int i = 0; i < bytes.Length; i++) + { + // function exists + if (!hexData.Functions.TryGetValue(i, out var func)) + { + hexData.MyProperty[i] = GetSingleByte(bytes[i]); + move = 1; + } + else + { + hexData.MyProperty[i] = (ushort)0x200u; + if (func.Size > 1) + { + for(int j=1;j new { Index = i, Item = x }) + .Where(x => IsVariable(x.Item)).ToDictionary(x => GetVariableId(x.Item), x => x.Index); + + return hexData; + } + + /// + /// Create data instance from data packet + /// + /// + /// + /// + public static HexData Create(byte[] data, int length) + { + var hexData = new HexData(); + hexData.MyProperty = new ushort[length]; + for (int i = 0; i < length; i++) + { + hexData.MyProperty[i] = data[i]; + } + + return hexData; + } + + /// + /// Variable checker + /// + /// + /// + public static bool IsVariable(ushort data) + { + return ((data & 0x100u) == 0x100u); + } + + /// + /// Return lower byte + /// + /// + /// + public static byte GetVariableId(ushort data) + { + return Convert.ToByte(data & 0xFFu); + } + + + /// + /// Function checker + /// + /// + /// + public static bool IsFunction(ushort data) + { + return ((data & 0x2FFu) == 0x200u); + } + + /// + /// Function checker + /// + /// + /// + public static bool IsFunctionArea(ushort data) + { + return ((data & 0x200u) == 0x200u); + } + + /// + /// Return byte representation. + /// Numeric value is as is. + /// Variable symbol $xx is converted into 0x01xx + /// + /// + /// + private static ushort GetSingleByte(string singlenumber) + { + return Convert.ToUInt16(singlenumber.StartsWith('$') + ? (0x100u + byte.Parse(singlenumber[1..])) + : byte.Parse(singlenumber, NumberStyles.HexNumber)); + } + } + + /// + /// HexData collection + /// + internal class HexDataCollection + { + private readonly Dictionary> _data = new Dictionary>( + new HexDataEqualityComparer()); + + /// + /// Clear collection + /// + public void Clear() + { + _data.Clear(); + } + /// + /// Count + /// + public int Count => _data.Count; + /// + /// Add data/value data into collection + /// + /// + /// + /// + public bool TryAdd(HexData key, HexData value) + { + return _data.TryAdd(key, new Tuple(key, value)); + } + /// + /// Get value by hex data + /// + /// + /// + /// + public bool TryGetValue(HexData key, out Tuple? value) + { + return _data.TryGetValue(key, out value); + } + /// + /// Get value by raw data + /// + /// + /// + /// + /// + public bool TryGetValue(byte[] data, int length, out byte[] value) + { + var hexkey = HexData.Create(data, length); + + if (!_data.TryGetValue(hexkey, out var output)) + { + value = []; + return false; + } + else + { + var hexvalue = output.Item2; + if (hexvalue.MyProperty == null) + { + value = []; + return false; + } + + value = new byte[hexvalue.MyProperty.Length]; + + for (int i = 0; i < hexvalue.MyProperty.Length; i++) + { + if (HexData.IsVariable(hexvalue.MyProperty[i])) + { + // variables not defined + if (output.Item1.Variables == null) + { + value[i] = 0; + continue; + } + // get variable ID from answer + var id = HexData.GetVariableId(hexvalue.MyProperty[i]); + // look for variable in ask + if (!output.Item1.Variables.TryGetValue(id, out var index)) + { + value[i] = 0; + } + else + { + // copy data from source from specified index + value[i] = data[index]; + } + } + else if (hexvalue.Functions != null && HexData.IsFunction(hexvalue.MyProperty[i])) + { + hexvalue.Functions[i].Compute(value, i); + } + else if (HexData.IsFunctionArea(hexvalue.MyProperty[i])) + { + continue; + } + else + { + value[i] = Convert.ToByte(hexvalue.MyProperty[i] & 0xFF); + } + } + } + + return true; + } + } + + internal class HexDataEqualityComparer : IEqualityComparer + { + public bool Equals(HexData? a, HexData? b) + { + if (a?.MyProperty == null || b?.MyProperty == null) + return false; + + if (a.MyProperty.Length != b.MyProperty.Length) + return false; + + if (a.MyProperty?.SequenceEqual(b.MyProperty!) == true) + return true; + + // compare with variables + for (int i = 0; i < a.MyProperty!.Length; i++) + { + // variable or function + if (((a.MyProperty[i] & 0x300u) == 0 + && (b.MyProperty![i] & 0x300u) == 0) + && a.MyProperty[i] != b.MyProperty[i]) + { + return false; + } + } + + return true; + } + + public int GetHashCode(HexData? p) + { + return 0; + } + } +} diff --git a/SerialMonitor/Program.cs b/SerialMonitor/Program.cs index 2fd3be0..4799208 100644 --- a/SerialMonitor/Program.cs +++ b/SerialMonitor/Program.cs @@ -18,18 +18,25 @@ namespace SerialMonitor class Program { static readonly ArgumentCollection arguments = new ArgumentCollection(new string[] { "baudrate", "parity", "databits", "stopbits", - "repeatfile", "logfile", "logincomingonly", "showascii", "notime", "gaptolerance", "continuousmode", "nogui" }); + "repeatfile", "logfile", "logincomingonly", "showascii", "notime", "gaptolerance", "continuousmode", "nogui", "service" }); static readonly string? version = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString(3); static long lastTimeReceved = 0; static bool repeaterEnabled = false; static bool repeaterUseHex = false; - static readonly Dictionary repeaterMap = new Dictionary(); + static readonly Dictionary repeaterStringMap = new Dictionary(); + static readonly HexDataCollection repeaterHexMap = new HexDataCollection(); static bool logfile = false; static bool logincomingonly = false; - static string logFilename = ""; + // default log file name + static readonly string logSystemFilename = logDataFilename = Path.Combine(OperatingSystem.IsLinux() ? "/var/log/serialmonitor" : Directory.GetCurrentDirectory(), "serialmonitor.log"); + static string logDataFilename = $"log_{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.txt"; static int gapTolerance = 0; static bool gapToleranceEnable = false; static bool continuousMode = false; + static bool serviceMode = false; + static FileSystemWatcher? _watcher; + static readonly TraceSource trace = new TraceSource("System"); + static readonly TraceSource traceData = new TraceSource("Data"); /// /// Flag for stop printing communication data. Log into file will continue. /// @@ -58,7 +65,7 @@ class Program static void Main(string[] args) { DateTime lastTry = DateTime.MinValue; - setting.Port = System.OperatingSystem.IsWindows() ? "COM1" : "/dev/ttyS1"; + setting.Port = OperatingSystem.IsWindows() ? "COM1" : "/dev/ttyS1"; if (args.Length > 0) { @@ -90,16 +97,26 @@ static void Main(string[] args) } } + + trace.Switch = new SourceSwitch("SourceSwitch", "All"); + trace.Switch.Level = SourceLevels.All; + trace.Listeners.Clear(); + trace.Listeners.Add(new System.Diagnostics.TextWriterTraceListener(logSystemFilename, "System")); + // parse commandline arguments arguments.Parse(args); - continuousMode = arguments.GetArgument("continuousmode").Enabled || arguments.GetArgument("nogui").Enabled; + continuousMode = arguments.GetArgument("continuousmode").Enabled + || arguments.GetArgument("nogui").Enabled + || arguments.GetArgument("service").Enabled; + + serviceMode = arguments.GetArgument("service").Enabled; if (!continuousMode) UI.Init(); - else + else if (!serviceMode) ConsoleWriteLineNoTrace($"SerialMonitor v.{version}"); - + setting.BaudRate = 9600; setting.Parity = Parity.None; setting.DataBits = 8; @@ -149,7 +166,7 @@ static void Main(string[] args) port.PortName = setting.Port; port.BaudRate = setting.BaudRate; - port.Parity = setting.Parity; + port.Parity = setting.Parity; port.DataBits = setting.DataBits; port.StopBits = setting.StopBits; port.DataReceived += new SerialDataReceivedEventHandler(port_DataReceived); @@ -177,11 +194,11 @@ static void Main(string[] args) { if (arg.Parameter.Length == 0) { - logFilename = "log_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".txt"; - ConsoleWriteLine("Warning: Log file name not specified. Used " + logFilename); + if (!serviceMode) + ConsoleWriteLine(TraceEventType.Warning, "Warning: Log file name not specified. Used " + logDataFilename); } else - logFilename = arg.Parameter; + logDataFilename = arg.Parameter; logfile = true; } @@ -191,9 +208,8 @@ static void Main(string[] args) if (!logfile) { logfile = true; - logFilename = "log_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".txt"; - ConsoleWriteLine("Warning: Parameter logfile not specified. Log enabled to the file: " + logFilename); - logfile = true; + if (!serviceMode) + ConsoleWriteLine(TraceEventType.Warning, "Warning: Parameter logfile not specified. Log enabled to the file: " + logDataFilename); } logincomingonly = true; } @@ -205,44 +221,49 @@ static void Main(string[] args) _ = int.TryParse(arg.Parameter, out gapTolerance); if (gapTolerance == 0) - ConsoleWriteLine("Warning: Parameter gaptolerance has invalid argument. Gap tolerance must be greater than zero."); + ConsoleWriteLine(TraceEventType.Warning, "Warning: Parameter gaptolerance has invalid argument. Gap tolerance must be greater than zero."); else gapToleranceEnable = true; } - if (logfile) { //check path - string? path = System.IO.Path.GetDirectoryName(logFilename); + string? path = Path.GetDirectoryName(logDataFilename); if (path?.Length > 0) { - if (!System.IO.Directory.Exists(path)) + if (!Directory.Exists(path)) try { - System.IO.Directory.CreateDirectory(path); + Directory.CreateDirectory(path); } catch (Exception ex) { - ConsoleWriteLine($"Warning: Cannot create directory {path}. {ex.Message}"); + ConsoleWriteLine(TraceEventType.Warning, $"Warning: Cannot create directory {path}. {ex.Message}"); } } else { - logFilename = System.IO.Directory.GetCurrentDirectory() + "\\" + logFilename; - } + if (OperatingSystem.IsLinux()) + logDataFilename = Path.Combine("/var/log/serialmonitor", logDataFilename); + else + logDataFilename = Path.Combine(Directory.GetCurrentDirectory(), logDataFilename); + } - if (!IsFileNameValid(logFilename)) + if (!IsFileNameValid(logDataFilename)) { - ConsoleWriteLine("\nPress [Enter] to exit"); - Console.ReadLine(); - + if (!serviceMode) + { + ConsoleWriteLine("\nPress [Enter] to exit"); + Console.ReadLine(); + } return; } //assign file to listener - if (Trace.Listeners["Default"] is DefaultTraceListener listener) - listener.LogFileName = logFilename; + traceData.Switch = new SourceSwitch("SourceSwitch", "All"); + traceData.Listeners.Clear(); + traceData.Listeners.Add(new System.Diagnostics.TextWriterTraceListener(logDataFilename, "Data")); } /* @@ -260,15 +281,17 @@ static void Main(string[] args) { if (!IsFileNameValid(repeatfile.Parameter)) { - ConsoleWriteLine("\nPress [Enter] to exit"); - Console.ReadLine(); - + if (!serviceMode) + { + ConsoleWriteLine("\nPress [Enter] to exit"); + Console.ReadLine(); + } return; } PrepareRepeatFile(repeatfile.Parameter); } - - ConsoleWriteLine($"Opening port {port.PortName}: baudrate={port.BaudRate}b/s, parity={port.Parity}, databits={port.DataBits}, stopbits={port.StopBits}"); + + ConsoleWriteLine(TraceEventType.Information, $"Opening port {port.PortName}: baudrate={port.BaudRate}b/s, parity={port.Parity}, databits={port.DataBits}, stopbits={port.StopBits}"); bool exit = false; @@ -287,6 +310,7 @@ static void Main(string[] args) UI.FileHistory.AddRange(fileList); fileList = null; } + trace.Flush(); if (continuousMode) { @@ -301,20 +325,21 @@ static void Main(string[] args) } } else - { + { UI.PrintAsHexToLogView = !setting.ShowAscii; UI.ActionHelp = () => { PrintHelp(); }; UI.ActionPrint = (print) => { pausePrint = !print; }; UI.ActionPrintAsHex = (hex) => { setting.ShowAscii = !hex; }; UI.ActionOpenClose = (close) => { pauseConnection = close; if (close) port.Close(); UI.SetPortStatus(port); UI.SetPinStatus(port); }; - UI.ActionSend = (data) => { UserDataSend(port,data); }; + UI.ActionSend = (data) => { UserDataSend(port, data); }; UI.ActionSendFile = (file) => { UserDataSendFile(port, file); }; UI.ActionRts = () => { port.RtsEnable = !port.RtsEnable; UI.SetPortStatus(port); UI.SetPinStatus(port); }; UI.ActionDtr = () => { port.DtrEnable = !port.DtrEnable; UI.SetPortStatus(port); UI.SetPinStatus(port); }; UI.ActionCommand = (text) => { ProcessCommand(text); }; UI.ActionSettingLoad = () => { return setting; }; - UI.ActionSettingSave = (setting) => { - if(port.PortName != setting.Port || port.BaudRate != setting.BaudRate) + UI.ActionSettingSave = (setting) => + { + if (port.PortName != setting.Port || port.BaudRate != setting.BaudRate) { if (port.IsOpen) { @@ -330,9 +355,9 @@ static void Main(string[] args) port.PortName = setting.Port; port.BaudRate = setting.BaudRate; } - UI.SetPortStatus(port); + UI.SetPortStatus(port); } - + return Config.SaveSetting(setting.Port, setting.BaudRate, setting.ShowTime, setting.ShowTimeGap, setting.ShowSentData, setting.ShowAscii); }; @@ -356,6 +381,10 @@ static void Main(string[] args) Exit(); } + + _watcher?.Dispose(); + trace.Flush(); + trace.Close(); } /// @@ -386,7 +415,7 @@ private static void ProcessCommand(string? text) } /// - /// Send tada typed by user + /// Send data typed by user /// /// /// @@ -402,12 +431,12 @@ private static bool UserDataSend(SerialPort port, string? line) bool hex = false; byte[] data; - if (line.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)) + if (line.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = true; if (hex) { - string prepared = line.Replace("0x", ""); + string prepared = line.Replace("0x", "", StringComparison.OrdinalIgnoreCase); Regex reg = new Regex("^([0-9A-Fa-f]{1,2}\\s*)+$"); if (!reg.IsMatch(prepared)) @@ -544,6 +573,10 @@ private static void PortConnectInfinite(SerialPort port) ConsoleWrite(new string(' ', waitText.Length + 5)); ConsoleCursorLeftReset(); } + else + { + ConsoleWriteLine($" Port {port.PortName} opened"); + } // TODO: /* @@ -596,7 +629,7 @@ private static void PortClose(SerialPort port) if (port.IsOpen) port.Close(); } - + /// /// Validating file name(path) /// @@ -604,7 +637,7 @@ private static void PortClose(SerialPort port) /// private static bool IsFileNameValid(string filePath) { - foreach (char c in System.IO.Path.GetInvalidPathChars()) + foreach (char c in Path.GetInvalidPathChars()) { if (filePath.Contains(c.ToString())) { @@ -614,9 +647,9 @@ private static bool IsFileNameValid(string filePath) } } - foreach (char c in System.IO.Path.GetInvalidFileNameChars()) + foreach (char c in Path.GetInvalidFileNameChars()) { - if (System.IO.Path.GetFileName(filePath).Contains(c.ToString())) + if (Path.GetFileName(filePath).Contains(c.ToString())) { ConsoleWriteError($"File name {filePath} contains invalid character [{c}]. Enter right file name."); @@ -627,6 +660,29 @@ private static bool IsFileNameValid(string filePath) return true; } + /// + /// Event of changed repeat file + /// + /// + /// + /// + private static void OnRepeatFileChanged(object source, FileSystemEventArgs e) + { + try + { + _watcher!.EnableRaisingEvents = false; + + ConsoleWriteLine($"File {e.FullPath} was changed. Repeat file will be read now in a while..."); + Thread.Sleep(500); + PrepareRepeatFile(e.FullPath); + } + + finally + { + _watcher!.EnableRaisingEvents = true; + } + } + /// /// Procedure to read data from repeat file /// @@ -639,99 +695,132 @@ private static void PrepareRepeatFile(string fileName) { try { + if (_watcher == null) + { + var directory = Path.GetDirectoryName(fileName); + if (string.IsNullOrEmpty(directory)) + directory = Directory.GetCurrentDirectory(); + var fileNameFiltered = Path.GetFileName(fileName); + _watcher = new FileSystemWatcher(directory, fileNameFiltered); + _watcher.NotifyFilter = NotifyFilters.LastWrite; + _watcher.Changed += new FileSystemEventHandler(OnRepeatFileChanged); + _watcher.EnableRaisingEvents = true; + } string[] lines = File.ReadAllLines(fileName); if (lines.Length == 0) - ConsoleWriteError($"Zero lines in file {fileName}"); + { + ConsoleWriteError($"No line in file {fileName}"); + return; + } ConsoleWriteLine($"File {fileName} opened and {lines.Length} lines has been read"); - repeaterMap.Clear(); + repeaterStringMap.Clear(); + repeaterHexMap.Clear(); + + // check format file + // first line without comment + string? startLine = lines.FirstOrDefault(x => !x.Trim().StartsWith('#')); + if (startLine == null) + { + ConsoleWriteError($"No line without comment ('#') in file {fileName}"); + return; + } - //check format file - string startLine = lines[0]; int linesWithData = 0; - string ask = ""; - Regex reg = new Regex("^(0x[0-9A-Fa-f]{1,2}\\s*)+$"); + Regex reg = new Regex(@"^(?!\s*$)(?:(0x[0-9A-Fa-f]{1,2})*|(\$\d+)*|(\@.+)*| )+$"); // match hex string if (reg.IsMatch(startLine)) { ConsoleWriteLine("First line corresponds hex format. File will be read and packets compared as HEX."); - Regex regWhite = new Regex("\\s+"); - + HexData ask = HexData.Create(startLine); + ++linesWithData; //check whole file - for (int i = 0; i < lines.Length; i++) + for (int i = 1; i < lines.Length; i++) { - if (lines[i].Trim().Length > 0) + var line = lines[i].Trim(); + // empty or commented line + if (line.Length == 0 || line.StartsWith('#')) + continue; + + if (reg.IsMatch(line)) { - if (reg.IsMatch(lines[i])) - { - if (++linesWithData % 2 == 1) - ask = regWhite.Replace(lines[i].Replace("0x", ""), ""); - else - repeaterMap.Add(ask, regWhite.Replace(lines[i].Replace("0x", ""), "")); - } + var data = HexData.Create(line); + + if (++linesWithData % 2 == 1) + ask = data; else - { - throw new RepeatFileException("Line {0} not coresponds to hex format.", i); - } + repeaterHexMap.TryAdd(ask, data); + } + else + { + throw new RepeatFileException("Line {0} not coresponds to hex format.", i); } } repeaterUseHex = true; + ConsoleWriteLine($"{repeaterHexMap.Count} pairs ask/answer ready"); } else { reg = new Regex("^([0-9A-Fa-f])+$"); // match hex string if (reg.IsMatch(startLine)) - { + { ConsoleWriteLine("First line corresponds hex format. File will be read and packets compared as HEX."); - + HexData ask = HexData.Create(startLine); //check whole file for (int i = 0; i < lines.Length; i++) { - if (lines[i].Trim().Length > 0) + var line = lines[i].Trim(); + // empty or commented line + if (line.Length == 0 || line.StartsWith('#')) + continue; + + if (line.Length % 2 == 1) + { + throw new RepeatFileException("Line {0} has odd number of characters.", i); + } + + if (reg.IsMatch(line)) { - if (lines[i].Length % 2 == 1) - { - throw new RepeatFileException("Line {0} has odd number of characters.", i); - } - - if (reg.IsMatch(lines[i])) - { - if (++linesWithData % 2 == 1) - ask = lines[i]; - else - repeaterMap.Add(ask, lines[i]); - } + var data = HexData.Create(line); + if (++linesWithData % 2 == 1) + ask = data; else - { - throw new RepeatFileException("Line {0} not coresponds to hex format.", i); - } + repeaterHexMap.TryAdd(ask, data); + } + else + { + throw new RepeatFileException("Line {0} not coresponds to hex format.", i); } } repeaterUseHex = true; + ConsoleWriteLine($"{repeaterHexMap.Count} pairs ask/answer ready"); } else { // non hex string ConsoleWriteLine("First line not corresponds hex format. File will be read and packets compared as ASCII."); - + string ask = string.Empty; //check whole file for (int i = 0; i < lines.Length; i++) { - if (lines[i].Trim().Length > 0) - { - if (++linesWithData % 2 == 1) - ask = lines[i]; - else - repeaterMap.Add(ask, lines[i]); - } + var line = lines[i].Trim(); + // empty or commented line + if (line.Length == 0 || line.StartsWith('#')) + continue; + if (++linesWithData % 2 == 1) + ask = line; + else + repeaterStringMap.TryAdd(ask, line); } + + ConsoleWriteLine($"{repeaterStringMap.Count} pairs ask/answer ready"); } } @@ -739,13 +828,16 @@ private static void PrepareRepeatFile(string fileName) ConsoleWriteError($"Odd number of lines in file {fileName} with code. One line ask, one line answer."); repeaterEnabled = true; - - ConsoleWriteLine($"{repeaterMap.Count} pairs ask/answer ready"); + } + catch (FormatException ex) + { + ConsoleWriteError($"Format mismatch in file {fileName}"); + ConsoleWriteError(ex.Message); } catch (Exception ex) { ConsoleWriteError($"Cannot read file {fileName}"); - ConsoleWriteError(ex.ToString()); + ConsoleWriteError(ex.Message); } } } @@ -779,7 +871,7 @@ static void port_ErrorReceived(object sender, SerialErrorReceivedEventArgs e) /// /// static void port_DataReceived(object sender, SerialDataReceivedEventArgs e) - { + { SerialPort port = ((SerialPort)sender); int byteCount; int cycle = 0; @@ -795,7 +887,7 @@ static void port_DataReceived(object sender, SerialDataReceivedEventArgs e) if (incoming.Length < byteCount) incoming = new byte[byteCount]; - + port.Read(incoming, 0, byteCount); } catch (Exception ex) @@ -807,7 +899,7 @@ static void port_DataReceived(object sender, SerialDataReceivedEventArgs e) TimeSpan time = DateTime.Now.TimeOfDay; bool applyGapTolerance = false; - //print time since last receive only if not disabled + // print time since last receive only if not disabled if (lastTimeReceved > 0) { double sinceLastReceive = ((double)(time.Ticks - lastTimeReceved) / 10000); @@ -817,20 +909,20 @@ static void port_DataReceived(object sender, SerialDataReceivedEventArgs e) ConsoleWriteCommunication(ConsoleColor.Magenta, "\n+" + sinceLastReceive.ToString("F3") + " ms"); } - //Write to output + // Write to output string line = ""; if (setting.ShowAscii) { if (!setting.ShowTime || applyGapTolerance) - line = ASCIIEncoding.ASCII.GetString(incoming,0,byteCount); + line = ASCIIEncoding.ASCII.GetString(incoming, 0, byteCount); else line = time.ToString() + " " + Encoding.ASCII.GetString(incoming, 0, byteCount); } else { if (!setting.ShowTime || applyGapTolerance) - line = string.Join(" ", incoming.Take(byteCount).Select(x=> $"0x{x:X2}")); + line = string.Join(" ", incoming.Take(byteCount).Select(x => $"0x{x:X2}")); else line = time.ToString() + " " + string.Join(" ", incoming.Take(byteCount).Select(x => $"0x{x:X2}")); } @@ -874,17 +966,11 @@ static void port_DataReceived(object sender, SerialDataReceivedEventArgs e) { if (repeaterUseHex) { - string ask = string.Join("", incoming.Take(byteCount).Select(x => x.ToString("X2"))); + //HexData ask = string.Join("", incoming.Take(byteCount).Select(x => x.ToString("X2"))); - if (repeaterMap.ContainsKey(ask)) + if (repeaterHexMap.TryGetValue(incoming, byteCount, out var answer)) { - string answer = repeaterMap[ask]; - byte[] data = new byte[answer.Length / 2]; - - for (int i = 0; i < data.Length; i++) - { - data[i] = Convert.ToByte(answer.Substring(2 * i, 2), 16); - } + var data = answer; if (!setting.ShowTime) ConsoleWriteCommunication(ConsoleColor.Green, string.Join("\n ", Array.ConvertAll(data, x => $"0x{x:X2}"))); @@ -895,16 +981,16 @@ static void port_DataReceived(object sender, SerialDataReceivedEventArgs e) } else { - ConsoleWriteLine("Repeater: Unknown ask"); + ConsoleWriteLineNoTrace("\nRepeater: Unknown ask"); } } else { - string ask = ASCIIEncoding.ASCII.GetString(incoming,0,byteCount); + string ask = ASCIIEncoding.ASCII.GetString(incoming, 0, byteCount); - if (repeaterMap.ContainsKey(ask)) + if (repeaterStringMap.ContainsKey(ask)) { - string answer = repeaterMap[ask]; + string answer = repeaterStringMap[ask]; if (!setting.ShowTime) ConsoleWriteCommunication(ConsoleColor.Green, "\n" + answer); @@ -915,7 +1001,7 @@ static void port_DataReceived(object sender, SerialDataReceivedEventArgs e) } else { - ConsoleWriteLine("Repeater: Unknown ask"); + ConsoleWriteLineNoTrace("Repeater: Unknown ask"); } } @@ -928,7 +1014,7 @@ static void port_DataReceived(object sender, SerialDataReceivedEventArgs e) /// private static void ConsoleWriteError(string text) { - ConsoleWriteLine(ConsoleColor.Red, text); + ConsoleWriteLine(TraceEventType.Error, text); } /// @@ -939,11 +1025,8 @@ private static void ConsoleWrite(string message) { if (!continuousMode) UI.Write(message); - else + else if (!serviceMode) Console.Write(message); - - if (logfile && !logincomingonly) - Trace.Write(message); } /// @@ -956,15 +1039,16 @@ private static void ConsoleWriteLine(string message) { UI.WriteLine(message); } - else + else if (!serviceMode) { if (Console.CursorLeft > 0) Console.WriteLine(""); Console.WriteLine(message); } - - if (logfile && !logincomingonly) - Trace.WriteLine(string.Format(message)); + else + { + Trace(TraceEventType.Information, message); + } } /// @@ -972,21 +1056,30 @@ private static void ConsoleWriteLine(string message) /// /// /// - private static void ConsoleWriteLine(ConsoleColor color, string message) + private static void ConsoleWriteLine(TraceEventType eventType, string message) { if (!continuousMode) { - UI.WriteLine(message, color); + UI.WriteLine(message, + eventType == TraceEventType.Critical ? ConsoleColor.DarkRed + : eventType == TraceEventType.Error ? ConsoleColor.Red + : eventType == TraceEventType.Warning ? ConsoleColor.Yellow + : ConsoleColor.White + ); } - else + else if (!serviceMode || eventType <= TraceEventType.Information) { - Console.ForegroundColor = color; + Console.ForegroundColor = eventType == TraceEventType.Critical ? ConsoleColor.DarkRed + : eventType == TraceEventType.Error ? ConsoleColor.Red + : eventType == TraceEventType.Warning ? ConsoleColor.Yellow + : ConsoleColor.White; Console.WriteLine(message); Console.ResetColor(); } - - if (logfile && !logincomingonly) - Trace.WriteLine(string.Format(message)); + else + { + Trace(eventType, message); + } } /// @@ -997,7 +1090,7 @@ private static void ConsoleWriteLineNoTrace(string message) { if (!continuousMode) UI.WriteLine(message); - else + else if (!serviceMode) Console.WriteLine(message); } @@ -1012,7 +1105,7 @@ private static void ConsoleWriteLineNoTrace(ConsoleColor color, string message) { UI.WriteLine(message, color); } - else + else if (!serviceMode) { Console.ForegroundColor = color; Console.WriteLine(message); @@ -1033,7 +1126,7 @@ private static void ConsoleWriteCommunication(ConsoleColor color, string message { UI.Write(message, color); } - else + else if (!serviceMode) { Console.ForegroundColor = color; ConsoleWrite(message); @@ -1042,7 +1135,11 @@ private static void ConsoleWriteCommunication(ConsoleColor color, string message } if (logfile) - Trace.Write(string.Format(message)); + { + // log all received data (yellov color) or others if enabled + if (!logincomingonly || color == ConsoleColor.Yellow) + TraceData(string.Format(message)); + } } /// @@ -1052,9 +1149,13 @@ private static void ConsoleWriteCommunication(ConsoleColor color, string message private static void ConsoleCursorLeft(int moveBy) { if (continuousMode) - Console.CursorLeft += moveBy; - //else - // Cinout.CursorLeft(moveBy); + { + var newposition = Console.CursorLeft + moveBy; + if (newposition > 0 && newposition < Console.BufferWidth) + { + Console.CursorLeft += moveBy; + } + } } /// @@ -1064,8 +1165,6 @@ private static void ConsoleCursorLeftReset() { if (continuousMode) Console.CursorLeft = 0; - //else - // Cinout.CursorLeftReset(); } /// @@ -1087,7 +1186,8 @@ private static void PrintHelp() ConsoleWriteLineNoTrace("-showascii: communication would be show in ASCII format (otherwise HEX is used)"); ConsoleWriteLineNoTrace("-notime: time information about incoming data would not be printed"); ConsoleWriteLineNoTrace("-gaptolerance {{time gap in ms}}: messages received within specified time gap will be printed together"); - ConsoleWriteLineNoTrace("-nogui: start program in normal console mode (scrolling list). Not with primitive text GUI"); + ConsoleWriteLineNoTrace("-nogui: start program in normal console mode (scrolling list). Not with text GUI"); + ConsoleWriteLineNoTrace("-service: start program in normal console mode with minimal verbose"); ConsoleWriteLineNoTrace(""); ConsoleWriteLineNoTrace("Example: serialmonitor COM1"); @@ -1100,18 +1200,19 @@ private static void PrintHelp() ConsoleWriteLineNoTrace("F2: setup program"); ConsoleWriteLineNoTrace("F3: toggle between data print format (HEX / ASCII)"); ConsoleWriteLineNoTrace("F4: pause/resume connection to serial port"); - ConsoleWriteLineNoTrace("F5: send specified data (in HEX format if data start with 0x otherwise ASCII is send)"); - ConsoleWriteLineNoTrace("F6: send specified file)"); + ConsoleWriteLineNoTrace("F5: send a data (in HEX format if data start with 0x otherwise ASCII is send)"); + ConsoleWriteLineNoTrace("F6: send a file"); - ConsoleWriteLineNoTrace("F10: program exit"); - ConsoleWriteLineNoTrace("F11: toggle RTS pin"); - ConsoleWriteLineNoTrace("F12: toggle DTR pin"); + ConsoleWriteLineNoTrace("^Q: program exit"); + ConsoleWriteLineNoTrace("F11 or ^1: toggle RTS pin"); + ConsoleWriteLineNoTrace("F12 or ^2: toggle DTR pin"); ConsoleWriteLineNoTrace("^P: pause / resume print on screen"); ConsoleWriteLineNoTrace(""); ConsoleWriteLineNoTrace("In program commands:"); ConsoleWriteLineNoTrace("help: print help"); ConsoleWriteLineNoTrace("send : send message"); + ConsoleWriteLineNoTrace("exit: exit the program"); if (continuousMode) { @@ -1140,7 +1241,39 @@ private static void Exit() Config.SaveHistory(UI.CommandHistory); Config.SaveFileList(UI.FileHistory); + trace.Close(); UI.Exit(); } + + /// + /// Log system message + /// + /// + /// + private static void Trace(TraceEventType eventType, string message) + { + if (trace.Switch.ShouldTrace(eventType)) + { + string tracemessage = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.FFF")}\t[{eventType}]\t{string.Format(message)}"; + foreach (TraceListener listener in trace.Listeners) + { + listener.WriteLine(tracemessage); + listener.Flush(); + } + } + } + + /// + /// Log data message + /// + /// + private static void TraceData(string message) + { + foreach (TraceListener listener in traceData.Listeners) + { + listener.WriteLine(message); + listener.Flush(); + } + } } } diff --git a/SerialMonitor/Properties/PublishProfiles/Linux64.pubxml b/SerialMonitor/Properties/PublishProfiles/Linux64.pubxml new file mode 100644 index 0000000..2c7ef13 --- /dev/null +++ b/SerialMonitor/Properties/PublishProfiles/Linux64.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + bin\Release\net8.0\publish\linux-x64\ + FileSystem + net8.0 + linux-x64 + true + false + + \ No newline at end of file diff --git a/SerialMonitor/Properties/PublishProfiles/LinuxArm.pubxml b/SerialMonitor/Properties/PublishProfiles/LinuxArm.pubxml new file mode 100644 index 0000000..f0ebb30 --- /dev/null +++ b/SerialMonitor/Properties/PublishProfiles/LinuxArm.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + bin\Release\net8.0\publish\linux-arm\ + FileSystem + net8.0 + linux-arm + true + false + false + + \ No newline at end of file diff --git a/SerialMonitor/Properties/PublishProfiles/LinuxArm64.pubxml b/SerialMonitor/Properties/PublishProfiles/LinuxArm64.pubxml new file mode 100644 index 0000000..ac82eb9 --- /dev/null +++ b/SerialMonitor/Properties/PublishProfiles/LinuxArm64.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + bin\Release\net8.0\publish\linux-arm64\ + FileSystem + net8.0 + linux-arm64 + true + false + false + + \ No newline at end of file diff --git a/SerialMonitor/Properties/PublishProfiles/Win64.pubxml b/SerialMonitor/Properties/PublishProfiles/Win64.pubxml new file mode 100644 index 0000000..a2b937a --- /dev/null +++ b/SerialMonitor/Properties/PublishProfiles/Win64.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + bin\Release\net6.0\publish\win-x64\ + FileSystem + net8.0 + win-x64 + false + false + + diff --git a/SerialMonitor/Resources/changelog.Debian b/SerialMonitor/Resources/changelog.Debian new file mode 100644 index 0000000..706b711 --- /dev/null +++ b/SerialMonitor/Resources/changelog.Debian @@ -0,0 +1,6 @@ +serialmonitor (2.1.0-1) stretch; urgency=medium + + * Possibility to use variables in repeat file + * Fix loading repeat file + + -- VitaTucek Mon, 27 May 2024 12:00:00 +0200 \ No newline at end of file diff --git a/SerialMonitor/Resources/control b/SerialMonitor/Resources/control new file mode 100644 index 0000000..8ffed3f --- /dev/null +++ b/SerialMonitor/Resources/control @@ -0,0 +1,10 @@ +Package: serialmonitor +Version: 2.1.0-1 +Section: utils +Priority: standard +Homepage: https://github.com/docbender/SerialMonitor +Maintainer: Vita Tucek +Description: Utility for communication over serial port + This utility could be use for monitor communication + at the serial port. It allow send user data, files and also + act as serial device emulator. diff --git a/SerialMonitor/Resources/copyright b/SerialMonitor/Resources/copyright new file mode 100644 index 0000000..4377281 --- /dev/null +++ b/SerialMonitor/Resources/copyright @@ -0,0 +1,4 @@ +serialmonitor + +Copyright: 2024 Vita Tucek + diff --git a/SerialMonitor/Resources/postinst b/SerialMonitor/Resources/postinst new file mode 100644 index 0000000..5d11904 --- /dev/null +++ b/SerialMonitor/Resources/postinst @@ -0,0 +1,3 @@ +#!/bin/bash + +ln -s /usr/share/serialmonitor/SerialMonitor /usr/bin/serialmonitor diff --git a/SerialMonitor/Resources/postinst.service b/SerialMonitor/Resources/postinst.service new file mode 100644 index 0000000..e342576 --- /dev/null +++ b/SerialMonitor/Resources/postinst.service @@ -0,0 +1,5 @@ +#!/bin/bash + +ln -s /usr/share/serialmonitor/SerialMonitor /usr/bin/serialmonitor +systemctl enable serialmonitor + diff --git a/SerialMonitor/Resources/postrm b/SerialMonitor/Resources/postrm new file mode 100644 index 0000000..05a7907 --- /dev/null +++ b/SerialMonitor/Resources/postrm @@ -0,0 +1,2 @@ +#!/bin/bash + diff --git a/SerialMonitor/Resources/prerm b/SerialMonitor/Resources/prerm new file mode 100644 index 0000000..85e3e26 --- /dev/null +++ b/SerialMonitor/Resources/prerm @@ -0,0 +1,6 @@ +#!/bin/bash + +CMD=$1 +if [ $CMD=="remove" ] || [ $CMD=="upgrade" ]; then + rm /usr/bin/serialmonitor +fi diff --git a/SerialMonitor/Resources/prerm.service b/SerialMonitor/Resources/prerm.service new file mode 100644 index 0000000..e163f2c --- /dev/null +++ b/SerialMonitor/Resources/prerm.service @@ -0,0 +1,7 @@ +#!/bin/bash + +CMD=$1 +if [ $CMD=="remove" ] || [ $CMD=="upgrade" ]; then + systemctl stop serialmonitor + rm /usr/bin/serialmonitor +fi diff --git a/SerialMonitor/SerialMonitor.csproj b/SerialMonitor/SerialMonitor.csproj index a911b08..1d19cc5 100644 --- a/SerialMonitor/SerialMonitor.csproj +++ b/SerialMonitor/SerialMonitor.csproj @@ -2,20 +2,106 @@ Exe - net6.0 + net8.0 enable enable - 2.0.0.0 - 2.0.0.0 - 2.0.0.0.myversion + 2.1.0.0 + 2.1.0.24173 + 2.0.0.0.myversion + $(AssemblyName) + none + True - - + + portable + + + + portable + - - + + + + + + + armhf + + + + + arm64 + + + + + amd64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SerialMonitor/UI.cs b/SerialMonitor/UI.cs index 630205c..8aeae61 100644 --- a/SerialMonitor/UI.cs +++ b/SerialMonitor/UI.cs @@ -79,7 +79,7 @@ public static void Init() }; // hotkeys - Application.QuitKey = Key.F10; + Application.QuitKey = Key.CtrlMask | Key.q; win.KeyUp += (e) => { e.Handled = ProcessHotKey(e.KeyEvent); @@ -208,7 +208,7 @@ public static void Shutdown() } private static bool ProcessHotKey(KeyEvent keyEvent) - { + { if (keyEvent.Key == Key.F1) { ActionHelp?.Invoke(); @@ -413,11 +413,11 @@ private static bool ProcessHotKey(KeyEvent keyEvent) // Send file data ActionSendFile?.Invoke(FileHistory[fileList.SelectedItem]); } - else if (keyEvent.Key == Key.F11) + else if (keyEvent.Key == Key.F11 || keyEvent.Key == (Key.D1 | Key.CtrlMask)) { ActionRts?.Invoke(); } - else if (keyEvent.Key == Key.F12) + else if (keyEvent.Key == Key.F12 || keyEvent.Key == (Key.D2 | Key.CtrlMask)) { ActionDtr?.Invoke(); } @@ -437,7 +437,7 @@ private static bool ProcessHotKey(KeyEvent keyEvent) private static void SetBottomMenuText() { - menu.Text = $" F1 Help | F2 Setup | F3 {(!PrintAsHexToLogView ? "Hex " : "Text")} | F4 {(!RequestPortClose ? "Close" : "Open ")} | F5 Send | F6 SendFile | F10 Exit | F11 RTS | F12 DTR | ^P {(!PrintToLogView ? "Print" : "Pause")}"; + menu.Text = $" F1 Help | F2 Setup | F3 {(!PrintAsHexToLogView ? "Hex " : "Text")} | F4 {(!RequestPortClose ? "Close" : "Open ")} | F5 Send | F6 SendFile | F11 RTS | F12 DTR | ^P {(!PrintToLogView ? "Print" : "Pause")} | ^Q Exit"; } public static void Run(Func action)