diff --git a/LGSTrayBattery/HttpServer.cs b/LGSTrayBattery/HttpServer.cs
new file mode 100644
index 0000000..fc72c3d
--- /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.Length) > 0 ? 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
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)