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 @@
+
+
+
+
+
+
+
+
+