first commit

This commit is contained in:
BlubbFish 2026-08-03 22:12:18 +02:00
commit 0ff9e48e2e
16 changed files with 705 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.vs
GPSDLib/bin
GPSDLib/obj

31
GPSDLib.sln Normal file
View File

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31112.23
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GPSDLib", "GPSDLib\GPSDLib.csproj", "{570367E6-EA1B-4558-B414-D8C2537283F9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "litjson", "..\litjson\litjson\litjson.csproj", "{1CCC9A15-799A-4FFD-8030-4B9EEA881F13}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{570367E6-EA1B-4558-B414-D8C2537283F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{570367E6-EA1B-4558-B414-D8C2537283F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{570367E6-EA1B-4558-B414-D8C2537283F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{570367E6-EA1B-4558-B414-D8C2537283F9}.Release|Any CPU.Build.0 = Release|Any CPU
{1CCC9A15-799A-4FFD-8030-4B9EEA881F13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1CCC9A15-799A-4FFD-8030-4B9EEA881F13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1CCC9A15-799A-4FFD-8030-4B9EEA881F13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1CCC9A15-799A-4FFD-8030-4B9EEA881F13}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C0D8961F-6017-4E9E-B98C-CDE6D67C47B9}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,9 @@
using System;
namespace Ghostware.GPSDLib.Exceptions {
public class ConnectionLostException : Exception {
public ConnectionLostException() : base("The connection is lost.") {
}
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Ghostware.GPSDLib.Exceptions {
public class NotConnectedException : Exception {
public NotConnectedException() : base("The connection is not open. Plz connect first!") {
}
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Ghostware.GPSDLib.Exceptions {
public class UnknownTypeException : Exception {
public UnknownTypeException() : base("Unknown Class Type") {
}
}
}

11
GPSDLib/GPSDLib.csproj Normal file
View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\litjson\litjson\litjson.csproj" />
</ItemGroup>
</Project>

11
GPSDLib/GpsdConstants.cs Normal file
View File

@ -0,0 +1,11 @@
using System;
using Ghostware.GPSDLib.Models;
namespace Ghostware.GPSDLib {
public static class GpsdConstants {
public static GpsdOptions DefaultGpsdOptions = new GpsdOptions(true, true);
public const String DisableCommand = "?WATCH={\"enable\":false}";
public const String PollCommand = "?POLL;";
}
}

30
GPSDLib/GpsdDataParser.cs Normal file
View File

@ -0,0 +1,30 @@
using System;
using Ghostware.GPSDLib.Models;
using LitJson;
namespace Ghostware.GPSDLib {
public class GpsdDataParser {
public Object GetGpsData(String gpsData) {
try {
JsonData json = JsonMapper.ToObject(gpsData);
if(json.ContainsKey("class") && json["class"].IsString) {
return json["class"].ToString() switch {
"VERSION" => new GpsdVersion(json),
"DEVICES" => new GpsDevices(json),
"DEVICE" => new GpsDevice(json),
"WATCH" => new GpsdOptions(json),
"TPV" => new GpsLocation(json),
"SKY" => new GpsSky(json),
_ => null,
};
}
} catch(Exception e) {
Console.WriteLine(e);
}
return null;
}
}
}

261
GPSDLib/GpsdService.cs Normal file
View File

@ -0,0 +1,261 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security;
using System.Threading.Tasks;
using Ghostware.GPSDLib.Exceptions;
using Ghostware.GPSDLib.Models;
namespace Ghostware.GPSDLib {
public class GpsdService : IDisposable {
#region Private Properties
private TcpClient _client;
private StreamReader _streamReader;
private StreamWriter _streamWriter;
private GpsdDataParser _gpsdDataParser;
private readonly String _serverAddress;
private readonly Int32 _serverPort;
private Boolean _proxyEnabled;
private String _proxyAddress;
private Int32 _proxyPort;
private Boolean _proxyAuthenticationEnabled;
private String _proxyUsername;
private SecureString _proxyPassword;
private GpsLocation _previousGpsLocation;
private GpsSky _lastSky;
private Int32 _retryReadCount;
#endregion
#region Properties
public Boolean IsRunning {
get; set;
}
public Int32 ReadFrequenty { get; set; } = 1000;
public Int32 RetryRead { get; set; } = 3;
public GpsdOptions GpsOptions {
get; set;
}
#endregion
#region Events
public delegate void VersionEventHandler(Object source, GpsdVersion e);
public delegate void LocationEventHandler(Object source, Tuple<GpsLocation, GpsSky> e);
public delegate void RawLocationEventHandler(Object source, String rawLocation);
public event VersionEventHandler OnGpsdVersionChanged;
public event LocationEventHandler OnLocationChanged;
public event RawLocationEventHandler OnRawLocationChanged;
#endregion
#region Constructors
public GpsdService(String serverAddress, Int32 serverPort) {
this._serverAddress = serverAddress;
this._serverPort = serverPort;
this.GpsOptions = GpsdConstants.DefaultGpsdOptions;
this.IsRunning = true;
}
public GpsdService(String serverAddress, Int32 serverPort, GpsdOptions gpsOptions = null) : this(serverAddress, serverPort) => this.GpsOptions = gpsOptions ?? GpsdConstants.DefaultGpsdOptions;
#endregion
#region Connection Functionality
public Boolean Connect() {
this._client = this._proxyEnabled ? this.ConnectViaHttpProxy() : new TcpClient(this._serverAddress, this._serverPort);
this._streamReader = new StreamReader(this._client.GetStream());
this._streamWriter = new StreamWriter(this._client.GetStream());
this._gpsdDataParser = new GpsdDataParser();
String gpsData = this._streamReader.ReadLine();
Object message = this._gpsdDataParser.GetGpsData(gpsData);
if(!(message is GpsdVersion version)) {
return false;
}
OnGpsdVersionChanged?.Invoke(this, version);
this.ExecuteGpsdCommand(this.GpsOptions.GetCommand());
return true;
}
public Boolean Disconnect() {
this.StopGpsReading();
this.Dispose();
return true;
}
#endregion
#region Gps Reading Functionality
/// <summary>
/// This task reads the gps. This task will run in a loop, so keep in mind to run it in a thread.
/// </summary>
/// <exception cref="NotConnectedException">Exception when client is not connected or streamreader is null. Plz call connect!</exception>
/// <exception cref="ConnectionLostException">Exception when the connection is lost.</exception>
public void StartGpsReading() {
if(this._streamReader == null || !this._client.Connected) {
throw new NotConnectedException();
}
this._retryReadCount = this.RetryRead;
this.IsRunning = true;
while(this.IsRunning) {
if(!this._client.Connected) {
throw new ConnectionLostException();
}
try {
String gpsData = this._streamReader.ReadLine();
OnRawLocationChanged?.Invoke(this, gpsData);
if(gpsData == null) {
if(this._retryReadCount == 0) {
throw new ConnectionLostException();
}
this._retryReadCount--;
continue;
}
Object message = this._gpsdDataParser.GetGpsData(gpsData);
if(message is GpsSky sky) {
this._lastSky = sky;
}
if(!(message is GpsLocation gpsLocation) || this._previousGpsLocation != null && gpsLocation.Time.Subtract(new TimeSpan(0, 0, 0, 0, this.ReadFrequenty)) <= this._previousGpsLocation.Time) {
continue;
}
OnLocationChanged?.Invoke(this, new Tuple<GpsLocation, GpsSky>(gpsLocation,this._lastSky));
this._previousGpsLocation = gpsLocation;
} catch(IOException) {
return;
}
}
}
public Task StartGpsReadingAsync() => new Task(this.StartGpsReading);
public void StopGpsReading() {
if(!this.IsRunning) {
return;
}
this.IsRunning = false;
this.ExecuteGpsdCommand(GpsdConstants.DisableCommand);
}
#endregion
#region Helper Functions
private void ExecuteGpsdCommand(String command) {
if(this._streamWriter == null) {
return;
}
this._streamWriter.WriteLine(command);
this._streamWriter.Flush();
}
#endregion
#region Proxies
public void SetProxy(String proxyAddress, Int32 proxyPort) {
this._proxyEnabled = true;
this._proxyAddress = proxyAddress;
this._proxyPort = proxyPort;
}
public void SetProxyAuthentication(String username, String password) {
this._proxyAuthenticationEnabled = true;
this._proxyUsername = username;
SecureString securePass = new SecureString();
foreach(Char passwordChar in password) {
securePass.AppendChar(passwordChar);
}
this._proxyPassword = securePass;
}
public void SetProxyAuthentication(String username, SecureString password) {
this._proxyAuthenticationEnabled = true;
this._proxyUsername = username;
this._proxyPassword = password;
}
public void DisableProxy() => this._proxyEnabled = false;
private TcpClient ConnectViaHttpProxy() {
UriBuilder uriBuilder = new UriBuilder {
Scheme = Uri.UriSchemeHttp,
Host = _proxyAddress,
Port = _proxyPort
};
Uri proxyUri = uriBuilder.Uri;
WebRequest request = WebRequest.Create("http://" + this._serverAddress + ":" + this._serverPort);
WebProxy webProxy = new WebProxy(proxyUri);
request.Proxy = webProxy;
request.Method = "CONNECT";
if(this._proxyAuthenticationEnabled) {
webProxy.Credentials = new NetworkCredential(this._proxyUsername, this._proxyPassword);
} else {
webProxy.UseDefaultCredentials = true;
}
WebResponse response = request.GetResponse();
Stream responseStream = response.GetResponseStream();
Debug.Assert(responseStream != null);
const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
Type rsType = responseStream.GetType();
PropertyInfo connectionProperty = rsType.GetProperty("Connection", flags);
Object connection = connectionProperty.GetValue(responseStream, null);
Type connectionType = connection.GetType();
PropertyInfo networkStreamProperty = connectionType.GetProperty("NetworkStream", flags);
Object networkStream = networkStreamProperty.GetValue(connection, null);
Type nsType = networkStream.GetType();
PropertyInfo socketProperty = nsType.GetProperty("Socket", flags);
Socket socket = (Socket)socketProperty.GetValue(networkStream, null);
return new TcpClient { Client = socket };
}
#endregion
#region Dispose
public void Dispose() {
this._streamReader?.Close();
this._streamWriter?.Close();
this._client?.Close();
}
#endregion
}
}

View File

@ -0,0 +1,59 @@
using System;
using LitJson;
namespace Ghostware.GPSDLib.Models {
public class GpsDevice {
public GpsDevice(JsonData json) {
this.Path = json["path"].ToString();
if(json.ContainsKey("driver")) {
this.Driver = json["driver"].ToString();
}
this.Activated = DateTime.Parse(json["activated"].ToString(), new System.Globalization.CultureInfo("en-US", false));
this.Flags = Double.Parse(json["flags"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Native = Double.Parse(json["native"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Bps = Double.Parse(json["bps"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Parity = json["parity"].ToString();
this.Stopbits = Double.Parse(json["stopbits"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Cycle = Double.Parse(json["cycle"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
public String Path {
get; set;
}
public String Driver {
get; set;
}
public DateTime Activated {
get; set;
}
public Double Flags {
get; set;
}
public Double Native {
get; set;
}
public Double Bps {
get; set;
}
public String Parity {
get; set;
}
public Double Stopbits {
get; set;
}
public Double Cycle {
get; set;
}
public override String ToString() => $"Path: {this.Path} - Driver: {this.Driver} - Activated: {this.Activated} - Flags: {this.Flags} - Native: {this.Native} - Bps: {this.Bps} - Parity: {this.Parity} - Stopbits: {this.Stopbits} - Cycle: {this.Cycle}";
}
}

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
using LitJson;
namespace Ghostware.GPSDLib.Models {
public class GpsDevices {
public GpsDevices(JsonData json) {
this.Devices = new List<GpsDevice>();
foreach(JsonData item in json["devices"]) {
this.Devices.Add(new GpsDevice(item));
}
}
public List<GpsDevice> Devices {
get; set;
}
}
}

View File

@ -0,0 +1,73 @@
using System;
using LitJson;
namespace Ghostware.GPSDLib.Models {
public class GpsLocation {
public GpsLocation(JsonData json) {
this.Device = json["device"].ToString();
this.Mode = Double.Parse(json["mode"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
if(json.ContainsKey("time")) {
this.Time = DateTime.Parse(json["time"].ToString(), new System.Globalization.CultureInfo("en-US", false));
}
if(json.ContainsKey("ept")) {
this.Ept = Double.Parse(json["ept"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
if(json.ContainsKey("alt")) {
this.Altitute = Double.Parse(json["alt"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
if(json.ContainsKey("lat")) {
this.Latitude = Double.Parse(json["lat"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
if(json.ContainsKey("lon")) {
this.Longitude = Double.Parse(json["lon"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
if(json.ContainsKey("track")) {
this.Track = Double.Parse(json["track"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
if(json.ContainsKey("speed")) {
this.SpeedKnots = Double.Parse(json["speed"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
}
public String Device {
get; set;
}
public Double Mode {
get; set;
}
public DateTime Time {
get; set;
}
public Double Ept {
get; set;
}
public Double Altitute {
get; set;
}
public Double Latitude {
get; set;
}
public Double Longitude {
get; set;
}
public Double Track {
get; set;
}
public Double SpeedKnots {
get; set;
}
public Double Speed => this.SpeedKnots * 1.852;
public override String ToString() => $"Device: {this.Device} - Mode: {this.Mode} - Time: {this.Time} - Latitude: {this.Latitude} - Longitude: {this.Longitude} - Altitute: {this.Altitute} - Track: {this.Track} - Speed: {this.Speed}";
}
}

View File

@ -0,0 +1,41 @@
using System;
using LitJson;
namespace Ghostware.GPSDLib.Models {
public class GpsSatelites {
public GpsSatelites(JsonData json) {
this.PRN = Double.Parse(json["PRN"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
if(json.ContainsKey("el")) {
this.El = Double.Parse(json["el"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
this.Az = Double.Parse(json["az"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Ss = Double.Parse(json["ss"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Used = Boolean.Parse(json["used"].ToString());
this.Gnssid = Double.Parse(json["gnssid"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Svid = Double.Parse(json["svid"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
public Double PRN {
get;
}
public Double El {
get;
}
public Double Az {
get;
}
public Double Ss {
get;
}
public Boolean Used {
get;
}
public Double Gnssid {
get;
}
public Double Svid {
get;
}
}
}

47
GPSDLib/Models/GpsSky.cs Normal file
View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using LitJson;
namespace Ghostware.GPSDLib.Models {
public class GpsSky {
public GpsSky(JsonData json) {
this.Vdop = Double.Parse(json["vdop"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Hdop = Double.Parse(json["hdop"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Pdop = Double.Parse(json["pdop"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
if(json.ContainsKey("nSat")) {
this.Sat = Double.Parse(json["nSat"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
if(json.ContainsKey("uSat") && this.Sat == 0) {
this.Sat = Double.Parse(json["uSat"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
if(json.ContainsKey("satellites")) {
this.Satelites = new List<GpsSatelites>();
foreach(JsonData item in json["satellites"]) {
this.Satelites.Add(new GpsSatelites(item));
}
}
}
public Double Vdop {
get;
}
public Double Hdop {
get;
}
public Double Pdop {
get;
}
public Double Sat {
get;
}
public List<GpsSatelites> Satelites {
get;
}
public override String ToString() => $"Vdop: {this.Vdop} - Hdop: {this.Hdop} - Ndop: {this.Pdop} - Satelites: {this.Sat}";
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using LitJson;
namespace Ghostware.GPSDLib.Models {
public class GpsdOptions {
public GpsdOptions(JsonData json) {
this.Enable = Boolean.Parse(json["enable"].ToString());
this.Json = Boolean.Parse(json["json"].ToString());
this.Nmea = Boolean.Parse(json["nmea"].ToString());
this.Raw = Double.Parse(json["raw"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Scaled = Boolean.Parse(json["scaled"].ToString());
this.Timing = Boolean.Parse(json["timing"].ToString());
this.Split24 = Boolean.Parse(json["split24"].ToString());
this.Pps = Boolean.Parse(json["pps"].ToString());
}
public GpsdOptions(Boolean enable, Boolean json) {
this.Enable = enable;
this.Json = json;
}
public Boolean Enable {
get; set;
}
public Boolean Json {
get; set;
}
public Boolean Nmea {
get; set;
}
public Double Raw {
get; set;
}
public Boolean Scaled {
get; set;
}
public Boolean Timing {
get; set;
}
public Boolean Split24 {
get; set;
}
public Boolean Pps {
get; set;
}
public String GetCommand() => $"?WATCH={JsonMapper.ToJson(new Dictionary<String, Object>() { { "enable", this.Enable }, { "json", this.Json }, { "nmea", this.Nmea }, { "raw", this.Raw }, { "scaled", this.Scaled }, { "timing", this.Timing }, { "split24", this.Split24 }, { "pps", this.Pps } })}";
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Runtime.Serialization;
using LitJson;
namespace Ghostware.GPSDLib.Models {
public class GpsdVersion {
public GpsdVersion(JsonData json) {
this.Release = Double.Parse(json["release"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.Rev = Double.Parse(json["rev"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.ProtoMajor = Double.Parse(json["proto_major"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
this.ProtoMinor = Double.Parse(json["proto_minor"].ToString().Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
}
public Double Release {
get; set;
}
public Double Rev {
get; set;
}
public Double ProtoMajor {
get; set;
}
public Double ProtoMinor {
get; set;
}
public override String ToString() => $"Release: {this.Release} - Revision: {this.Rev} - ProtoMajor: {this.ProtoMajor} - ProtoMinor: {this.ProtoMinor}";
}
}