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 @@ public async Task StartTouchSensorPolling() 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 @@ public async Task StartTouchSensorPolling() } 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 @@ public async Task StartTouchSensorPolling() 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); - - if (cleanedCommand == "STAT") - { - isActiveMode = true; - } - else if (cleanedCommand == "RSET") + foreach (var command in commands) { + var cleanedCommand = command.TrimStart('{'); + Logger.Info($"Received serial data: {cleanedCommand}"); + OnDataRecieved?.Invoke(cleanedCommand); - } - 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]; + if (cleanedCommand == "STAT") + { + isActiveMode = true; + } + else if (cleanedCommand == "RSET") + { - var newString = $"({leftOrRight}{sensor}{cleanedCommand[2]}{ratio})"; - serialPort.Write(newString); - OnDataSent(newString); - } - else - { - Console.WriteLine(cleanedCommand); + } + 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?.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 MainWindow() 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 @@ private async void ExitWithSinmaiLoop() 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 @@ private async void ExitWithSinmaiLoop() } } 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 @@ private void exitWithSinmai_Click(object sender, RoutedEventArgs e) 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 @@ private async void StateCheckLoop() { 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 @@ private void Window_Loaded(object sender, RoutedEventArgs e) 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 @@ private void Element_TouchDown(object sender, TouchEventArgs e) { // 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 @@ private void Element_TouchMove(object sender, TouchEventArgs e) HighlightElement(previousElement, false); Application.Current.Dispatcher.Invoke(() => { - onRelease((TouchValue)previousElement.Tag); + onRelease?.Invoke((TouchValue)previousElement.Tag); }); }); @@ -133,7 +136,7 @@ private void Element_TouchMove(object sender, TouchEventArgs e) // 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 @@ private void Element_TouchUp(object sender, TouchEventArgs e) 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 void SetDebugMode(bool enabled) }); } - 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 @@ public struct RECT 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 @@ public async Task CheckIfPortInstalled(string port, bool expectToExist) 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 @@ private async Task ExecuteCommandAsync(string command, string arguments) process.Start(); await process.WaitForExitAsync(); + Logger.Info($"Command {command} completed"); } }