From 877135acba7d55b8322565f57fb5c576c746faa9 Mon Sep 17 00:00:00 2001 From: andyvorld Date: Sun, 5 Jul 2020 17:11:17 +1000 Subject: [PATCH 1/3] added http server with http interactions --- LGSTrayBattery/HttpServer.cs | 135 ++++++++++++++++++++++++++ LGSTrayBattery/LGSTrayBattery.csproj | 4 + LGSTrayBattery/LogiDevice.cs | 21 +++- LGSTrayBattery/MainWindow.xaml | 1 + LGSTrayBattery/MainWindow.xaml.cs | 56 ++++++++--- LGSTrayBattery/MainWindowViewModel.cs | 8 +- LGSTrayBattery/packages.config | 1 + 7 files changed, 202 insertions(+), 24 deletions(-) create mode 100644 LGSTrayBattery/HttpServer.cs diff --git a/LGSTrayBattery/HttpServer.cs b/LGSTrayBattery/HttpServer.cs new file mode 100644 index 0000000..fa63754 --- /dev/null +++ b/LGSTrayBattery/HttpServer.cs @@ -0,0 +1,135 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using IniParser; +using IniParser.Model; + +namespace LGSTrayBattery +{ + class HttpServer + { + public static bool ServerEnabled; + private static int _tcpPort; + + public static void LoadConfig() + { + var parser = new FileIniDataParser(); + + if (!File.Exists("./HttpConfig.ini")) + { + File.Create("./HttpConfig.ini").Close(); + } + + IniData data = parser.ReadFile("./HttpConfig.ini"); + + if (!bool.TryParse(data["HTTPServer"]["serverEnable"], out ServerEnabled)) + { + data["HTTPServer"]["serverEnable"] = "false"; + } + + if (!int.TryParse(data["HTTPServer"]["tcpPort"], out _tcpPort)) + { + data["HTTPServer"]["tcpPort"] = "12321"; + } + + parser.WriteFile("./HttpConfig.ini", data); + } + + public static async Task ServerLoop(MainWindowViewModel viewmodel) + { + Debug.WriteLine("\nHttp Server started"); + + IPHostEntry host = Dns.GetHostEntry("localhost"); + IPAddress ipAddress = host.AddressList[0]; + IPEndPoint localEndPoint = new IPEndPoint(ipAddress, _tcpPort); + + Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + listener.Bind(localEndPoint); + listener.Listen(10); + + Debug.WriteLine($"Http Server listening on port {_tcpPort}\n"); + + while (true) + { + using (Socket client = listener.Accept()) + { + var bytes = new byte[1024]; + var bytesRec = client.Receive(bytes); + + string httpRequest = Encoding.ASCII.GetString(bytes, 0, bytesRec); + + var matches = Regex.Match(httpRequest, @"GET (.+?) HTTP\/[0-9\.]+"); + if (matches.Groups.Count > 0) + { + int statusCode = 200; + string contentType = "text"; + string content; + + string[] request = matches.Groups[1].ToString().Split(new string[] {"/"}, StringSplitOptions.RemoveEmptyEntries); + switch (request[0]) + { + case ("devices"): + contentType = "text/html"; + content = ""; + + foreach (var logiDevice in viewmodel.LogiDevices) + { + content += $"{logiDevice.DeviceName} : {logiDevice.UsbSerialId}
"; + } + + content += ""; + break; + case ("device"): + if (request.Length < 2) + { + statusCode = 400; + content = "Missing device id"; + } + else + { + LogiDevice targetDevice = + viewmodel.LogiDevices.FirstOrDefault(x => x.UsbSerialId == request[1]); + + if (targetDevice == null) + { + statusCode = 400; + content = $"Device not found, ID = {request[1]}"; + } + else + { + contentType = "text/xml"; + await targetDevice.UpdateBatteryPercentage(); + content = targetDevice.XmlData(); + } + } + + break; + default: + statusCode = 400; + content = $"Requested {matches.Groups[1]}"; + break; + } + + string response = $"HTTP/1.1 {statusCode}\r\n"; + response += $"{contentType}\r\n"; + + response += "Cache-Control: no-store, must-revalidate\r\n"; + response += "Pragma: no-cache\r\n"; + response += "Expires: 0\r\n"; + + response += $"\r\n{content}"; + + client.Send(Encoding.ASCII.GetBytes(response)); + } + } + } + // ReSharper disable once FunctionNeverReturns + } + } +} \ No newline at end of file diff --git a/LGSTrayBattery/LGSTrayBattery.csproj b/LGSTrayBattery/LGSTrayBattery.csproj index cbcea5b..3f2ae95 100644 --- a/LGSTrayBattery/LGSTrayBattery.csproj +++ b/LGSTrayBattery/LGSTrayBattery.csproj @@ -69,6 +69,9 @@ ..\packages\Hid.Net.3.1.0\lib\net45\Hid.Net.dll + + ..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll + ..\packages\PropertyChanged.Fody.2.6.1\lib\net452\PropertyChanged.dll @@ -105,6 +108,7 @@ App.xaml Code + diff --git a/LGSTrayBattery/LogiDevice.cs b/LGSTrayBattery/LogiDevice.cs index 3063033..f56bacb 100644 --- a/LGSTrayBattery/LogiDevice.cs +++ b/LGSTrayBattery/LogiDevice.cs @@ -20,7 +20,7 @@ public class LogiDevice : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; - public string UsbSerialId { get; set; } + public string UsbSerialId { get; private set; } public bool IsChecked { get; set; } = false; @@ -72,7 +72,7 @@ private set private bool _listen = false; - private const int HidTimeOut = 250; + private const int HidTimeOut = 500; private LogiDeviceException _lastException; @@ -81,10 +81,10 @@ private set private Thread _shortListener; private Thread _longListener; - public LogiDevice(IEnumerable devices, string usbSerialId, out bool valid) + public LogiDevice(IEnumerable devices, string usbSerialId, byte deviceIdx, out bool valid) { valid = false; - this.UsbSerialId = usbSerialId; + this.UsbSerialId = $"{usbSerialId}_{deviceIdx}"; foreach (var device in devices) { @@ -176,6 +176,19 @@ private async Task UpdateBatteryVoltage() } } + public string XmlData() + { + string output = ""; + output += "\n"; + output += "\n"; + output += $"{DeviceName}\n"; + output += $"{BatteryVoltage:f2}\n"; + output += $"{BatteryPercentage:f2}\n"; + output += ""; + + return output; + } + public async Task Listen() { if (_listen) diff --git a/LGSTrayBattery/MainWindow.xaml b/LGSTrayBattery/MainWindow.xaml index a314e32..258100f 100644 --- a/LGSTrayBattery/MainWindow.xaml +++ b/LGSTrayBattery/MainWindow.xaml @@ -48,6 +48,7 @@ + diff --git a/LGSTrayBattery/MainWindow.xaml.cs b/LGSTrayBattery/MainWindow.xaml.cs index 0e2da46..073562d 100644 --- a/LGSTrayBattery/MainWindow.xaml.cs +++ b/LGSTrayBattery/MainWindow.xaml.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -26,6 +27,8 @@ public partial class MainWindow : Window { MainWindowViewModel viewModel = new MainWindowViewModel(); + private Thread _httpServerThread; + public MainWindow() { InitializeComponent(); @@ -34,22 +37,7 @@ public MainWindow() this.DataContext = viewModel; - this.TaskbarIcon.Icon = LGSTrayBattery.Properties.Resources.Discovery; - - BackgroundWorker worker = new BackgroundWorker(); - worker.DoWork += new DoWorkEventHandler((s,e) => viewModel.LoadViewModel().Wait()); - worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((s, e) => - { - if (e.Error != null) - { - throw e.Error; - } - - TaskbarIcon.Icon = LGSTrayBattery.Properties.Resources.Unknown; - viewModel.LoadLastSelected(); - }); - - worker.RunWorkerAsync(); + LoadDevices(); } private void CrashHandler(object sender, UnhandledExceptionEventArgs args) @@ -85,5 +73,41 @@ private void TaskbarIcon_OnTrayMouseDoubleClick(object sender, RoutedEventArgs e Debug.WriteLine("Forced Refresh"); viewModel.ForceBatteryRefresh(); } + + private void RescanDevices(object sender, RoutedEventArgs e) + { + LoadDevices(false); + } + + private void LoadDevices(bool startHttpServer = true) + { + this.TaskbarIcon.Icon = LGSTrayBattery.Properties.Resources.Discovery; + + BackgroundWorker worker = new BackgroundWorker(); + worker.DoWork += new DoWorkEventHandler((s, e) => viewModel.LoadViewModel().Wait()); + worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((s, e) => + { + if (e.Error != null) + { + throw e.Error; + } + + TaskbarIcon.Icon = LGSTrayBattery.Properties.Resources.Unknown; + viewModel.LoadLastSelected(); + + HttpServer.LoadConfig(); + + if (HttpServer.ServerEnabled && startHttpServer) + { + _httpServerThread = new Thread(async () => + { + await HttpServer.ServerLoop(viewModel); + }); + _httpServerThread.Start(); + } + }); + + worker.RunWorkerAsync(); + } } } diff --git a/LGSTrayBattery/MainWindowViewModel.cs b/LGSTrayBattery/MainWindowViewModel.cs index 7df8b9c..0128a72 100644 --- a/LGSTrayBattery/MainWindowViewModel.cs +++ b/LGSTrayBattery/MainWindowViewModel.cs @@ -59,12 +59,14 @@ public MainWindowViewModel() { PollIntervals = new List() { - new PollInterval(1000, "1 second"), + new PollInterval(5000, "5 second"), new PollInterval(10000, "10 seconds"), new PollInterval(60*1000, "1 minute"), new PollInterval(5*60*1000, "5 minutes"), new PollInterval(10*60*1000, "15 minutes") }; + + LogiFeatures.LoadConfig(); } public async Task LoadViewModel() @@ -72,8 +74,6 @@ public async Task LoadViewModel() var logger = new DebugLogger(); var tracer = new DebugTracer(); - LogiFeatures.LoadConfig(); - //Register the factory for creating Usb devices. This only needs to be done once. WindowsHidDeviceFactory.Register(logger, tracer); @@ -110,7 +110,7 @@ public async Task LoadViewModel() var temp = new List(); foreach (var usbGroup in hidDeviceGroups) { - LogiDevice logiDevice = new LogiDevice(usbGroup.Value, usbGroup.Key, out var valid); + LogiDevice logiDevice = new LogiDevice(usbGroup.Value, usbGroup.Key, 1, out var valid); if (valid) { diff --git a/LGSTrayBattery/packages.config b/LGSTrayBattery/packages.config index 9f9e2e0..3ef397e 100644 --- a/LGSTrayBattery/packages.config +++ b/LGSTrayBattery/packages.config @@ -4,6 +4,7 @@ + \ No newline at end of file From 18b6d841fdb3a306f86a8efbbbf0149568265a79 Mon Sep 17 00:00:00 2001 From: andyvorld Date: Sun, 5 Jul 2020 17:28:45 +1000 Subject: [PATCH 2/3] catch root request --- LGSTrayBattery/HttpServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LGSTrayBattery/HttpServer.cs b/LGSTrayBattery/HttpServer.cs index fa63754..fc72c3d 100644 --- a/LGSTrayBattery/HttpServer.cs +++ b/LGSTrayBattery/HttpServer.cs @@ -72,7 +72,7 @@ public static async Task ServerLoop(MainWindowViewModel viewmodel) string content; string[] request = matches.Groups[1].ToString().Split(new string[] {"/"}, StringSplitOptions.RemoveEmptyEntries); - switch (request[0]) + switch ((request.Length) > 0 ? request[0] : "") { case ("devices"): contentType = "text/html"; From 3051b6f094bfbb2f8975923ea65ac272a9bbc2b2 Mon Sep 17 00:00:00 2001 From: andyvorld Date: Sun, 5 Jul 2020 17:28:54 +1000 Subject: [PATCH 3/3] updated readme for http server --- readme.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/readme.md b/readme.md index e775ef8..15ed34a 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,25 @@ # LGS Tray Battery A tray app used to track battery levels of wireless Logitech mouse. +## Features +### Tray Indicator +![](https://i.imgur.com/g5e3jsz.png) + +Battery percentage and voltage (if supported) in a tray tooltip with notification icon. + +Right-click for more options. + +### Http/Web "server" api +By default the running of the http server is disabled, to enable modify `HttpConfig.ini` and change `serverEnable = false` to `serverEnable = true`. Default port number is `12321`, which is configurable if you run into any issues with port numbers. + +![](https://i.imgur.com/IH4YKHl.png) + +Send a GET/HTTP request to `localhost:{port}/devices`, for the list of devices currently detected by the program and the corresponding `deviceID`. + +![](https://i.imgur.com/hFIlh0o.png) + +With the `deviceID`, a GET/HTTP request to `localhost:{port}/device/{deviceID}`, will result in an xml document of the name and battery status of the device. Devices that do not support `battery_voltage` will report 0.00. + ## Known Issues Logitech gaming mouses do not natively have a way of reporting battery level, but rather voltage levels. A voltage to percentage lookup table is available for some mouses from Logitech Gaming Software and are included in `PowerModel`. However newer mice have their files embedded within Logitech G Hub and it is not possible to retrieve them without owning said mice. It is possible to dump an `.xml` file within `PowerModel` for support. [Refer to this issue in libratbag.](https://github.com/libratbag/piper/issues/222#issuecomment-487557251)