diff --git a/CShocker/CShocker.csproj b/CShocker/CShocker.csproj index af47cc0..4f5ace4 100644 --- a/CShocker/CShocker.csproj +++ b/CShocker/CShocker.csproj @@ -7,7 +7,7 @@ Glax https://github.com/C9Glax/CShocker git - 1.3.8 + 2.0.0 diff --git a/CShocker/Devices/Abstract/Device.cs b/CShocker/Devices/Abstract/Device.cs new file mode 100644 index 0000000..662661c --- /dev/null +++ b/CShocker/Devices/Abstract/Device.cs @@ -0,0 +1,77 @@ +using CShocker.Devices.Additional; +using CShocker.Ranges; +using CShocker.Shockers.Abstract; +using Microsoft.Extensions.Logging; + +namespace CShocker.Devices.Abstract; + +public abstract class Device : IDisposable +{ + // ReSharper disable 4 times MemberCanBePrivate.Global external use + public readonly IntensityRange IntensityRange; + public readonly DurationRange DurationRange; + protected ILogger? Logger; + public readonly DeviceApi ApiType; + private readonly Queue> _queue = new(); + private bool _workQueue = true; + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly Thread _workQueueThread; + private const short CommandDelay = 50; + + public void Control(ControlAction action, int? intensity = null, int? duration = null, params IShocker[] shockers) + { + int i = intensity ?? IntensityRange.GetRandomRangeValue(); + int d = duration ?? DurationRange.GetRandomRangeValue(); + if (action is ControlAction.Nothing) + { + this.Logger?.Log(LogLevel.Information, "Doing nothing"); + return; + } + foreach (IShocker shocker in shockers) + { + this.Logger?.Log(LogLevel.Debug, $"Enqueueing {action} {(intensity is not null ? $"Overwrite {i}" : $"{i}")} {(duration is not null ? $"Overwrite {d}" : $"{d}")}"); + _queue.Enqueue(new(action, shocker, i ,d)); + } + } + + protected abstract void ControlInternal(ControlAction action, IShocker shocker, int intensity, int duration); + + protected Device(IntensityRange intensityRange, DurationRange durationRange, DeviceApi apiType, ILogger? logger = null) + { + Thread workQueueThread; + this.IntensityRange = intensityRange; + this.DurationRange = durationRange; + this.ApiType = apiType; + this.Logger = logger; + this._workQueueThread = new Thread(QueueThread); + this._workQueueThread.Start(); + } + + private void QueueThread() + { + while (_workQueue) + if (_queue.Count > 0 && _queue.Dequeue() is { } action) + { + this.Logger?.Log(LogLevel.Information, $"{action.Item1} {action.Item2} {action.Item3} {action.Item4}"); + ControlInternal(action.Item1, action.Item2, action.Item3, action.Item4); + Thread.Sleep(action.Item4 + CommandDelay); + } + } + + public void SetLogger(ILogger? logger) + { + this.Logger = logger; + } + + public override string ToString() + { + return $"ShockerType: {Enum.GetName(typeof(DeviceApi), this.ApiType)}\n" + + $"IntensityRange: {IntensityRange}\n" + + $"DurationRange: {DurationRange}\n\r"; + } + + public void Dispose() + { + _workQueue = false; + } +} \ No newline at end of file diff --git a/CShocker/Devices/Abstract/OpenShockDevice.cs b/CShocker/Devices/Abstract/OpenShockDevice.cs new file mode 100644 index 0000000..99fc951 --- /dev/null +++ b/CShocker/Devices/Abstract/OpenShockDevice.cs @@ -0,0 +1,72 @@ +using System.Net.Http.Headers; +using CShocker.Devices.Additional; +using CShocker.Ranges; +using CShocker.Shockers; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace CShocker.Devices.Abstract; + +public abstract class OpenShockDevice : Device +{ + protected readonly HttpClient HttpClient = new(); + public string Endpoint { get; init; } + public string ApiKey { get; init; } + + public OpenShockDevice(IntensityRange intensityRange, DurationRange durationRange, DeviceApi apiType, string apiKey, string endpoint = "https://api.shocklink.net", ILogger? logger = null) : base(intensityRange, durationRange, apiType, logger) + { + this.Endpoint = endpoint; + this.ApiKey = apiKey; + } + + public List GetShockers() + { + List shockers = new(); + + HttpClient httpClient = new(); + HttpRequestMessage requestOwnShockers = new (HttpMethod.Get, $"{Endpoint}/1/shockers/own") + { + Headers = + { + UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, + Accept = { new MediaTypeWithQualityHeaderValue("application/json") } + } + }; + requestOwnShockers.Headers.Add("OpenShockToken", ApiKey); + this.Logger?.Log(LogLevel.Debug, $"Requesting {requestOwnShockers.RequestUri}"); + HttpResponseMessage ownResponse = httpClient.Send(requestOwnShockers); + this.Logger?.Log(!ownResponse.IsSuccessStatusCode ? LogLevel.Error : LogLevel.Debug, + $"{requestOwnShockers.RequestUri} response: {ownResponse.StatusCode}"); + if (!ownResponse.IsSuccessStatusCode) + return shockers; + + StreamReader ownShockerStreamReader = new(ownResponse.Content.ReadAsStream()); + string ownShockerJson = ownShockerStreamReader.ReadToEnd(); + this.Logger?.Log(LogLevel.Debug,ownShockerJson); + JObject ownShockerListJObj = JObject.Parse(ownShockerJson); + shockers.AddRange(ownShockerListJObj.SelectTokens("$.data..shockers[*]").Select(t => t.ToObject())); + + HttpRequestMessage requestSharedShockers = new (HttpMethod.Get, $"{Endpoint}/1/shockers/shared") + { + Headers = + { + UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, + Accept = { new MediaTypeWithQualityHeaderValue("application/json") } + } + }; + requestSharedShockers.Headers.Add("OpenShockToken", ApiKey); + this.Logger?.Log(LogLevel.Debug, $"Requesting {requestSharedShockers.RequestUri}"); + HttpResponseMessage sharedResponse = httpClient.Send(requestSharedShockers); + this.Logger?.Log(!sharedResponse.IsSuccessStatusCode ? LogLevel.Error : LogLevel.Debug, + $"{requestSharedShockers.RequestUri} response: {sharedResponse.StatusCode}"); + if (!sharedResponse.IsSuccessStatusCode) + return shockers; + + StreamReader sharedShockerStreamReader = new(sharedResponse.Content.ReadAsStream()); + string sharedShockerJson = sharedShockerStreamReader.ReadToEnd(); + this.Logger?.Log(LogLevel.Debug, sharedShockerJson); + JObject sharedShockerListJObj = JObject.Parse(sharedShockerJson); + shockers.AddRange(sharedShockerListJObj.SelectTokens("$.data..shockers[*]").Select(t => t.ToObject())); + return shockers; + } +} \ No newline at end of file diff --git a/CShocker/Devices/Abstract/PiShockDevice.cs b/CShocker/Devices/Abstract/PiShockDevice.cs new file mode 100644 index 0000000..79747d2 --- /dev/null +++ b/CShocker/Devices/Abstract/PiShockDevice.cs @@ -0,0 +1,12 @@ +using CShocker.Devices.Additional; +using CShocker.Ranges; +using Microsoft.Extensions.Logging; + +namespace CShocker.Devices.Abstract; + +public abstract class PiShockDevice : Device +{ + protected PiShockDevice(IntensityRange intensityRange, DurationRange durationRange, DeviceApi apiType, ILogger? logger = null) : base(intensityRange, durationRange, apiType, logger) + { + } +} \ No newline at end of file diff --git a/CShocker/Devices/Abstract/SerialPortInfo.cs b/CShocker/Devices/Abstract/SerialPortInfo.cs new file mode 100644 index 0000000..1ce7871 --- /dev/null +++ b/CShocker/Devices/Abstract/SerialPortInfo.cs @@ -0,0 +1,20 @@ +namespace CShocker.Devices.Abstract; + +public struct SerialPortInfo +{ + public readonly string? PortName, Description, Manufacturer, DeviceID; + + public SerialPortInfo(string? portName, string? description, string? manufacturer, string? deviceID) + { + this.PortName = portName; + this.Description = description; + this.Manufacturer = manufacturer; + this.DeviceID = deviceID; + } + + public override string ToString() + { + return + $"{GetType().Name}\nPortName: {PortName}\nDescription: {Description}\nManufacturer: {Manufacturer}\nDeviceID: {DeviceID}\n\r"; + } +} diff --git a/CShocker/Shockers/ControlAction.cs b/CShocker/Devices/Additional/ControlActionEnum.cs similarity index 64% rename from CShocker/Shockers/ControlAction.cs rename to CShocker/Devices/Additional/ControlActionEnum.cs index a2c42fa..49bffb4 100644 --- a/CShocker/Shockers/ControlAction.cs +++ b/CShocker/Devices/Additional/ControlActionEnum.cs @@ -1,4 +1,4 @@ -namespace CShocker.Shockers; +namespace CShocker.Devices.Additional; public enum ControlAction { diff --git a/CShocker/Shockers/Abstract/ShockerApi.cs b/CShocker/Devices/Additional/DeviceApiEnum.cs similarity index 57% rename from CShocker/Shockers/Abstract/ShockerApi.cs rename to CShocker/Devices/Additional/DeviceApiEnum.cs index 89e3b65..bcd5276 100644 --- a/CShocker/Shockers/Abstract/ShockerApi.cs +++ b/CShocker/Devices/Additional/DeviceApiEnum.cs @@ -1,6 +1,6 @@ -namespace CShocker.Shockers.Abstract; +namespace CShocker.Devices.Additional; -public enum ShockerApi : byte +public enum DeviceApi : byte { OpenShockHttp = 0, OpenShockSerial = 1, diff --git a/CShocker/Shockers/ShockerJsonConverter.cs b/CShocker/Devices/Additional/DeviceJsonConverter.cs similarity index 68% rename from CShocker/Shockers/ShockerJsonConverter.cs rename to CShocker/Devices/Additional/DeviceJsonConverter.cs index 4600880..722eb6b 100644 --- a/CShocker/Shockers/ShockerJsonConverter.cs +++ b/CShocker/Devices/Additional/DeviceJsonConverter.cs @@ -1,51 +1,48 @@ -using CShocker.Ranges; -using CShocker.Shockers.Abstract; -using CShocker.Shockers.APIS; +using CShocker.Devices.Abstract; +using CShocker.Ranges; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace CShocker.Shockers; +namespace CShocker.Devices.Additional; -public class ShockerJsonConverter : JsonConverter +public class DeviceJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) { - return (objectType == typeof(Shocker)); + return (objectType == typeof(Device)); } public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { JObject jo = JObject.Load(reader); - ShockerApi? apiType = (ShockerApi?)jo.SelectToken("ApiType")?.Value(); + DeviceApi? apiType = (DeviceApi?)jo.SelectToken("ApiType")?.Value(); switch (apiType) { - case ShockerApi.OpenShockHttp: + case DeviceApi.OpenShockHttp: return new OpenShockHttp( - jo.SelectToken("ShockerIds")!.ToObject>()!, jo.SelectToken("IntensityRange")!.ToObject()!, jo.SelectToken("DurationRange")!.ToObject()!, jo.SelectToken("ApiKey")!.Value()!, jo.SelectToken("Endpoint")!.Value()! ); - case ShockerApi.OpenShockSerial: + case DeviceApi.OpenShockSerial: return new OpenShockSerial( - jo.SelectToken("Model")!.ToObject>()!, jo.SelectToken("IntensityRange")!.ToObject()!, jo.SelectToken("DurationRange")!.ToObject()!, - jo.SelectToken("SerialPortI")!.ToObject()! + jo.SelectToken("SerialPortI")!.ToObject()!, + jo.SelectToken("ApiKey")!.Value()!, + jo.SelectToken("Endpoint")!.Value()! ); - case ShockerApi.PiShockHttp: + case DeviceApi.PiShockHttp: return new PiShockHttp( - jo.SelectToken("ShockerIds")!.ToObject>()!, jo.SelectToken("IntensityRange")!.ToObject()!, jo.SelectToken("DurationRange")!.ToObject()!, jo.SelectToken("ApiKey")!.Value()!, jo.SelectToken("Username")!.Value()!, - jo.SelectToken("ShareCode")!.Value()!, jo.SelectToken("Endpoint")!.Value()! ); - case ShockerApi.PiShockSerial: + case DeviceApi.PiShockSerial: throw new NotImplementedException(); default: throw new Exception(); diff --git a/CShocker/Shockers/Abstract/SerialShocker.cs b/CShocker/Devices/Additional/SerialHelper.cs similarity index 55% rename from CShocker/Shockers/Abstract/SerialShocker.cs rename to CShocker/Devices/Additional/SerialHelper.cs index 6eb0e18..6a2c332 100644 --- a/CShocker/Shockers/Abstract/SerialShocker.cs +++ b/CShocker/Devices/Additional/SerialHelper.cs @@ -1,24 +1,12 @@ -using System.IO.Ports; -using CShocker.Ranges; -using Microsoft.Extensions.Logging; -using System.Management; +using System.Management; using System.Runtime.Versioning; +using CShocker.Devices.Abstract; using Microsoft.Win32; -namespace CShocker.Shockers.Abstract; +namespace CShocker.Devices.Additional; -public abstract class SerialShocker : Shocker +public static class SerialHelper { - public SerialPortInfo SerialPortI; - protected readonly SerialPort SerialPort; - - protected SerialShocker(List shockerIds, IntensityRange intensityRange, DurationRange durationRange, SerialPortInfo serialPortI, int baudRate, ShockerApi apiType, ILogger? logger = null) : base(shockerIds, intensityRange, durationRange, apiType, logger) - { - this.SerialPortI = serialPortI; - this.SerialPort = new SerialPort(serialPortI.PortName, baudRate); - this.SerialPort.Open(); - } - [SupportedOSPlatform("windows")] public static List GetSerialPorts() { @@ -56,22 +44,4 @@ public abstract class SerialShocker : Shocker return ret; } - public class SerialPortInfo - { - public readonly string? PortName, Description, Manufacturer, DeviceID; - - public SerialPortInfo(string? portName, string? description, string? manufacturer, string? deviceID) - { - this.PortName = portName; - this.Description = description; - this.Manufacturer = manufacturer; - this.DeviceID = deviceID; - } - - public override string ToString() - { - return - $"PortName: {PortName}\nDescription: {Description}\nManufacturer: {Manufacturer}\nDeviceID: {DeviceID}"; - } - } } \ No newline at end of file diff --git a/CShocker/Devices/OpenShockHttp.cs b/CShocker/Devices/OpenShockHttp.cs new file mode 100644 index 0000000..1537be3 --- /dev/null +++ b/CShocker/Devices/OpenShockHttp.cs @@ -0,0 +1,64 @@ +using System.Net.Http.Headers; +using System.Text; +using CShocker.Devices.Abstract; +using CShocker.Devices.Additional; +using CShocker.Ranges; +using CShocker.Shockers; +using CShocker.Shockers.Abstract; +using Microsoft.Extensions.Logging; + +namespace CShocker.Devices; + +public class OpenShockHttp : OpenShockDevice +{ + + + protected override void ControlInternal(ControlAction action, IShocker shocker, int intensity, int duration) + { + if (shocker is not OpenShockShocker openShockShocker) + { + this.Logger?.Log(LogLevel.Warning, $"Shocker {shocker} is not {typeof(OpenShockShocker).FullName}"); + return; + } + + HttpRequestMessage request = new (HttpMethod.Post, $"{Endpoint}/2/shockers/control") + { + Headers = + { + UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, + Accept = { new MediaTypeWithQualityHeaderValue("application/json") } + }, + Content = new StringContent("{" + + " \"shocks\": [" + + " {" + + $" \"id\": \"{openShockShocker.id}\"," + + $" \"type\": {ControlActionToByte(action)}," + + $" \"intensity\": {intensity}," + + $" \"duration\": {duration}" + + " }" + + " ]," + + " \"customName\": \"CShocker\"" + + "}", Encoding.UTF8, new MediaTypeHeaderValue("application/json")) + }; + request.Headers.Add("OpenShockToken", ApiKey); + this.Logger?.Log(LogLevel.Debug, $"Request-Content: {request.Content.ReadAsStringAsync().Result}"); + HttpResponseMessage response = HttpClient.Send(request); + this.Logger?.Log(!response.IsSuccessStatusCode ? LogLevel.Error : LogLevel.Debug, + $"{request.RequestUri} response: {response.StatusCode}"); + } + + private byte ControlActionToByte(ControlAction action) + { + return action switch + { + ControlAction.Beep => 3, + ControlAction.Vibrate => 2, + ControlAction.Shock => 1, + _ => 0 + }; + } + + public OpenShockHttp(IntensityRange intensityRange, DurationRange durationRange, string apiKey, string endpoint = "https://api.shocklink.net", ILogger? logger = null) : base(intensityRange, durationRange, DeviceApi.OpenShockHttp, apiKey, endpoint, logger) + { + } +} \ No newline at end of file diff --git a/CShocker/Devices/OpenShockSerial.cs b/CShocker/Devices/OpenShockSerial.cs new file mode 100644 index 0000000..8e1dad9 --- /dev/null +++ b/CShocker/Devices/OpenShockSerial.cs @@ -0,0 +1,51 @@ +using System.IO.Ports; +using CShocker.Devices.Abstract; +using CShocker.Devices.Additional; +using CShocker.Ranges; +using CShocker.Shockers; +using CShocker.Shockers.Abstract; +using Microsoft.Extensions.Logging; + +namespace CShocker.Devices; + +public class OpenShockSerial : OpenShockDevice +{ + private const int BaudRate = 115200; + public SerialPortInfo SerialPortI; + private readonly SerialPort _serialPort; + + public OpenShockSerial(IntensityRange intensityRange, DurationRange durationRange, SerialPortInfo serialPortI, string apiKey, string endpoint = "https://api.shocklink.net", ILogger? logger = null) : base(intensityRange, durationRange, DeviceApi.OpenShockSerial, apiKey, endpoint, logger) + { + this.SerialPortI = serialPortI; + this._serialPort = new SerialPort(serialPortI.PortName, BaudRate); + this._serialPort.Open(); + } + + protected override void ControlInternal(ControlAction action, IShocker shocker, int intensity, int duration) + { + if (shocker is not OpenShockShocker openShockShocker) + { + this.Logger?.Log(LogLevel.Warning, $"Shocker {shocker} is not {typeof(OpenShockShocker).FullName}"); + return; + } + string json = "rftransmit {" + + $"\"model\":\"{Enum.GetName(openShockShocker.model)!.ToLower()}\"," + + $"\"id\":{openShockShocker.rfId}," + + $"\"type\":\"{ControlActionToString(action)}\"," + + $"\"intensity\":{intensity}," + + $"\"durationMs\":{duration}" + + "}"; + _serialPort.WriteLine(json); + } + + private static string ControlActionToString(ControlAction action) + { + return action switch + { + ControlAction.Beep => "sound", + ControlAction.Vibrate => "vibrate", + ControlAction.Shock => "shock", + _ => "stop" + }; + } +} \ No newline at end of file diff --git a/CShocker/Devices/PiShockHttp.cs b/CShocker/Devices/PiShockHttp.cs new file mode 100644 index 0000000..cc7da22 --- /dev/null +++ b/CShocker/Devices/PiShockHttp.cs @@ -0,0 +1,65 @@ +using System.Net.Http.Headers; +using System.Text; +using CShocker.Devices.Abstract; +using CShocker.Devices.Additional; +using CShocker.Ranges; +using CShocker.Shockers; +using CShocker.Shockers.Abstract; +using Microsoft.Extensions.Logging; + +namespace CShocker.Devices; + +public class PiShockHttp : PiShockDevice +{ + // ReSharper disable twice MemberCanBePrivate.Global external usage + public string Username, Endpoint, ApiKey; + protected readonly HttpClient HttpClient = new(); + + public PiShockHttp(IntensityRange intensityRange, DurationRange durationRange, string apiKey, string username, string endpoint = "https://do.pishock.com/api/apioperate", ILogger? logger = null) : base(intensityRange, durationRange, DeviceApi.PiShockHttp, logger) + { + this.Username = username; + this.Endpoint = endpoint; + this.ApiKey = apiKey; + } + + protected override void ControlInternal(ControlAction action, IShocker shocker, int intensity, int duration) + { + if (shocker is not PiShockShocker piShockShocker) + { + this.Logger?.Log(LogLevel.Warning, $"Shocker {shocker} is not {typeof(OpenShockShocker).FullName}"); + return; + } + + HttpRequestMessage request = new (HttpMethod.Post, $"{Endpoint}") + { + Headers = + { + UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, + Accept = { new MediaTypeWithQualityHeaderValue("text/plain") } + }, + Content = new StringContent("{" + + $"\"Username\":\"{Username}\"," + + "\"Name\":\"CShocker\"," + + $"\"Code\":\"{piShockShocker.Code}\"," + + $"\"Intensity\":\"{intensity}\"," + + $"\"Duration\":\"{duration/1000}\"," + //duration is in seconds no ms + $"\"Apikey\":\"{ApiKey}\"," + + $"\"Op\":\"{ControlActionToByte(action)}\"" + + "}", Encoding.UTF8, new MediaTypeHeaderValue("application/json")) + }; + HttpResponseMessage response = HttpClient.Send(request); + this.Logger?.Log(!response.IsSuccessStatusCode ? LogLevel.Error : LogLevel.Debug, + $"{request.RequestUri} response: {response.StatusCode}"); + } + + private byte ControlActionToByte(ControlAction action) + { + return action switch + { + ControlAction.Beep => 2, + ControlAction.Vibrate => 1, + ControlAction.Shock => 0, + _ => 2 + }; + } +} \ No newline at end of file diff --git a/CShocker/Shockers/APIS/PiShockSerial.cs b/CShocker/Devices/PiShockSerial.cs similarity index 56% rename from CShocker/Shockers/APIS/PiShockSerial.cs rename to CShocker/Devices/PiShockSerial.cs index ad95012..f32a538 100644 --- a/CShocker/Shockers/APIS/PiShockSerial.cs +++ b/CShocker/Devices/PiShockSerial.cs @@ -1,18 +1,26 @@ -using CShocker.Ranges; +using System.IO.Ports; +using CShocker.Devices.Abstract; +using CShocker.Devices.Additional; +using CShocker.Ranges; using CShocker.Shockers.Abstract; using Microsoft.Extensions.Logging; -namespace CShocker.Shockers.APIS; +namespace CShocker.Devices; -public class PiShockSerial : SerialShocker +public class PiShockSerial : PiShockDevice { private const int BaudRate = 115200; - public PiShockSerial(List shockerIds, IntensityRange intensityRange, DurationRange durationRange, SerialPortInfo serialPortI, ILogger? logger = null) : base(shockerIds, intensityRange, durationRange, serialPortI, BaudRate, ShockerApi.PiShockSerial, logger) + public SerialPortInfo SerialPortI; + private readonly SerialPort _serialPort; + + public PiShockSerial(IntensityRange intensityRange, DurationRange durationRange, DeviceApi apiType, SerialPortInfo serialPortI, ILogger? logger = null) : base(intensityRange, durationRange, apiType, logger) { + this.SerialPortI = serialPortI; + this._serialPort = new SerialPort(this.SerialPortI.PortName, BaudRate); throw new NotImplementedException(); } - - protected override void ControlInternal(ControlAction action, string shockerId, int intensity, int duration) + + protected override void ControlInternal(ControlAction action, IShocker shocker, int intensity, int duration) { string json = "{" + "\"cmd\": \"operate\"," + @@ -23,7 +31,7 @@ public class PiShockSerial : SerialShocker $"\"id\": " + "}" + "}"; - SerialPort.WriteLine(json); + _serialPort.WriteLine(json); } private static string ControlActionToOp(ControlAction action) diff --git a/CShocker/Shockers/APIS/OpenShockHttp.cs b/CShocker/Shockers/APIS/OpenShockHttp.cs deleted file mode 100644 index 39fb69e..0000000 --- a/CShocker/Shockers/APIS/OpenShockHttp.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using CShocker.Ranges; -using CShocker.Shockers.Abstract; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; - -namespace CShocker.Shockers.APIS; - -public class OpenShockHttp : HttpShocker -{ - - public List GetShockers() - { - HttpRequestMessage requestDevices = new (HttpMethod.Get, $"{Endpoint}/2/devices") - { - Headers = - { - UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, - Accept = { new MediaTypeWithQualityHeaderValue("application/json") } - } - }; - requestDevices.Headers.Add("OpenShockToken", ApiKey); - this.Logger?.Log(LogLevel.Debug, $"Requesting {requestDevices.RequestUri}"); - HttpResponseMessage responseDevices = HttpClient.Send(requestDevices); - - StreamReader deviceStreamReader = new(responseDevices.Content.ReadAsStream()); - string deviceJson = deviceStreamReader.ReadToEnd(); - this.Logger?.Log(!responseDevices.IsSuccessStatusCode ? LogLevel.Critical : LogLevel.Debug, - $"{requestDevices.RequestUri} response: {responseDevices.StatusCode}\n{deviceJson}"); - JObject deviceListJObj = JObject.Parse(deviceJson); - List deviceIds = new(); - deviceIds.AddRange(deviceListJObj["data"]!.Children()["id"].Values()!); - - List shockerIds = new(); - foreach (string deviceId in deviceIds) - { - HttpRequestMessage requestShockers = new (HttpMethod.Get, $"{Endpoint}/2/devices/{deviceId}/shockers") - { - Headers = - { - UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, - Accept = { new MediaTypeWithQualityHeaderValue("application/json") } - } - }; - requestShockers.Headers.Add("OpenShockToken", ApiKey); - this.Logger?.Log(LogLevel.Debug, $"Requesting {requestShockers.RequestUri}"); - HttpResponseMessage response = HttpClient.Send(requestShockers); - - StreamReader shockerStreamReader = new(response.Content.ReadAsStream()); - string shockerJson = shockerStreamReader.ReadToEnd(); - this.Logger?.Log(!response.IsSuccessStatusCode ? LogLevel.Critical : LogLevel.Debug, - $"{requestShockers.RequestUri} response: {response.StatusCode}\n{shockerJson}"); - JObject shockerListJObj = JObject.Parse(shockerJson); - shockerIds.AddRange(shockerListJObj["data"]!.Children()["id"].Values()!); - - } - return shockerIds; - } - - protected override void ControlInternal(ControlAction action, string shockerId, int intensity, int duration) - { - HttpRequestMessage request = new (HttpMethod.Post, $"{Endpoint}/2/shockers/control") - { - Headers = - { - UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, - Accept = { new MediaTypeWithQualityHeaderValue("application/json") } - }, - Content = new StringContent("{" + - "\"shocks\": ["+ - "{"+ - $"\"id\": \"{shockerId}\"," + - $"\"type\": {ControlActionToByte(action)},"+ - $"\"intensity\": {intensity},"+ - $"\"duration\": {duration}"+ - "}" + - "]," + - "\"customName\": CShocker" + - "}", Encoding.UTF8, new MediaTypeHeaderValue("application/json")) - }; - request.Headers.Add("OpenShockToken", ApiKey); - this.Logger?.Log(LogLevel.Debug, $"Request-Content: {request.Content}"); - HttpResponseMessage response = HttpClient.Send(request); - this.Logger?.Log(!response.IsSuccessStatusCode ? LogLevel.Critical : LogLevel.Debug, - $"{request.RequestUri} response: {response.StatusCode}"); - } - - private byte ControlActionToByte(ControlAction action) - { - return action switch - { - ControlAction.Beep => 3, - ControlAction.Vibrate => 2, - ControlAction.Shock => 1, - _ => 0 - }; - } - - public OpenShockHttp(List shockerIds, IntensityRange intensityRange, DurationRange durationRange, string apiKey, string endpoint = "https://api.shocklink.net", ILogger? logger = null) : base(shockerIds, intensityRange, durationRange, apiKey, endpoint, ShockerApi.OpenShockHttp, logger) - { - } -} \ No newline at end of file diff --git a/CShocker/Shockers/APIS/OpenShockSerial.cs b/CShocker/Shockers/APIS/OpenShockSerial.cs deleted file mode 100644 index 03b2b19..0000000 --- a/CShocker/Shockers/APIS/OpenShockSerial.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Net.Http.Headers; -using CShocker.Ranges; -using CShocker.Shockers.Abstract; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; - -namespace CShocker.Shockers.APIS; - -public class OpenShockSerial : SerialShocker -{ - // ReSharper disable once MemberCanBePrivate.Global external usage - public readonly Dictionary Model; - private const int BaudRate = 115200; - public OpenShockSerial(Dictionary shockerIds, IntensityRange intensityRange, DurationRange durationRange, SerialPortInfo serialPortI, ILogger? logger = null) : base(shockerIds.Keys.ToList(), intensityRange, durationRange, serialPortI, BaudRate, ShockerApi.OpenShockSerial, logger) - { - this.Model = shockerIds; - } - - protected override void ControlInternal(ControlAction action, string shockerId, int intensity, int duration) - { - string json = "rftransmit {" + - $"\"model\":\"{Enum.GetName(Model[shockerId])!.ToLower()}\"," + - $"\"id\":{shockerId}," + - $"\"type\":\"{ControlActionToString(action)}\"," + - $"\"intensity\":{intensity}," + - $"\"durationMs\":{duration}" + - "}"; - SerialPort.WriteLine(json); - } - - public Dictionary GetShockers(string apiEndpoint, string apiKey) - { - HttpClient httpClient = new(); - HttpRequestMessage requestDevices = new (HttpMethod.Get, $"{apiEndpoint}/2/devices") - { - Headers = - { - UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, - Accept = { new MediaTypeWithQualityHeaderValue("application/json") } - } - }; - requestDevices.Headers.Add("OpenShockToken", apiKey); - HttpResponseMessage responseDevices = httpClient.Send(requestDevices); - - StreamReader deviceStreamReader = new(responseDevices.Content.ReadAsStream()); - string deviceJson = deviceStreamReader.ReadToEnd(); - this.Logger?.Log(LogLevel.Debug, $"{requestDevices.RequestUri} response: {responseDevices.StatusCode}\n{deviceJson}"); - JObject deviceListJObj = JObject.Parse(deviceJson); - List deviceIds = new(); - deviceIds.AddRange(deviceListJObj["data"]!.Children()["id"].Values()!); - - Dictionary models = new(); - foreach (string deviceId in deviceIds) - { - HttpRequestMessage requestShockers = new (HttpMethod.Get, $"{apiEndpoint}/2/devices/{deviceId}/shockers") - { - Headers = - { - UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, - Accept = { new MediaTypeWithQualityHeaderValue("application/json") } - } - }; - requestShockers.Headers.Add("OpenShockToken", apiKey); - HttpResponseMessage response = httpClient.Send(requestShockers); - - StreamReader shockerStreamReader = new(response.Content.ReadAsStream()); - string shockerJson = shockerStreamReader.ReadToEnd(); - this.Logger?.Log(LogLevel.Debug, $"{requestShockers.RequestUri} response: {response.StatusCode}\n{shockerJson}"); - JObject shockerListJObj = JObject.Parse(shockerJson); - for (int i = 0; i < shockerListJObj["data"]!.Children().Count(); i++) - { - models.Add( - shockerListJObj["data"]![i]!["rfId"]!.Value().ToString(), - Enum.Parse(shockerListJObj["data"]![i]!["model"]!.Value()!) - ); - } - } - - return models; - } - - public enum ShockerModel : byte - { - CaiXianlin = 0, - Petrainer = 1 - } - - private static string ControlActionToString(ControlAction action) - { - return action switch - { - ControlAction.Beep => "sound", - ControlAction.Vibrate => "vibrate", - ControlAction.Shock => "shock", - _ => "stop" - }; - } -} \ No newline at end of file diff --git a/CShocker/Shockers/APIS/PiShockHttp.cs b/CShocker/Shockers/APIS/PiShockHttp.cs deleted file mode 100644 index 766d09c..0000000 --- a/CShocker/Shockers/APIS/PiShockHttp.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using CShocker.Ranges; -using CShocker.Shockers.Abstract; -using Microsoft.Extensions.Logging; - -namespace CShocker.Shockers.APIS; - -public class PiShockHttp : HttpShocker -{ - // ReSharper disable twice MemberCanBePrivate.Global external usage - public readonly string Username, ShareCode; - - public PiShockHttp(List shockerIds, IntensityRange intensityRange, DurationRange durationRange, string apiKey, string username, string shareCode, string endpoint = "https://do.pishock.com/api/apioperate", ILogger? logger = null) : base(shockerIds, intensityRange, durationRange, apiKey, endpoint, ShockerApi.PiShockHttp, logger) - { - this.Username = username; - this.ShareCode = shareCode; - } - - protected override void ControlInternal(ControlAction action, string shockerId, int intensity, int duration) - { - HttpRequestMessage request = new (HttpMethod.Post, $"{Endpoint}") - { - Headers = - { - UserAgent = { new ProductInfoHeaderValue("CShocker", "1") }, - Accept = { new MediaTypeWithQualityHeaderValue("text/plain") } - }, - Content = new StringContent("{" + - $"\"Username\":\"{Username}\"," + - "\"Name\":\"CShocker\"," + - $"\"Code\":\"{ShareCode}\"," + - $"\"Intensity\":\"{intensity}\"," + - $"\"Duration\":\"{duration/1000}\"," + //duration is in seconds no ms - $"\"Apikey\":\"{ApiKey}\"," + - $"\"Op\":\"{ControlActionToByte(action)}\"" + - "}", Encoding.UTF8, new MediaTypeHeaderValue("application/json")) - }; - HttpResponseMessage response = HttpClient.Send(request); - this.Logger?.Log(LogLevel.Debug, $"{request.RequestUri} response: {response.StatusCode} {response.Content.ReadAsStringAsync()}"); - } - - private byte ControlActionToByte(ControlAction action) - { - return action switch - { - ControlAction.Beep => 2, - ControlAction.Vibrate => 1, - ControlAction.Shock => 0, - _ => 2 - }; - } -} \ No newline at end of file diff --git a/CShocker/Shockers/Abstract/HttpShocker.cs b/CShocker/Shockers/Abstract/HttpShocker.cs deleted file mode 100644 index 0df4535..0000000 --- a/CShocker/Shockers/Abstract/HttpShocker.cs +++ /dev/null @@ -1,25 +0,0 @@ -using CShocker.Ranges; -using Microsoft.Extensions.Logging; - -namespace CShocker.Shockers.Abstract; - -public abstract class HttpShocker : Shocker -{ - protected readonly HttpClient HttpClient = new(); - // ReSharper disable twice MemberCanBeProtected.Global external usage - public string Endpoint { get; init; } - public string ApiKey { get; init; } - - protected HttpShocker(List shockerIds, IntensityRange intensityRange, DurationRange durationRange, string apiKey, string endpoint, ShockerApi apiType, ILogger? logger = null) : base(shockerIds, intensityRange, durationRange, apiType, logger) - { - Endpoint = endpoint; - ApiKey = apiKey; - } - - public override string ToString() - { - return $"{base.ToString()}\n" + - $"Endpoint: {Endpoint}\n" + - $"ApiKey: {ApiKey}"; - } -} \ No newline at end of file diff --git a/CShocker/Shockers/Abstract/Shocker.cs b/CShocker/Shockers/Abstract/Shocker.cs index 7d86b00..caab141 100644 --- a/CShocker/Shockers/Abstract/Shocker.cs +++ b/CShocker/Shockers/Abstract/Shocker.cs @@ -1,53 +1,5 @@ -using System.Reflection.Metadata; -using CShocker.Ranges; -using Microsoft.Extensions.Logging; +namespace CShocker.Shockers.Abstract; -namespace CShocker.Shockers.Abstract; - -public abstract class Shocker +public interface IShocker { - // ReSharper disable 4 times MemberCanBePrivate.Global external use - public readonly List ShockerIds; - public readonly IntensityRange IntensityRange; - public readonly DurationRange DurationRange; - protected ILogger? Logger; - public readonly ShockerApi ApiType; - - public void Control(ControlAction action, string? shockerId = null, int? intensity = null, int? duration = null) - { - int i = intensity ?? IntensityRange.GetRandomRangeValue(); - int d = duration ?? DurationRange.GetRandomRangeValue(); - this.Logger?.Log(LogLevel.Information, $"{action} {(intensity is not null ? $"Overwrite {i}" : $"{i}")} {(duration is not null ? $"Overwrite {d}" : $"{d}")}"); - if (action is ControlAction.Nothing) - return; - if(shockerId is null) - foreach (string shocker in ShockerIds) - ControlInternal(action, shocker, i, d); - else - ControlInternal(action, shockerId, i, d); - } - - protected abstract void ControlInternal(ControlAction action, string shockerId, int intensity, int duration); - - protected Shocker(List shockerIds, IntensityRange intensityRange, DurationRange durationRange, ShockerApi apiType, ILogger? logger = null) - { - this.ShockerIds = shockerIds; - this.IntensityRange = intensityRange; - this.DurationRange = durationRange; - this.ApiType = apiType; - this.Logger = logger; - } - - public void SetLogger(ILogger? logger) - { - this.Logger = logger; - } - - public override string ToString() - { - return $"ShockerType: {Enum.GetName(typeof(ShockerApi), this.ApiType)}\n" + - $"Shocker-IDs: {string.Join(", ", this.ShockerIds)}\n" + - $"IntensityRange: {IntensityRange}\n" + - $"DurationRange: {DurationRange}"; - } } \ No newline at end of file diff --git a/CShocker/Shockers/Additional/ShockerJsonConverter.cs b/CShocker/Shockers/Additional/ShockerJsonConverter.cs new file mode 100644 index 0000000..168279a --- /dev/null +++ b/CShocker/Shockers/Additional/ShockerJsonConverter.cs @@ -0,0 +1,48 @@ +using CShocker.Shockers.Abstract; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CShocker.Shockers.Additional; + +public class ShockerJsonConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(IShocker)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + if (jo.ContainsKey("model")) //OpenShockShocker + { + return new OpenShockShocker() + { + name = jo.SelectToken("name")!.Value()!, + id = jo.SelectToken("id")!.Value()!, + rfId = jo.SelectToken("rfId")!.Value(), + model = (OpenShockShocker.OpenShockModel)jo.SelectToken("model")!.Value(), + createdOn = jo.SelectToken("createdOn")!.Value(), + isPaused = jo.SelectToken("isPaused")!.Value() + }; + } + else //PiShockShocker + { + return new PiShockShocker() + { + Code = jo.SelectToken("Code")!.Value()! + }; + } + throw new Exception(); + } + + public override bool CanWrite => false; + + /// + /// Don't call this + /// + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + throw new Exception("Dont call this"); + } +} \ No newline at end of file diff --git a/CShocker/Shockers/OpenShockShocker.cs b/CShocker/Shockers/OpenShockShocker.cs new file mode 100644 index 0000000..11590f7 --- /dev/null +++ b/CShocker/Shockers/OpenShockShocker.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using CShocker.Shockers.Abstract; + +namespace CShocker.Shockers; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +[SuppressMessage("ReSharper", "InconsistentNaming")] +public struct OpenShockShocker : IShocker +{ + public string name, id; + public short rfId; + public OpenShockModel model; + public DateTime createdOn; + public bool isPaused; + + public enum OpenShockModel : byte + { + CaiXianlin = 0, + Petrainer = 1 + } + + public override string ToString() + { + return $"{GetType().Name}\n" + + $"Name: {name}\n" + + $"ID: {id}\n" + + $"RF-ID: {rfId}\n" + + $"Model: {Enum.GetName(model)}\n" + + $"Created On: {createdOn}\n" + + $"Paused: {isPaused}\n\r"; + } +} \ No newline at end of file diff --git a/CShocker/Shockers/PiShockShocker.cs b/CShocker/Shockers/PiShockShocker.cs new file mode 100644 index 0000000..efb9e24 --- /dev/null +++ b/CShocker/Shockers/PiShockShocker.cs @@ -0,0 +1,8 @@ +using CShocker.Shockers.Abstract; + +namespace CShocker.Shockers; + +public struct PiShockShocker : IShocker +{ + public string Code; +} \ No newline at end of file diff --git a/TestApp/Program.cs b/TestApp/Program.cs index 5d8dd88..0babb27 100644 --- a/TestApp/Program.cs +++ b/TestApp/Program.cs @@ -1,13 +1,37 @@ -using CShocker.Ranges; +using CShocker.Devices; +using CShocker.Devices.Abstract; +using CShocker.Devices.Additional; +using CShocker.Ranges; +using CShocker.Shockers; using CShocker.Shockers.Abstract; -using CShocker.Shockers.APIS; -using Microsoft.Extensions.Logging; +using CShocker.Shockers.Additional; +using Newtonsoft.Json; using TestApp; -Logger logger = new (LogLevel.Trace); +Logger logger = new (); +List shockers = new(); +Console.WriteLine("OpenShock API Key:"); +string? apiKey = Console.ReadLine(); +while(apiKey is null || apiKey.Length < 1) + apiKey = Console.ReadLine(); + + +OpenShockHttp openShockHttp = new (new IntensityRange(30, 50), new DurationRange(1000, 1000), apiKey, logger: logger); +shockers = openShockHttp.GetShockers(); +openShockHttp.Control(ControlAction.Vibrate, 20, 1000, shockers.First()); + +File.WriteAllText("devices.json", JsonConvert.SerializeObject(openShockHttp)); +OpenShockHttp deserialized = JsonConvert.DeserializeObject(File.ReadAllText("devices.json"))!; +Thread.Sleep(1100); //Wait for previous to end +deserialized.Control(ControlAction.Vibrate, 20, 1000, shockers.First()); +openShockHttp.Dispose(); +deserialized.Dispose(); + + +/* #pragma warning disable CA1416 -List serialPorts = SerialDevice.GetSerialPorts(); +List serialPorts = SerialHelper.GetSerialPorts(); if (serialPorts.Count < 1) return; @@ -17,13 +41,23 @@ for(int i = 0; i < serialPorts.Count; i++) Console.WriteLine($"Select Serial Port [0-{serialPorts.Count-1}]:"); string? selectedPortStr = Console.ReadLine(); -int selectedPort = -1; +int selectedPort; while (!int.TryParse(selectedPortStr, out selectedPort) || selectedPort < 0 || selectedPort > serialPorts.Count - 1) { Console.WriteLine($"Select Serial Port [0-{serialPorts.Count-1}]:"); selectedPortStr = Console.ReadLine(); } -OpenShockSerial shockSerial = new (new Dictionary(), new IntensityRange(30,50), new DurationRange(1000,1000), serialPorts[selectedPort], logger); -Dictionary shockers = shockSerial.GetShockers("https://api.shocklink.net", "LOLAPIKEY"); -Console.ReadKey(); \ No newline at end of file +OpenShockSerial openShockSerial = new(new IntensityRange(30, 50), new DurationRange(1000, 1000),serialPorts[selectedPort], apiKey, logger: logger); +shockers = openShockSerial.GetShockers(); +openShockSerial.Control(ControlAction.Vibrate, 20, 1000, shockers.First()); +File.WriteAllText("devices.json", JsonConvert.SerializeObject(openShockSerial)); +OpenShockHttp deserialized = JsonConvert.DeserializeObject(File.ReadAllText("devices.json"))!; +openShockSerial.Dispose(); +deserialized.Dispose(); +*/ + +foreach(OpenShockShocker s in shockers) + Console.Write(s); +File.WriteAllText("shockers.json", JsonConvert.SerializeObject(shockers)); +List deserializedShockers = JsonConvert.DeserializeObject>(File.ReadAllText("shockers.json"), new ShockerJsonConverter())!; \ No newline at end of file