From ac19e20fb7919b980361d2f14bf0b9040991c88c Mon Sep 17 00:00:00 2001 From: glax Date: Mon, 29 Jan 2024 15:37:19 +0100 Subject: [PATCH] Fix OpenShockHttp: Wrong json caused Bad Request Get OpenShock Shockers from API. Save Shockers for PiShock and OpenShock in different structs Implement Action Queue, to avoid synchronous actions getting lost. Moved SerialPortInfo to own file Created ShockerJsonConverter Better separation of Devices/APIs and Shockers --- CShocker/CShocker.csproj | 2 +- CShocker/Devices/Abstract/Device.cs | 77 +++++++++++++ CShocker/Devices/Abstract/OpenShockDevice.cs | 72 ++++++++++++ CShocker/Devices/Abstract/PiShockDevice.cs | 12 ++ CShocker/Devices/Abstract/SerialPortInfo.cs | 20 ++++ .../Additional/ControlActionEnum.cs} | 2 +- .../Additional/DeviceApiEnum.cs} | 4 +- .../Additional/DeviceJsonConverter.cs} | 29 +++-- .../Additional/SerialHelper.cs} | 38 +------ CShocker/Devices/OpenShockHttp.cs | 64 +++++++++++ CShocker/Devices/OpenShockSerial.cs | 51 +++++++++ CShocker/Devices/PiShockHttp.cs | 65 +++++++++++ .../APIS => Devices}/PiShockSerial.cs | 22 ++-- CShocker/Shockers/APIS/OpenShockHttp.cs | 103 ------------------ CShocker/Shockers/APIS/OpenShockSerial.cs | 98 ----------------- CShocker/Shockers/APIS/PiShockHttp.cs | 53 --------- CShocker/Shockers/Abstract/HttpShocker.cs | 25 ----- CShocker/Shockers/Abstract/Shocker.cs | 52 +-------- .../Additional/ShockerJsonConverter.cs | 48 ++++++++ CShocker/Shockers/OpenShockShocker.cs | 32 ++++++ CShocker/Shockers/PiShockShocker.cs | 8 ++ TestApp/Program.cs | 52 +++++++-- 22 files changed, 530 insertions(+), 399 deletions(-) create mode 100644 CShocker/Devices/Abstract/Device.cs create mode 100644 CShocker/Devices/Abstract/OpenShockDevice.cs create mode 100644 CShocker/Devices/Abstract/PiShockDevice.cs create mode 100644 CShocker/Devices/Abstract/SerialPortInfo.cs rename CShocker/{Shockers/ControlAction.cs => Devices/Additional/ControlActionEnum.cs} (64%) rename CShocker/{Shockers/Abstract/ShockerApi.cs => Devices/Additional/DeviceApiEnum.cs} (57%) rename CShocker/{Shockers/ShockerJsonConverter.cs => Devices/Additional/DeviceJsonConverter.cs} (68%) rename CShocker/{Shockers/Abstract/SerialShocker.cs => Devices/Additional/SerialHelper.cs} (55%) create mode 100644 CShocker/Devices/OpenShockHttp.cs create mode 100644 CShocker/Devices/OpenShockSerial.cs create mode 100644 CShocker/Devices/PiShockHttp.cs rename CShocker/{Shockers/APIS => Devices}/PiShockSerial.cs (56%) delete mode 100644 CShocker/Shockers/APIS/OpenShockHttp.cs delete mode 100644 CShocker/Shockers/APIS/OpenShockSerial.cs delete mode 100644 CShocker/Shockers/APIS/PiShockHttp.cs delete mode 100644 CShocker/Shockers/Abstract/HttpShocker.cs create mode 100644 CShocker/Shockers/Additional/ShockerJsonConverter.cs create mode 100644 CShocker/Shockers/OpenShockShocker.cs create mode 100644 CShocker/Shockers/PiShockShocker.cs 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