diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a468e7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Exclude Visual Studio solution and project specific user options. +*.suo +*.user +*.userosscache +*.sln.docstates +.vs/ +*.opendb +*.VC.db +*.VC.VC.opendb +*.sln + +# Exclude StyleCop cache files. +[Ss]tyle[Cc]op.[Cc]ache + +# Exclude build result files. +[Bb]inary/ +[Bb]in/ +[Oo]bj/ +[Dd]ebug/ +[Rr]elease/ +[Tt]estResults/ + +# Exclude NuGet specific folders and files. +*.nupkg +**/[Pp]ackages/* +!**/[Pp]ackages/[Ww]itron* +!**/[Pp]ackages/[Bb]uild/ +!**/[Pp]ackages/[Rr]epositories.config + +# Exclude common temporary/log files. +*~ +*.~* +*.log + +# Exclude backup files created by some editors. +*.bak \ No newline at end of file diff --git a/App.axaml b/App.axaml new file mode 100644 index 0000000..d086068 --- /dev/null +++ b/App.axaml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/App.axaml.cs b/App.axaml.cs new file mode 100644 index 0000000..4b15de1 --- /dev/null +++ b/App.axaml.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using MqttDebugger.ViewModels; +using MqttDebugger.Views; + +namespace MqttDebugger +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/Assets/Images/logo.afdesign b/Assets/Images/logo.afdesign new file mode 100644 index 0000000..05e9fe4 Binary files /dev/null and b/Assets/Images/logo.afdesign differ diff --git a/Assets/Images/logo.ico b/Assets/Images/logo.ico new file mode 100644 index 0000000..e2f04b4 Binary files /dev/null and b/Assets/Images/logo.ico differ diff --git a/Assets/Images/logo.png b/Assets/Images/logo.png new file mode 100644 index 0000000..354d68a Binary files /dev/null and b/Assets/Images/logo.png differ diff --git a/Models/Client.cs b/Models/Client.cs new file mode 100644 index 0000000..c731fcc --- /dev/null +++ b/Models/Client.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Security; +using System.Text; + +namespace MqttDebugger.Models +{ + public class Client + { + public bool IsConnected { get; set; } = false; + public string Host { get; set; } = "localhost"; + public int Port { get; set; } = 1883; + public MqttUser User { get; set; } + public string Topic { get; set; } = "myTopic"; + public bool ConnectToInternalServer { get; set; } = false; + } +} diff --git a/Models/MqttMessageOptions.cs b/Models/MqttMessageOptions.cs new file mode 100644 index 0000000..541a964 --- /dev/null +++ b/Models/MqttMessageOptions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MqttDebugger.Models +{ + public class MqttMessageOptions + { + public bool DisplayTopic { get; set; } = false; + public bool DisplayPayloadAsString { get; set; } = true; + public string FilterByTopic { get; set; } = "#"; + public bool WritePayloadToFile { get; set; } = false; + public string FolderOutPath { get; set; } + } +} diff --git a/Models/MqttUser.cs b/Models/MqttUser.cs new file mode 100644 index 0000000..971181a --- /dev/null +++ b/Models/MqttUser.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MqttDebugger.Models +{ + public class MqttUser + { + public string Username { get; set; } + public string Password { get; set; } + + public MqttUser(string _username, string _password) + { + Username = _username; + Password = _password; + } + } +} diff --git a/Models/Server.cs b/Models/Server.cs new file mode 100644 index 0000000..63f5d5b --- /dev/null +++ b/Models/Server.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Security; +using System.Text; + +namespace MqttDebugger.Models +{ + public class Server + { + public bool IsRunning { get; set; } + public List Users { get; set; } + public int Port { get; set; } = 1883; + } +} diff --git a/MqttDebugger.csproj b/MqttDebugger.csproj new file mode 100644 index 0000000..568ecaf --- /dev/null +++ b/MqttDebugger.csproj @@ -0,0 +1,16 @@ + + + WinExe + netcoreapp3.1 + logo.ico + + + + + + + + + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..0a1479a --- /dev/null +++ b/Program.cs @@ -0,0 +1,23 @@ +using System; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.ReactiveUI; + +namespace MqttDebugger +{ + class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToDebug() + .UseReactiveUI(); + } +} diff --git a/Properties/PublishProfiles/FolderProfile.pubxml b/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..0f3adc4 --- /dev/null +++ b/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + bin\Release\netcoreapp3.1\publish\ + FileSystem + netcoreapp3.1 + win-x64 + false + True + True + + \ No newline at end of file diff --git a/ViewLocator.cs b/ViewLocator.cs new file mode 100644 index 0000000..c99f7fc --- /dev/null +++ b/ViewLocator.cs @@ -0,0 +1,32 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using MqttDebugger.ViewModels; + +namespace MqttDebugger +{ + public class ViewLocator : IDataTemplate + { + public bool SupportsRecycling => false; + + public IControl Build(object data) + { + var name = data.GetType().FullName.Replace("ViewModel", "View"); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type); + } + else + { + return new TextBlock { Text = "Not Found: " + name }; + } + } + + public bool Match(object data) + { + return data is ViewModelBase; + } + } +} \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..ebcb329 --- /dev/null +++ b/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,663 @@ +using Avalonia.Controls.Notifications; +using Avalonia.Media; +using Avalonia.Platform; +using MqttDebugger.Models; +using MqttDebugger.Views; +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Client.Options; +using MQTTnet.Extensions; +using MQTTnet.Protocol; +using MQTTnet.Server; +using ReactiveUI; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reactive; +using System.Reactive.Linq; +using System.Security; +using System.Text; +using System.Threading; + +namespace MqttDebugger.ViewModels +{ + public class MainWindowViewModel : ViewModelBase + { + /// + /// Create a NotificationManager in order to display messages. + /// + public IManagedNotificationManager NotificationManager + { + get { return _notificationManager; } + set { this.RaiseAndSetIfChanged(ref _notificationManager, value); } + } + + /// + /// Let the user choose which key should trigger the send action. + /// + private bool sendMessageShortcut = true; + public bool SendMessageShortcut + { + get => sendMessageShortcut; + set => this.RaiseAndSetIfChanged(ref sendMessageShortcut, value); + } + + /// + /// Let the user select choose dark or light appearance. + /// + private bool darkMode = false; + public bool DarkMode + { + get => darkMode; + set => this.RaiseAndSetIfChanged(ref darkMode, value); + } + + /// + /// Indicates if the included MQTT-Server is running. + /// + private bool isServerRunning = false; + public bool IsServerRunning + { + get => isServerRunning; + set => this.RaiseAndSetIfChanged(ref isServerRunning, value); + } + + /// + /// Indicates if the application is connected to a broker (e.g. MQTT-Server). + /// + private bool isConnectedToServer = false; + public bool IsConnectedToServer + { + get => isConnectedToServer; + set + { + this.RaiseAndSetIfChanged(ref isConnectedToServer, value); + mqttClient.IsConnected = IsConnectedToServer; + } + } + + /// + /// The username of the included MQTT-Server. + /// + private string serverUsernames = string.Empty; + public string ServerUsernames + { + get => serverUsernames; + set => this.RaiseAndSetIfChanged(ref serverUsernames, value); + } + + /// + /// The password of the included MQTT-Server. + /// + private string serverPasswords = string.Empty; + public string ServerPasswords + { + get => serverPasswords; + set => this.RaiseAndSetIfChanged(ref serverPasswords, value); + } + + /// + /// The host, to which the client should connect. + /// + private string clientHostname = string.Empty; + public string ClientHostname + { + get => clientHostname; + set + { + this.RaiseAndSetIfChanged(ref clientHostname, value); + mqttClient.Host = clientHostname; + } + } + + /// + /// The username used to connect to the specified host. + /// + private string clientUsername = string.Empty; + public string ClientUsername + { + get => clientUsername; + set => this.RaiseAndSetIfChanged(ref clientUsername, value); + } + + /// + /// The password used to connect to the specified host. + /// + private string clientPassword; + public string ClientPassword + { + get => clientPassword; + set => this.RaiseAndSetIfChanged(ref clientPassword, value); + } + + /// + /// The topic used to send messages to. + /// + private string clientTopic = "myTopic"; + public string ClientTopic + { + get => clientTopic; + set + { + this.RaiseAndSetIfChanged(ref clientTopic, value); + mqttClient.Topic = ClientTopic; + } + } + + /// + /// Shows if the user wants to connect to the included server. + /// In that case hostname, username and password are not necessary. + /// + private bool connectToInternalServer = true; + public bool ConnectToInternalServer + { + get => connectToInternalServer; + set => this.RaiseAndSetIfChanged(ref connectToInternalServer, value); + } + + /// + /// The text to display the client connection status. + /// + private string clientStatusText = "Status: Not connected."; + public string ClientStatusText + { + get => clientStatusText; + set => this.RaiseAndSetIfChanged(ref clientStatusText, value); + } + + /// + /// The text to indicate which option the button will trigger. + /// + private string clientConnectionButtonText = "Connect"; + public string ClientConnectionButtonText + { + get => clientConnectionButtonText; + set => this.RaiseAndSetIfChanged(ref clientConnectionButtonText, value); + } + + /// + /// The status display text for the server area. + /// + private string serverStatusText = "Stopped."; + public string ServerStatusText + { + get => serverStatusText; + set => this.RaiseAndSetIfChanged(ref serverStatusText, value); + } + + /// + /// Adjust text color according to status. + /// + private IBrush serverStatusTextColor = Brushes.Red; + public IBrush ServerStatusTextColor + { + get => serverStatusTextColor; + set => this.RaiseAndSetIfChanged(ref serverStatusTextColor, value); + } + + /// + /// The message currently staged to be sent. + /// + private string mqttMessageText = string.Empty; + public string MqttMessageText + { + get => mqttMessageText; + set => this.RaiseAndSetIfChanged(ref mqttMessageText, value); + } + + /// + /// The received messages of the current topic. + /// + private string receivedMessages = string.Empty; + public string ReceivedMessages + { + get => receivedMessages; + set => this.RaiseAndSetIfChanged(ref receivedMessages, value); + } + + /// + /// The IP-Adress of the MQTT-Server in the local network. + /// + private string localIp = "127.0.0.1:1883"; + public string LocalIp + { + get => localIp; + set => this.RaiseAndSetIfChanged(ref localIp, value); + } + + /// + /// Shows if the user wants to connect to the included server. + /// In that case hostname, username and password are not necessary. + /// + private bool showTopicSelector = false; + public bool ShowTopicSelector + { + get => showTopicSelector; + set => this.RaiseAndSetIfChanged(ref showTopicSelector, value); + } + + /// + /// Wether the Topic should be shown in the log window. + /// + private bool messageOptionShowTopic = false; + public bool MessageOptionShowTopic + { + get => messageOptionShowTopic; + set + { + this.RaiseAndSetIfChanged(ref messageOptionShowTopic, value); + mqttMessageOptions.DisplayTopic = MessageOptionShowTopic; + } + } + + /// + /// The topic used to listen for messages. + /// Listen to all topics except topics that start with '$'. + /// + private string filterByTopic = "#"; + public string FilterByTopic + { + get => filterByTopic; + set + { + this.RaiseAndSetIfChanged(ref filterByTopic, value); + mqttMessageOptions.FilterByTopic = filterByTopic; + } + } + + /// + /// Wether the Payload should be logged to the output window as a UTF-8 endcoded string. + /// + private bool messageOptionDisplayPayloadAsString = true; + public bool MessageOptionDisplayPayloadAsString + { + get => messageOptionDisplayPayloadAsString; + set + { + this.RaiseAndSetIfChanged(ref messageOptionDisplayPayloadAsString, value); + mqttMessageOptions.DisplayPayloadAsString = MessageOptionDisplayPayloadAsString; + } + } + + /// + /// Wether the Payload should be written to a file. + /// + private bool messageOptionWritePayloadToFile = false; + public bool MessageOptionWritePayloadToFile + { + get => messageOptionWritePayloadToFile; + set + { + this.RaiseAndSetIfChanged(ref messageOptionWritePayloadToFile, value); + mqttMessageOptions.WritePayloadToFile = MessageOptionWritePayloadToFile; + } + } + + /// + /// The path to the folder where the payload will be saved if WritePayloadToFile is true. + /// + private string fileOutputFolder; + public string FileOutputFolder + { + get => fileOutputFolder; + set + { + this.RaiseAndSetIfChanged(ref fileOutputFolder, value); + mqttMessageOptions.FolderOutPath = fileOutputFolder; + } + } + + // Create reactive commands. + public ReactiveCommand StartServerCommand { get; } + public ReactiveCommand StopServerCommand { get; } + public ReactiveCommand RestartServerCommand { get; } + public ReactiveCommand ResetSettingsCommand { get; } + public ReactiveCommand ConnectToServerCommand { get; } + public ReactiveCommand SendMessageCommand { get; } + public ReactiveCommand ClearMessageLogCommand { get; } + + + // For notification handling. + private MainWindow _window; + private IManagedNotificationManager _notificationManager; + + // Create default instances of client and server + private Server mqttServer = new Server(); + private Client mqttClient = new Client(); + private MqttMessageOptions mqttMessageOptions = new MqttMessageOptions(); + + private IMqttServer server; + private IMqttClient client; + private Thread listenForMessagesThread; + + public MainWindowViewModel(MainWindow window, IManagedNotificationManager notificationManager) + { + // Initialize reactive commands. + StartServerCommand = ReactiveCommand.Create(StartServer); + StopServerCommand = ReactiveCommand.Create(StopServer); + RestartServerCommand = ReactiveCommand.Create(RestartServer); + ResetSettingsCommand = ReactiveCommand.Create(ResetServerSettings); + ConnectToServerCommand = ReactiveCommand.Create(ConnectToServer); + SendMessageCommand = ReactiveCommand.Create(SendMessage); + ClearMessageLogCommand = ReactiveCommand.Create(ClearMessageLog); + + // Copy references for window and notificationManager + _notificationManager = notificationManager; + _window = window; + } + + /// + /// Creates a new MQTT-Server instance and starts it. + /// + private async void StartServer() + { + IMqttServerOptions serverOptions; + // Get Users and Passwords from user input. + string[] usernames = ServerUsernames.Replace(" ", "").Split(';'); + string[] passwords = ServerPasswords.Replace(" ", "").Split(';'); + + if (usernames.Length != passwords.Length) + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", $"Could not start the server, because usernames and passwords do not match.", NotificationType.Error)); + return; + } + + mqttServer.Users = new List(); + for (int i = 0; i < usernames.Length; i++) + { + mqttServer.Users.Add(new MqttUser(usernames[i], passwords[i])); + } + + var factory = new MqttFactory(); + server = factory.CreateMqttServer(); + + if (ServerUsernames.Length > 0) + { + serverOptions = new MqttServerOptionsBuilder() + .WithDefaultEndpoint() + .WithDefaultEndpointPort(mqttServer.Port) + .WithConnectionValidator(c => + { + foreach (MqttUser user in mqttServer.Users) + { + if (user.Username == c.Username && user.Password == c.Password) + { + c.ReasonCode = MqttConnectReasonCode.Success; + return; + } + } + c.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + }) + .WithSubscriptionInterceptor(c => + { + c.AcceptSubscription = true; + }) + .WithApplicationMessageInterceptor(c => + { + c.AcceptPublish = true; + }) + .Build(); + } + else + { + serverOptions = new MqttServerOptionsBuilder() + .WithDefaultEndpoint() + .WithDefaultEndpointPort(mqttServer.Port) + .WithSubscriptionInterceptor(c => + { + c.AcceptSubscription = true; + }) + .WithApplicationMessageInterceptor(c => + { + c.AcceptPublish = true; + }) + .Build(); + } + + try + { + await server.StartAsync(serverOptions); + ServerStatusText = "Running"; + ServerStatusTextColor = Brushes.Green; + IsServerRunning = true; + } + catch (Exception e) + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", $"Could not start the server: {e.Message}", NotificationType.Error)); + } + + try + { + try + { + using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0)) + { + socket.Connect("8.8.8.8", 65530); + IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint; + LocalIp = $"{endPoint.Address}:{mqttServer.Port}"; + } + } + catch (SocketException) + { + LocalIp = $"127.0.0.1:{mqttServer.Port}"; + } + } + catch (Exception e) + { + Debug.WriteLine($"Could not read local ip adress: {e}"); + } + + } + + /// + /// Stops a running MQTT-Server if available. + /// + private async void StopServer() + { + try + { + await server.StopAsync(); + ServerStatusText = "Stopped"; + ServerStatusTextColor = Brushes.Red; + IsServerRunning = false; + } + catch (Exception e) + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", $"Could not stop the server: {e.Message}", NotificationType.Error)); + } + } + + /// + /// Restarts or Starts the MQTT-Server (depends on current state). + /// + private void RestartServer() + { + if (server.IsStarted) + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Information", $"Restarting the server...", NotificationType.Information)); + StopServer(); + StartServer(); + } + else + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Information", $"Starting the server...", NotificationType.Information)); + StartServer(); + } + } + + /// + /// Connect to a server via the provided credentials. + /// + private async void ConnectToServer() + { + if (mqttClient.IsConnected) + { + try + { + await client.DisconnectAsync(); + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Information", $"Disconnected from server.", NotificationType.Information)); + ClientStatusText = "Status: Not connected."; + ClientConnectionButtonText = "Connect"; + IsConnectedToServer = false; + MqttMessageText = string.Empty; + // One might also keep those message in the queue... + ReceivedMessages = string.Empty; + } + catch (Exception e) + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", $"Could not disconnect: {e.Message}", NotificationType.Error)); + } + + } + else + { + mqttClient.User = new MqttUser(ClientUsername, ClientPassword); + + if (ClientHostname.Length > 0) + { + if (ClientHostname.Contains(':')) + { + int port = 1883; + + mqttClient.Host = ClientHostname.Split(':')[0]; + if (int.TryParse(ClientHostname.Split(':')[1], out port)) + { + mqttClient.Port = port; + } + } + else + { + mqttClient.Host = ClientHostname; + } + } + + var factory = new MqttFactory(); + client = factory.CreateMqttClient(); + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer(mqttClient.Host, mqttClient.Port) + .WithCredentials((ConnectToInternalServer ? mqttServer.Users?.FirstOrDefault().Username : mqttClient.User.Username) ?? "", + (ConnectToInternalServer ? mqttServer.Users?.FirstOrDefault().Password : mqttClient.User.Password) ?? "") + .Build(); + try + { + await client.ConnectAsync(clientOptions, new CancellationToken()); + listenForMessagesThread = new Thread(ListenForMessages); + listenForMessagesThread.Start(); + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Success", $"Connected to Server.", NotificationType.Success)); + ClientStatusText = "Status: Connected to Server."; + ClientConnectionButtonText = "Disconnect"; + IsConnectedToServer = true; + } + catch (Exception e) + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", $"Could not connect: {e.Message}", NotificationType.Error)); + } + } + } + + /// + /// Send the message from the current queue. + /// + private async void SendMessage() + { + if (client.IsConnected) + { + if (string.IsNullOrEmpty(mqttClient.Topic)) + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "The Topic can not be empty.", NotificationType.Error)); + } + else + { + if (string.IsNullOrEmpty(MqttMessageText)) + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "The Payload can not be empty.", NotificationType.Error)); + } + else + { + await client.PublishAsync(mqttClient.Topic, MqttMessageText); + MqttMessageText = string.Empty; + } + } + } + else + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "The server can not be reached anymore...", NotificationType.Error)); + } + } + + private async void ListenForMessages() + { + List topicFilters = new List(); + string [] topicsAsString = mqttMessageOptions.FilterByTopic.Replace(" ", string.Empty).Split(';'); + + if (topicsAsString.Length > 0) + { + foreach (string topicAsString in topicsAsString) + { + MqttTopicFilter mqttTopicFilter = new MqttTopicFilter(); + mqttTopicFilter.Topic = topicAsString; + topicFilters.Add(mqttTopicFilter); + } + + await client.SubscribeAsync(topicFilters.ToArray()); + } +/* else if (topicsAsString.Length == 1) + { + await client.SubscribeAsync(mqttMessageOptions.FilterByTopic); + }*/ + else + { + NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Can not subscribe to chosen topic. Please change the topic in the general tab.", NotificationType.Error)); + } + + + while (client.IsConnected == true) + { + client.UseApplicationMessageReceivedHandler(e => + { + if (e.ProcessingFailed == false) + { + string ReceivedMessage = string.Empty; + if (mqttMessageOptions.DisplayTopic) + { + ReceivedMessage += $"Topic: {e.ApplicationMessage.Topic} "; + } + if (mqttMessageOptions.DisplayPayloadAsString) + { + if (mqttMessageOptions.DisplayTopic || mqttMessageOptions.WritePayloadToFile) + { + ReceivedMessage += $"Payload: {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)} "; + } + else + { + // If no other information is outputted, write out the message only. + ReceivedMessage += Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + } + } + ReceivedMessage += "\n"; + ReceivedMessages += ReceivedMessage; + //_window.ScrollTextToEnd(); // Does not work. + } + }); + } + } + + /// + /// Replaces the user configuration of the development server with the default values. + /// + private void ResetServerSettings() + { + ServerPasswords = string.Empty; + ServerUsernames = string.Empty; + StopServer(); + } + + private void ClearMessageLog() + { + ReceivedMessages = string.Empty; + } + } +} diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..60deb8c --- /dev/null +++ b/ViewModels/ViewModelBase.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; +using ReactiveUI; + +namespace MqttDebugger.ViewModels +{ + public class ViewModelBase : ReactiveObject + { + } +} diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml new file mode 100644 index 0000000..89bb48a --- /dev/null +++ b/Views/MainWindow.axaml @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..5e0cea9 --- /dev/null +++ b/Views/MainWindow.axaml.cs @@ -0,0 +1,106 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using MqttDebugger.ViewModels; +using System.Diagnostics; +using System.Windows.Input; + +namespace MqttDebugger.Views +{ + public class MainWindow : Window + { + private WindowNotificationManager _notificationArea; + private ScrollViewer messageLogScrollViewer; + + private TextBlock linkTextBlock; + private CheckBox saveToFileCheckBox; + private TextBox topicFilterTextBox; + + private string topicBefore = "#"; + + public MainWindow() + { + InitializeComponent(); + + _notificationArea = new WindowNotificationManager(this) + { + Position = NotificationPosition.TopRight, + MaxItems = 2 + }; + + DataContext = new MainWindowViewModel(this, _notificationArea); + + messageLogScrollViewer = this.FindControl("MessageLogScrollViewer"); + ScrollTextToEnd(); + + linkTextBlock = this.FindControl("LinkText"); + linkTextBlock.Tapped += OpenLink; + + saveToFileCheckBox = this.FindControl("SaveToFileCheckBox"); + saveToFileCheckBox.Checked += SaveToFileCheckBox_Checked; + + topicFilterTextBox = this.FindControl("TopicFilterTextBox"); + topicFilterTextBox.GotFocus += TopicFilterTextBox_GotFocus; + topicFilterTextBox.LostFocus += TopicFilterTextBox_LostFocus; + } + + private void TopicFilterTextBox_GotFocus(object sender, GotFocusEventArgs e) + { + topicBefore = topicFilterTextBox.Text; + } + + private void TopicFilterTextBox_LostFocus(object sender, RoutedEventArgs e) + { + if (((MainWindowViewModel)DataContext).IsConnectedToServer) + { + if (topicFilterTextBox.Text != topicBefore) + { + _notificationArea.Show(new Notification("Reload required.", "You will need to reconnect to the server, for that setting to become active.", NotificationType.Information)); + } + } + } + + private async void SaveToFileCheckBox_Checked(object sender, RoutedEventArgs e) + { + OpenFolderDialog dialog = new OpenFolderDialog(); + dialog.Title = "Select a folder to output the payload to."; + + string result = await dialog.ShowAsync(this); + + if (result.Length > 0) + { + ((MainWindowViewModel)DataContext).FileOutputFolder = result; + } + else + { + _notificationArea.Show(new Notification("Error", "An output folder is required to write the payload to files.", NotificationType.Error)); + saveToFileCheckBox.IsChecked = false; + } + } + + private void OpenLink(object sender, RoutedEventArgs e) + { + TextBlock urlTextBlock = (TextBlock)sender; + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = $"http://{urlTextBlock.Text}", + UseShellExecute = true + }; + Process.Start(psi); + } + + public void ScrollTextToEnd() + { + messageLogScrollViewer.ScrollToEnd(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/Views/ValueConverters/BooleanToKeyboardShortcutConverter.cs b/Views/ValueConverters/BooleanToKeyboardShortcutConverter.cs new file mode 100644 index 0000000..e56ccc2 --- /dev/null +++ b/Views/ValueConverters/BooleanToKeyboardShortcutConverter.cs @@ -0,0 +1,27 @@ +using Avalonia.Data.Converters; +using Avalonia.Input; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace MqttDebugger.Views.ValueConverters +{ + public class BooleanToKeyboardShortcutConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + bool isShiftEnter = (bool)value; + if (isShiftEnter) + { + return new KeyGesture(Key.Enter, KeyModifiers.Shift); + } + return new KeyGesture(Key.Enter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/logo.ico b/logo.ico new file mode 100644 index 0000000..e2f04b4 Binary files /dev/null and b/logo.ico differ diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..6c273ab --- /dev/null +++ b/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + +