Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Feature/http api #7

Merged
merged 3 commits into from
Jul 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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