diff --git a/App.xaml.cs b/App.xaml.cs index de2eeb3..81660d8 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,5 +1,5 @@ -using System.Configuration; -using System.Data; +using System.Diagnostics; +using System.IO; using System.Windows; namespace WpfMaiTouchEmulator; @@ -8,5 +8,38 @@ namespace WpfMaiTouchEmulator; /// public partial class App : Application { + public App() + { + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + DispatcherUnhandledException += App_DispatcherUnhandledException; + Logger.CleanupOldLogFiles(); + } + + private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) + { + CreateDump(e.Exception); + e.Handled = true; + } + + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + CreateDump(e.ExceptionObject as Exception); + } + + private void CreateDump(Exception exception) + { + var dumpFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "CrashDump.dmp"); + + using (var fs = new FileStream(dumpFilePath, FileMode.Create)) + { + var process = Process.GetCurrentProcess(); + DumpCreator.MiniDumpWriteDump(process.Handle, (uint)process.Id, fs.SafeFileHandle, DumpCreator.Typ.MiniDumpNormal, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + } + + Logger.Fatal("App encountered a fatal exception", exception); + MessageBox.Show($"A uncaught exception was thrown: {exception.Message}. \n\n {exception.StackTrace}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + Current.Shutdown(1); + } + } diff --git a/DumpCreator.cs b/DumpCreator.cs new file mode 100644 index 0000000..33bef9c --- /dev/null +++ b/DumpCreator.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.InteropServices; +using System.IO; +using System.Diagnostics; + +class DumpCreator +{ + [Flags] + public enum Typ : uint + { + // Add the MiniDump flags you need, for example: + MiniDumpNormal = 0x00000000, + MiniDumpWithDataSegs = 0x00000001, + // etc. + } + + [DllImport("DbgHelp.dll")] + public static extern bool MiniDumpWriteDump(IntPtr hProcess, uint ProcessId, SafeHandle hFile, Typ DumpType, + IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam); +} diff --git a/Logger.cs b/Logger.cs new file mode 100644 index 0000000..26dfb23 --- /dev/null +++ b/Logger.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Text; + +public static class Logger +{ + private static readonly object lockObj = new(); + private static string? logFilePath; + + private static string GetLogFilePath() + { + if (logFilePath == null) + { + var fileName = $"app_{DateTime.Now:yyyy-MM-dd}.log"; + logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + } + return logFilePath; + } + + + public static void CleanupOldLogFiles() + { + var directory = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); + var oldFiles = directory.GetFiles("app_*.log") + .Where(f => f.CreationTime < DateTime.Now.AddDays(-7)) + .ToList(); + + foreach (var file in oldFiles) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + Error("Failed to delete log file", ex); + } + } + } + + + public static void Info(string message) + { + Log("INFO", message); + } + + public static void Warn(string message) + { + Log("WARN", message); + } + + public static void Error(string message, Exception? ex = null) + { + var logMessage = new StringBuilder(message); + if (ex != null) + { + logMessage.AppendLine(); // Ensure the exception starts on a new line + logMessage.AppendLine($"Exception: {ex.Message}"); + logMessage.AppendLine($"StackTrace: {ex.StackTrace}"); + } + + Log("ERROR", logMessage.ToString()); + } + + public static void Fatal(string message, Exception? ex = null) + { + var logMessage = new StringBuilder(message); + if (ex != null) + { + logMessage.AppendLine(); // Ensure the exception starts on a new line + logMessage.AppendLine($"Exception: {ex.Message}"); + logMessage.AppendLine($"StackTrace: {ex.StackTrace}"); + } + Log("FATAL", logMessage.ToString()); + } + + private static void Log(string level, string message) + { + try + { + lock (lockObj) + { + using var sw = new StreamWriter(GetLogFilePath(), true, Encoding.UTF8); + sw.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {message}"); + } + } + catch + { + } + } +} diff --git a/MaiTouchComConnector.cs b/MaiTouchComConnector.cs index c72348b..649e038 100644 --- a/MaiTouchComConnector.cs +++ b/MaiTouchComConnector.cs @@ -2,54 +2,52 @@ using System.Windows; namespace WpfMaiTouchEmulator; -internal class MaiTouchComConnector +internal class MaiTouchComConnector(MaiTouchSensorButtonStateManager buttonState) { private static SerialPort? serialPort; private bool isActiveMode; private bool _connected; private bool _shouldReconnect = true; - private readonly MaiTouchSensorButtonStateManager _buttonState; + private readonly MaiTouchSensorButtonStateManager _buttonState = buttonState; - public Action OnConnectStatusChange + public Action? OnConnectStatusChange { get; internal set; } - public Action OnConnectError + public Action? OnConnectError { get; internal set; } - public Action OnDataSent + public Action? OnDataSent { get; internal set; } - public Action OnDataRecieved + public Action? OnDataRecieved { get; internal set; } - public MaiTouchComConnector(MaiTouchSensorButtonStateManager buttonState) - { - _buttonState = buttonState; - } - public async Task StartTouchSensorPolling() { if (!_connected && _shouldReconnect) { + Logger.Info("Trying to connect to COM port..."); var virtualPort = "COM23"; // Adjust as needed try { - OnConnectStatusChange("Conecting..."); - serialPort = new SerialPort(virtualPort, 9600, Parity.None, 8, StopBits.One); - serialPort.WriteTimeout = 100; + OnConnectStatusChange?.Invoke("Conecting..."); + serialPort = new SerialPort(virtualPort, 9600, Parity.None, 8, StopBits.One) + { + WriteTimeout = 100 + }; serialPort.DataReceived += SerialPort_DataReceived; serialPort.Open(); - Console.WriteLine("Serial port opened successfully."); - OnConnectStatusChange("Connected to port"); + Logger.Info("Serial port opened successfully."); + OnConnectStatusChange?.Invoke("Connected to port"); _connected = true; while (true) @@ -69,7 +67,7 @@ internal class MaiTouchComConnector catch (TimeoutException) { } catch (Exception ex) { - OnConnectError(); + OnConnectError?.Invoke(); Application.Current.Dispatcher.Invoke(() => { MessageBox.Show(ex.Message, "Error connecting to COM port", MessageBoxButton.OK, MessageBoxImage.Error); @@ -78,10 +76,13 @@ internal class MaiTouchComConnector } finally { + Logger.Info("Disconnecting from COM port"); _connected = false; - OnConnectStatusChange("Not Connected"); + OnConnectStatusChange?.Invoke("Not Connected"); if (serialPort?.IsOpen == true) { + serialPort.DiscardInBuffer(); + serialPort.DiscardOutBuffer(); serialPort.Close(); } } @@ -90,62 +91,71 @@ internal class MaiTouchComConnector public async Task Disconnect() { + Logger.Info("Disconnecting from COM port"); _shouldReconnect = false; _connected = false; try { - serialPort.DtrEnable = false; - serialPort.RtsEnable = false; - serialPort.DataReceived -= SerialPort_DataReceived; - await Task.Delay(200); - if (serialPort.IsOpen == true) + if (serialPort != null) { - serialPort.DiscardInBuffer(); - serialPort.DiscardOutBuffer(); - serialPort.Close(); + serialPort.DtrEnable = false; + serialPort.RtsEnable = false; + serialPort.DataReceived -= SerialPort_DataReceived; + await Task.Delay(200); + if (serialPort.IsOpen == true) + { + serialPort.DiscardInBuffer(); + serialPort.DiscardOutBuffer(); + serialPort.Close(); + } } + } catch (Exception ex) { + Logger.Error("Error whilst disconnecting from COM port", ex); MessageBox.Show(ex.Message); } } void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { - var recievedData = serialPort.ReadExisting(); - var commands = recievedData.Split(new[] { '}' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var command in commands) + var recievedData = serialPort?.ReadExisting(); + var commands = recievedData?.Split(new[] { '}' }, StringSplitOptions.RemoveEmptyEntries); + if (commands != null) { - var cleanedCommand = command.TrimStart('{'); - Console.WriteLine($"Received data: {cleanedCommand}"); - OnDataRecieved(cleanedCommand); + foreach (var command in commands) + { + var cleanedCommand = command.TrimStart('{'); + Logger.Info($"Received serial data: {cleanedCommand}"); + OnDataRecieved?.Invoke(cleanedCommand); - if (cleanedCommand == "STAT") - { - isActiveMode = true; - } - else if (cleanedCommand == "RSET") - { + if (cleanedCommand == "STAT") + { + isActiveMode = true; + } + else if (cleanedCommand == "RSET") + { - } - else if (cleanedCommand == "HALT") - { - isActiveMode = false; - } - else if (cleanedCommand[2] == 'r' || cleanedCommand[2] == 'k') - { - var leftOrRight = cleanedCommand[0]; - var sensor = cleanedCommand[1]; - var ratio = cleanedCommand[3]; + } + else if (cleanedCommand == "HALT") + { + isActiveMode = false; + } + else if (cleanedCommand[2] == 'r' || cleanedCommand[2] == 'k') + { + var leftOrRight = cleanedCommand[0]; + var sensor = cleanedCommand[1]; + var ratio = cleanedCommand[3]; - var newString = $"({leftOrRight}{sensor}{cleanedCommand[2]}{ratio})"; - serialPort.Write(newString); - OnDataSent(newString); - } - else - { - Console.WriteLine(cleanedCommand); + var newString = $"({leftOrRight}{sensor}{cleanedCommand[2]}{ratio})"; + serialPort?.Write(newString); + OnDataSent?.Invoke(newString); + } + else + { + Logger.Warn($"Unhandled serial data command {cleanedCommand}"); + } } } } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 4fb651f..f2afd9e 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -47,12 +47,14 @@ public partial class MainWindow : Window if (Properties.Settings.Default.FirstOpen) { + Logger.Info("First open occurred"); MessageBox.Show("Please remove any COM devices using the COM3 port before installing the virtual COM port. In Device Manager click \"View\" then enabled \"Show hidden devices\" and uninstall any devices that are using the COM3 port.\n\nAfter ensuring COM3 is free please use the install COM port button in the app to register the app.\n\nThe app needs to connect to the port prior to Sinmai.exe being opened.", "First time setup", MessageBoxButton.OK, MessageBoxImage.Information); Properties.Settings.Default.FirstOpen = false; Properties.Settings.Default.Save(); } Loaded += (s, e) => { + Logger.Info("Main window loaded, creating touch panel"); _touchPanel = new TouchPanel(); _touchPanel.onTouch = (value) => { buttonState.PressButton(value); }; _touchPanel.onRelease = (value) => { buttonState.ReleaseButton(value); }; @@ -90,6 +92,7 @@ public partial class MainWindow : Window var processes = Process.GetProcessesByName("Sinmai"); if (processes.Length > 0) { + Logger.Info("Found sinmai process to exit alongside with"); sinamiProcess = processes[0]; } else @@ -98,10 +101,13 @@ public partial class MainWindow : Window } } await sinamiProcess.WaitForExitAsync(); + Logger.Info("Sinmai exited"); var dataContext = (MainWindowViewModel)DataContext; if (dataContext.IsExitWithSinmaiEnabled) { + Logger.Info("Disconnecting from COM port before shutting down"); await connector.Disconnect(); + Logger.Info("Shutting down..."); Application.Current.Shutdown(); } } @@ -178,6 +184,8 @@ public partial class MainWindow : Window private async void buttonInstallComPort_Click(object sender, RoutedEventArgs e) { + throw new Exception("Test exception for crash dump generation"); + await comPortManager.InstallComPort(); } diff --git a/TouchPanel.xaml.cs b/TouchPanel.xaml.cs index d1faf97..ff0eb81 100644 --- a/TouchPanel.xaml.cs +++ b/TouchPanel.xaml.cs @@ -11,10 +11,10 @@ namespace WpfMaiTouchEmulator; /// public partial class TouchPanel : Window { - internal Action onTouch; - internal Action onRelease; + internal Action? onTouch; + internal Action? onRelease; - private readonly Dictionary activeTouches = []; + private readonly Dictionary activeTouches = []; private readonly TouchPanelPositionManager _positionManager; private List buttons = []; @@ -44,10 +44,10 @@ public partial class TouchPanel : Window { while (true) { - if (activeTouches.Any() && !TouchesOver.Any()) + if (activeTouches.Count != 0 && !TouchesOver.Any()) { await Task.Delay(100); - if (activeTouches.Any() && !TouchesOver.Any()) + if (activeTouches.Count != 0 && !TouchesOver.Any()) { DeselectAllItems(); } @@ -64,8 +64,11 @@ public partial class TouchPanel : Window public void PositionTouchPanel() { var position = _positionManager.GetSinMaiWindowPosition(); - if (position != null) + if (position != null && + (Top != position.Value.Top || Left != position.Value.Left || Width != position.Value.Width || Height != position.Value.Height) + ) { + Logger.Info("Touch panel not over sinmai window, repositioning"); Top = position.Value.Top; Left = position.Value.Left; Width = position.Value.Width; @@ -103,7 +106,7 @@ public partial class TouchPanel : Window { // Highlight the element and add it to the active touches tracking. HighlightElement(element, true); - onTouch((TouchValue)element.Tag); + onTouch?.Invoke((TouchValue)element.Tag); activeTouches[e.TouchDevice.Id] = element; } e.Handled = true; @@ -125,7 +128,7 @@ public partial class TouchPanel : Window HighlightElement(previousElement, false); Application.Current.Dispatcher.Invoke(() => { - onRelease((TouchValue)previousElement.Tag); + onRelease?.Invoke((TouchValue)previousElement.Tag); }); }); @@ -133,7 +136,7 @@ public partial class TouchPanel : Window // Highlight the new element and update the tracking. HighlightElement(newElement, true); - onTouch((TouchValue)newElement.Tag); + onTouch?.Invoke((TouchValue)newElement.Tag); activeTouches[e.TouchDevice.Id] = newElement; } @@ -146,29 +149,20 @@ public partial class TouchPanel : Window if (activeTouches.TryGetValue(e.TouchDevice.Id, out var element)) { HighlightElement(element, false); - onRelease((TouchValue)element.Tag); + onRelease?.Invoke((TouchValue)element.Tag); activeTouches.Remove(e.TouchDevice.Id); } e.Handled = true; } - private bool IsTouchInsideWindow(Point touchPoint) - { - // Define the window's bounds - var windowBounds = new Rect(0, 0, ActualWidth, ActualHeight); - - // Check if the touch point is within the window's bounds - return windowBounds.Contains(touchPoint); - } - private void DeselectAllItems() { // Logic to deselect all items or the last touched item foreach (var element in activeTouches.Values) { HighlightElement(element, false); - onRelease((TouchValue)element.Tag); + onRelease?.Invoke((TouchValue)element.Tag); } activeTouches.Clear(); } @@ -181,7 +175,7 @@ public partial class TouchPanel : Window }); } - private void HighlightElement(System.Windows.Controls.Image element, bool highlight) + private void HighlightElement(Image element, bool highlight) { if (Properties.Settings.Default.IsDebugEnabled) { diff --git a/TouchPanelPositionManager.cs b/TouchPanelPositionManager.cs index e044c67..24149ff 100644 --- a/TouchPanelPositionManager.cs +++ b/TouchPanelPositionManager.cs @@ -23,20 +23,28 @@ class TouchPanelPositionManager public Rect? GetSinMaiWindowPosition() { - var hWnd = FindWindow(null, "Sinmai"); - if (hWnd != IntPtr.Zero) + try { - RECT rect; - if (GetWindowRect(hWnd, out rect)) + var hWnd = FindWindow(null, "Sinmai"); + if (hWnd != IntPtr.Zero) { - // Calculate the desired size and position based on the other application's window - var width = Convert.ToInt32((rect.Right - rect.Left)); - var height = width; - var left = rect.Left + ((rect.Right - rect.Left) - width) / 2; // Center horizontally - var top = rect.Bottom - height; - return new Rect(left, top, width, height); + RECT rect; + if (GetWindowRect(hWnd, out rect)) + { + // Calculate the desired size and position based on the other application's window + var width = rect.Right - rect.Left; + var height = width; + var left = rect.Left + ((rect.Right - rect.Left) - width) / 2; // Center horizontally + var top = rect.Bottom - height; + return new Rect(left, top, width, height); + } } } + catch (Exception ex) + { + Logger.Error("Failed top get sinmai window position", ex); + } + return null; } } diff --git a/VirtualComPortManager.cs b/VirtualComPortManager.cs index cb45d87..3f938cd 100644 --- a/VirtualComPortManager.cs +++ b/VirtualComPortManager.cs @@ -28,57 +28,69 @@ internal class VirtualComPortManager public async Task InstallComPort() { + Logger.Info("Trying to install virtual COM port."); if (await CheckIfPortInstalled("COM3", false)) { + Logger.Warn("Port COM3 already registered."); MessageBox.Show("Port COM3 already registered. Either remove it via Device Manager or uninstall the virutal port."); return; } try { + Logger.Info("Calling com0com to install virtual COM ports"); await ExecuteCommandAsync("setupc.exe", $"install PortName=COM3 PortName=COM23"); if (await CheckIfPortInstalled("COM3", true)) { + Logger.Info("Port COM3 successfully installed."); MessageBox.Show("Port COM3 successfully installed."); } else { + Logger.Error("Port COM3 failed to install"); MessageBox.Show($"Port COM3 failed to install", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } catch (Exception ex) { + Logger.Error("Port COM3 failed to install", ex); MessageBox.Show($"Port COM3 failed to install. {ex}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } public async Task UninstallVirtualPorts() { - + Logger.Info("Trying to uninstall virtual COM port."); if (!await CheckIfPortInstalled("COM3", true)) { + Logger.Warn("Port COM3 not found. No need to uninstall."); MessageBox.Show("Port COM3 not found. No need to uninstall."); return; } try { + Logger.Info("Calling com0com to uninstall virtual COM ports"); await ExecuteCommandAsync("setupc.exe", $"uninstall"); if (!await CheckIfPortInstalled("COM3", false)) { + Logger.Info("Port COM3 successfully uninstalled."); MessageBox.Show("Port COM3 successfully uninstalled."); } else { + Logger.Error("Port COM3 failed to uninstall"); MessageBox.Show($"Port COM3 failed to uninstall. It may be a real device, uninstall it from Device Manager", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } catch (Exception ex) { + Logger.Error("Port COM3 failed to uninstall", ex); MessageBox.Show($"Port COM3 failed to uninstall. {ex}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private async Task ExecuteCommandAsync(string command, string arguments) { + Logger.Info($"Executing command {command} with arguments {arguments}"); var processStartInfo = new ProcessStartInfo { FileName = command, @@ -92,5 +104,6 @@ internal class VirtualComPortManager process.Start(); await process.WaitForExitAsync(); + Logger.Info($"Command {command} completed"); } }