Skip to content

Commit

Permalink
Merge pull request #7 from andyvorld/feature/http_api
Browse files Browse the repository at this point in the history
Feature/http api
  • Loading branch information
andyvorld authored Jul 5, 2020
2 parents 5b48e7d + 3051b6f commit 78ee829
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 24 deletions.
135 changes: 135 additions & 0 deletions LGSTrayBattery/HttpServer.cs
Original file line number Diff line number Diff line change
@@ -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 = "<html>";

foreach (var logiDevice in viewmodel.LogiDevices)
{
content += $"{logiDevice.DeviceName} : <a href=\"/device/{logiDevice.UsbSerialId}\">{logiDevice.UsbSerialId}</a><br>";
}

content += "</html>";
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
}
}
}
4 changes: 4 additions & 0 deletions LGSTrayBattery/LGSTrayBattery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
<Reference Include="Hid.Net, Version=3.1.0.0, Culture=neutral, PublicKeyToken=7b6bc3f8fb80505e, processorArchitecture=MSIL">
<HintPath>..\packages\Hid.Net.3.1.0\lib\net45\Hid.Net.dll</HintPath>
</Reference>
<Reference Include="INIFileParser, Version=2.5.2.0, Culture=neutral, PublicKeyToken=79af7b307b65cf3c, processorArchitecture=MSIL">
<HintPath>..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll</HintPath>
</Reference>
<Reference Include="PropertyChanged, Version=2.6.1.0, Culture=neutral, PublicKeyToken=ee3ee20bcf148ddd, processorArchitecture=MSIL">
<HintPath>..\packages\PropertyChanged.Fody.2.6.1\lib\net452\PropertyChanged.dll</HintPath>
</Reference>
Expand Down Expand Up @@ -105,6 +108,7 @@
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="HttpServer.cs" />
<Compile Include="LogiDevice.cs" />
<Compile Include="LogiDeviceException.cs" />
<Compile Include="LogiFeatures.cs" />
Expand Down
21 changes: 17 additions & 4 deletions LGSTrayBattery/LogiDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -72,7 +72,7 @@ private set

private bool _listen = false;

private const int HidTimeOut = 250;
private const int HidTimeOut = 500;

private LogiDeviceException _lastException;

Expand All @@ -81,10 +81,10 @@ private set
private Thread _shortListener;
private Thread _longListener;

public LogiDevice(IEnumerable<IDevice> devices, string usbSerialId, out bool valid)
public LogiDevice(IEnumerable<IDevice> devices, string usbSerialId, byte deviceIdx, out bool valid)
{
valid = false;
this.UsbSerialId = usbSerialId;
this.UsbSerialId = $"{usbSerialId}_{deviceIdx}";

foreach (var device in devices)
{
Expand Down Expand Up @@ -176,6 +176,19 @@ private async Task UpdateBatteryVoltage()
}
}

public string XmlData()
{
string output = "";
output += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
output += "<xml>\n";
output += $"<device_name>{DeviceName}</device_name>\n";
output += $"<battery_voltage>{BatteryVoltage:f2}</battery_voltage>\n";
output += $"<battery_percent>{BatteryPercentage:f2}</battery_percent>\n";
output += "</xml>";

return output;
}

public async Task Listen()
{
if (_listen)
Expand Down
1 change: 1 addition & 0 deletions LGSTrayBattery/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
<MenuItem Header="Rescan Devices" Click="RescanDevices"/>
<MenuItem Header="Exit" Click="ExitButton_OnClick"/>
</ContextMenu>
</tb:TaskbarIcon.ContextMenu>
Expand Down
56 changes: 40 additions & 16 deletions LGSTrayBattery/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,8 @@ public partial class MainWindow : Window
{
MainWindowViewModel viewModel = new MainWindowViewModel();

private Thread _httpServerThread;

public MainWindow()
{
InitializeComponent();
Expand All @@ -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)
Expand Down Expand Up @@ -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();
}
}
}
8 changes: 4 additions & 4 deletions LGSTrayBattery/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,21 @@ public MainWindowViewModel()
{
PollIntervals = new List<PollInterval>()
{
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()
{
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);

Expand Down Expand Up @@ -110,7 +110,7 @@ public async Task LoadViewModel()
var temp = new List<LogiDevice>();
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)
{
Expand Down
1 change: 1 addition & 0 deletions LGSTrayBattery/packages.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<package id="Fody" version="4.2.1" targetFramework="net461" developmentDependency="true" />
<package id="Hardcodet.NotifyIcon.Wpf" version="1.0.8" targetFramework="net461" />
<package id="Hid.Net" version="3.1.0" targetFramework="net461" />
<package id="ini-parser" version="2.5.2" targetFramework="net461" />
<package id="PropertyChanged.Fody" version="2.6.1" targetFramework="net461" />
<package id="System.ValueTuple" version="4.3.0" targetFramework="net461" />
</packages>
19 changes: 19 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down

0 comments on commit 78ee829

Please # to comment.