Compare commits

...

59 Commits

Author SHA1 Message Date
34fb4d89bd Fix Default endpoint for openshock 2025-01-16 22:40:49 +01:00
828a1a2bfa Test program 2025-01-16 22:36:57 +01:00
0b98638cd5 Less CPU usage by creating tasks per action, instead of thread 2025-01-16 22:36:52 +01:00
74d30ba1b9 Fix Openshock default endpoint 2025-01-16 22:36:24 +01:00
385dba1cf3 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	CShocker/Devices/Additional/ApiHttpClient.cs
2025-01-16 21:47:48 +01:00
1ff67a5b8b Fix Assembly string empty on Publish 2025-01-16 21:46:13 +01:00
c55592cd90 Fix Assembly string empty on Publish 2025-01-16 21:38:30 +01:00
b497117b62 Change OpenShock default Endpoint 2025-01-16 20:56:34 +01:00
73c19a3f8f v3.0 Upgrade to .net9.0 2025-01-16 20:55:24 +01:00
318598f62a 2.5.1 Make HTTP Default Endpoint public 2024-11-03 01:15:26 +01:00
cf0a7c630d Change Program.cs in testapp 2024-11-03 01:11:40 +01:00
ce0a287e4e 2.5.0
Update DefaultEndpoint
Update ControlActionEnum.cs
2024-11-03 01:11:33 +01:00
7693aa8b09 2.4.2 2024-11-03 00:35:59 +01:00
746e153cb5 Fix ShockerJsonConverter 2024-02-15 23:11:56 +01:00
60746d66df Version 2024-02-12 03:07:18 +01:00
eebf15804a ToString fancy 2024-02-12 03:06:28 +01:00
2c7da0352b Fancier output 2024-02-12 02:05:22 +01:00
17206bfb8c Let Constructor throw Exception instead of method 2024-02-12 02:05:13 +01:00
4839c12340 Add RandomValueWithinLimits to IntegerRange 2024-02-12 02:04:58 +01:00
1eed03ac14 GetHashcode and Equals usage 2024-02-12 02:04:45 +01:00
f6e8ddb91d ToString more conventional 2024-02-12 02:04:09 +01:00
8d70879c68 Capitalized FieldNames onn public fields 2024-02-12 02:03:35 +01:00
639b813fb6 Dependency from Nuget 2024-02-12 02:02:45 +01:00
171b057d58 Renamed to IsValueWithinLimits 2024-02-12 02:02:30 +01:00
bb8959160a Mark exposed fields and methods 2024-02-12 02:02:10 +01:00
cbca4fd7de ValidIntensityRange and ValidDurationRange internal readonly 2024-02-11 23:24:25 +01:00
4e79f40e80 Update Readme 2024-02-11 22:58:53 +01:00
978b384759 Create ApiHttpClient
Use Useragent from Assembly.
2024-02-11 22:57:35 +01:00
b56cfa39aa Cleanup 2024-02-11 22:56:55 +01:00
c14279fbbe Remove random Integer class.
Add check if value for intensity and duration is within accepted range.
2024-02-11 22:24:53 +01:00
194e54fd9a renamed variable to workONqueue 2024-02-11 22:11:35 +01:00
8a640491cb Fix Converters 2024-02-04 17:47:36 +01:00
f593efda62 UpdateReadme 2024-02-04 16:26:10 +01:00
c603fe0ccb Fix unecessary parameters in OpenShockApi.GetShockers(); 2024-02-04 16:25:15 +01:00
f939e79e69 Update Readme 2024-02-01 23:07:48 +01:00
65059f66ed Rewrite Hierachy that shockers now contain the api they use. 2024-02-01 23:03:28 +01:00
9596811ae7 Hashcodes 2024-01-29 17:23:03 +01:00
961dcd29d1 Equal overrides 2024-01-29 17:20:32 +01:00
90f804c7c6 Equal overrides 2024-01-29 17:05:13 +01:00
60c1ece41d redundant declaration 2024-01-29 15:37:43 +01:00
ac19e20fb7 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
2024-01-29 15:37:19 +01:00
e255caeb64 LOL APIKEY 2024-01-28 19:26:42 +01:00
8dc8af3792 Logging 2024-01-28 18:35:37 +01:00
dea3268cea Add more logging 2024-01-28 18:30:38 +01:00
ed4eb644a0 Fi xjson message string 2024-01-28 18:25:33 +01:00
4b19a9f434 Fix wrong api bug 2024-01-28 17:57:53 +01:00
38f7fe1aa3 nuget version 2024-01-20 21:31:31 +01:00
ad93d36d8d Revert "Overwrite ShockerIDs for OpenShockSerial"
This reverts commit a6040f2e5c.
2024-01-20 21:31:10 +01:00
359607879a Update Readme 2024-01-20 21:21:54 +01:00
a6040f2e5c Overwrite ShockerIDs for OpenShockSerial 2024-01-20 21:16:55 +01:00
1daf9a175e Use Http-API to get RF-IDs for OpenShockSerial 2024-01-20 21:15:26 +01:00
67bb9c4f9a JsonConverter 2024-01-20 20:27:50 +01:00
2d47aa7492 Formatting 2024-01-20 20:25:31 +01:00
d861c95ec3 dictionary 2024-01-20 20:21:27 +01:00
cab3536412 moved namespace 2024-01-20 20:21:23 +01:00
5de7e3c0ce wrong serial command 2024-01-20 20:19:25 +01:00
9bcfd75aa2 nullable 2024-01-20 20:04:10 +01:00
076b6fc271 Add TestApp 2024-01-20 20:02:12 +01:00
610518e5b5 Add OpenShockSerial 2024-01-20 20:02:05 +01:00
36 changed files with 887 additions and 394 deletions

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="LocationsWithSilentDeleteHolder">
<option name="locations">
<list>
<option value="$PROJECT_DIR$/CShocker/bin/Release/net9.0/win-x64" />
<option value="$PROJECT_DIR$/CShocker/bin/Release/net9.0/linux-x64/publish" />
<option value="$PROJECT_DIR$/CShocker/bin/Release/net9.0/win-x64/publish" />
</list>
</option>
</component>
</project>

View File

@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CShocker", "CShocker\CShocker.csproj", "{244585F2-3AD8-4B3A-B6DA-3B0D3EFB745F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CShocker", "CShocker\CShocker.csproj", "{244585F2-3AD8-4B3A-B6DA-3B0D3EFB745F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "TestApp\TestApp.csproj", "{3C3879F1-AB10-41C7-BF1A-3C1DA850970B}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -12,5 +14,9 @@ Global
{244585F2-3AD8-4B3A-B6DA-3B0D3EFB745F}.Debug|Any CPU.Build.0 = Debug|Any CPU {244585F2-3AD8-4B3A-B6DA-3B0D3EFB745F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{244585F2-3AD8-4B3A-B6DA-3B0D3EFB745F}.Release|Any CPU.ActiveCfg = Release|Any CPU {244585F2-3AD8-4B3A-B6DA-3B0D3EFB745F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{244585F2-3AD8-4B3A-B6DA-3B0D3EFB745F}.Release|Any CPU.Build.0 = Release|Any CPU {244585F2-3AD8-4B3A-B6DA-3B0D3EFB745F}.Release|Any CPU.Build.0 = Release|Any CPU
{3C3879F1-AB10-41C7-BF1A-3C1DA850970B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3C3879F1-AB10-41C7-BF1A-3C1DA850970B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C3879F1-AB10-41C7-BF1A-3C1DA850970B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C3879F1-AB10-41C7-BF1A-3C1DA850970B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

5
CShocker.sln.DotSettings Normal file
View File

@ -0,0 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Caixianlin/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Petrainer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rftransmit/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Xianlin/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -0,0 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=C_003A_005CUsers_005CGlax_005CRiderProjects_005CGlaxLogger_005CGlaxLogger_005Cbin_005CDebug_005Cnet7_002E0_005CGlaxLogger_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Users\Glax\RiderProjects\GlaxLogger\GlaxLogger\bin\Debug\net7.0\GlaxLogger.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String></wpf:ResourceDictionary>

View File

@ -1,18 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Authors>Glax</Authors> <Authors>Glax</Authors>
<RepositoryUrl>https://github.com/C9Glax/CShocker</RepositoryUrl> <RepositoryUrl>https://github.com/C9Glax/CShocker</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<Version>1.2.5</Version> <Version>3.1.0</Version>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<LangVersion>latestmajor</LangVersion>
<PackageProjectUrl>https://github.com/C9Glax/CShocker</PackageProjectUrl>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IO.Ports" Version="9.0.1" />
<PackageReference Include="System.Management" Version="9.0.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

@ -0,0 +1,37 @@
using CShocker.Devices.Abstract;
using CShocker.Devices.Additional;
using CShocker.Shockers;
using CShocker.Shockers.Abstract;
using Microsoft.Extensions.Logging;
namespace CShocker.Devices.APIs;
public class OpenShockHttp : OpenShockApi
{
protected override void ControlInternal(ControlAction action, Shocker 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 = "{" +
" \"shocks\": [" +
" {" +
$" \"id\": \"{openShockShocker.ID}\"," +
$" \"type\": \"{Enum.GetName(action)}\"," +
$" \"intensity\": {intensity}," +
$" \"duration\": {duration}" +
" }" +
" ]," +
" \"customName\": \"CShocker\"" +
"}";
ApiHttpClient.MakeAPICall(HttpMethod.Post, $"{Endpoint}/2/shockers/control", json, this.Logger, new ValueTuple<string, string>("OpenShockToken", ApiKey));
}
public OpenShockHttp(string apiKey, string? endpoint = null, ILogger? logger = null) : base(DeviceApi.OpenShockHttp, apiKey, endpoint??DefaultEndpoint, logger)
{
}
}

View File

@ -0,0 +1,58 @@
using System.IO.Ports;
using CShocker.Devices.Abstract;
using CShocker.Devices.Additional;
using CShocker.Shockers;
using CShocker.Shockers.Abstract;
using Microsoft.Extensions.Logging;
namespace CShocker.Devices.APIs;
public class OpenShockSerial : OpenShockApi
{
private const int BaudRate = 115200;
public SerialPortInfo SerialPortI;
private readonly SerialPort _serialPort;
public OpenShockSerial(SerialPortInfo serialPortI, string apiKey, string? endpoint = null, ILogger? logger = null) : base(DeviceApi.OpenShockSerial, apiKey, endpoint??DefaultEndpoint, logger)
{
this.SerialPortI = serialPortI;
this._serialPort = new SerialPort(serialPortI.PortName, BaudRate);
try
{
this._serialPort.Open();
}
catch (Exception e)
{
this.Logger?.Log(LogLevel.Error, e.Message);
throw;
}
}
protected override void ControlInternal(ControlAction action, Shocker 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"
};
}
}

View File

@ -0,0 +1,52 @@
using CShocker.Devices.Abstract;
using CShocker.Devices.Additional;
using CShocker.Shockers;
using CShocker.Shockers.Abstract;
using Microsoft.Extensions.Logging;
namespace CShocker.Devices.APIs;
public class PiShockHttp : PiShockApi
{
// ReSharper disable thrice MemberCanBePrivate.Global -> Exposed
public readonly string Username, Endpoint, ApiKey;
public PiShockHttp(string apiKey, string username, string endpoint = "https://do.pishock.com/api/apioperate", ILogger? logger = null) : base(DeviceApi.PiShockHttp, logger)
{
this.Username = username;
this.Endpoint = endpoint;
this.ApiKey = apiKey;
}
protected override void ControlInternal(ControlAction action, Shocker shocker, int intensity, int duration)
{
if (shocker is not PiShockShocker piShockShocker)
{
this.Logger?.Log(LogLevel.Warning, $"Shocker {shocker} is not {typeof(OpenShockShocker).FullName}");
return;
}
string json = "{" +
$"\"Username\":\"{Username}\"," +
"\"Name\":\"CShocker\"," +
$"\"Code\":\"{piShockShocker.Code}\"," +
$"\"Intensity\":\"{intensity}\"," +
$"\"Duration\":\"{duration / 1000}\"," + //duration is in seconds no ms
$"\"Apikey\":\"{ApiKey}\"," +
$"\"Op\":\"{ControlActionToByte(action)}\"" +
"}";
ApiHttpClient.MakeAPICall(HttpMethod.Post, $"{Endpoint}", json, this.Logger);
}
private byte ControlActionToByte(ControlAction action)
{
return action switch
{
ControlAction.Beep => 2,
ControlAction.Vibrate => 1,
ControlAction.Shock => 0,
_ => 2
};
}
}

View File

@ -0,0 +1,47 @@
using System.IO.Ports;
using CShocker.Devices.Abstract;
using CShocker.Devices.Additional;
using CShocker.Shockers.Abstract;
using Microsoft.Extensions.Logging;
namespace CShocker.Devices.APIs;
public class PiShockSerial : PiShockApi
{
private const int BaudRate = 115200;
// ReSharper disable once MemberCanBePrivate.Global -> Exposed
public readonly SerialPortInfo SerialPortI;
private readonly SerialPort _serialPort;
public PiShockSerial(DeviceApi apiType, SerialPortInfo serialPortI, ILogger? logger = null) : base(apiType, logger)
{
this.SerialPortI = serialPortI;
this._serialPort = new SerialPort(this.SerialPortI.PortName, BaudRate);
throw new NotImplementedException();
}
protected override void ControlInternal(ControlAction action, Shocker shocker, int intensity, int duration)
{
string json = "{" +
"\"cmd\": \"operate\"," +
"\"value\":{" +
$"\"op\": \"{ControlActionToOp(action)}\"," +
$"\"duration\": {duration}," +
$"\"intensity\": {intensity}," +
$"\"id\": " +
"}" +
"}";
_serialPort.WriteLine(json);
}
private static string ControlActionToOp(ControlAction action)
{
return action switch
{
ControlAction.Beep => "",
ControlAction.Vibrate => "vibrate",
ControlAction.Shock => "",
_ => ""
};
}
}

View File

@ -0,0 +1,100 @@
using CShocker.Devices.Additional;
using CShocker.Ranges;
using CShocker.Shockers.Abstract;
using Microsoft.Extensions.Logging;
namespace CShocker.Devices.Abstract;
public abstract class Api : IDisposable
{
// ReSharper disable 4 times MemberCanBePrivate.Global -> Exposed
protected ILogger? Logger;
public readonly DeviceApi ApiType;
private Queue<DateTime> order = new();
private Dictionary<DateTime, Task> tasks = new();
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private const short CommandDelay = 50;
internal readonly IntegerRange ValidIntensityRange, ValidDurationRange;
internal void Control(ControlAction action, int intensity, int duration, params Shocker[] shockers)
{
bool enqueueItem = true;
if (!ValidIntensityRange.IsValueWithinLimits(intensity))
{
this.Logger?.Log(LogLevel.Information, $"Value not within allowed {nameof(intensity)}-Range ({ValidIntensityRange.RangeString()}): {intensity}");
enqueueItem = false;
}
if (!ValidDurationRange.IsValueWithinLimits(duration))
{
this.Logger?.Log(LogLevel.Information, $"Value not within allowed {nameof(duration)}-Range ({ValidIntensityRange.RangeString()}): {duration}");
enqueueItem = false;
}
if (!enqueueItem)
{
this.Logger?.Log(LogLevel.Information, "Doing nothing.");
return;
}
foreach (Shocker shocker in shockers)
{
this.Logger?.Log(LogLevel.Debug, $"Enqueueing {action} Intensity: {intensity} Duration: {duration}\nShocker:\n{shocker}");
ValueTuple<ControlAction, Shocker, int, int> tuple = new(action, shocker, intensity, duration);
DateTime now = DateTime.Now;
Task t = new (() => ExecuteTask(now, tuple));
order.Enqueue(now);
tasks.Add(now, t);
t.Start();
}
}
protected abstract void ControlInternal(ControlAction action, Shocker shocker, int intensity, int duration);
protected Api(DeviceApi apiType, IntegerRange validIntensityRange, IntegerRange validDurationRange, ILogger? logger = null)
{
this.ApiType = apiType;
this.Logger = logger;
this.ValidIntensityRange = validIntensityRange;
this.ValidDurationRange = validDurationRange;
}
private void ExecuteTask(DateTime when, ValueTuple<ControlAction, Shocker, int, int> tuple)
{
while (order.First() != when)
Thread.Sleep(CommandDelay);
this.Logger?.Log(LogLevel.Information, $"Executing: {Enum.GetName(tuple.Item1)} Intensity: {tuple.Item3} Duration: {tuple.Item4}\nShocker:\n{tuple.Item2}");
ControlInternal(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4);
Thread.Sleep(tuple.Item4);
tasks.Remove(when);
order.Dequeue();
}
public void SetLogger(ILogger? logger)
{
this.Logger = logger;
}
public override string ToString()
{
return $"ShockerType: {Enum.GetName(typeof(DeviceApi), this.ApiType)}\n\r";
}
public override bool Equals(object? obj)
{
return obj is Api d && Equals(d);
}
protected bool Equals(Api other)
{
return ApiType == other.ApiType;
}
public override int GetHashCode()
{
return ApiType.GetHashCode();
}
public void Dispose()
{
foreach ((DateTime when, Task? task) in tasks)
task?.Dispose();
}
}

View File

@ -0,0 +1,79 @@
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 OpenShockApi : Api
{
// ReSharper disable twice MemberCanBeProtected.Global -> Exposed
public string Endpoint { get; init; }
public string ApiKey { get; init; }
public const string DefaultEndpoint = "https://api.openshock.app";
// ReSharper disable once PublicConstructorInAbstractClass -> Exposed
public OpenShockApi(DeviceApi apiType, string apiKey, string endpoint = DefaultEndpoint, ILogger? logger = null) : base(apiType, new IntegerRange(0, 100), new IntegerRange(300, 30000), logger)
{
this.Endpoint = endpoint;
this.ApiKey = apiKey;
}
public override bool Equals(object? obj)
{
return obj is OpenShockApi osd && Equals(osd);
}
private bool Equals(OpenShockApi other)
{
return base.Equals(other) && Endpoint.Equals(other.Endpoint) && ApiKey.Equals(other.ApiKey);
}
public override int GetHashCode()
{
return HashCode.Combine(Endpoint, ApiKey);
}
public IEnumerable<OpenShockShocker> GetShockers()
{
return GetShockers(this.ApiKey, this, this.Endpoint, this.Logger);
}
// ReSharper disable once MemberCanBePrivate.Global
public static IEnumerable<OpenShockShocker> GetShockers(string apiKey, OpenShockApi api, string apiEndpoint = DefaultEndpoint, ILogger? logger = null)
{
List<OpenShockShocker> shockers = new();
HttpResponseMessage ownResponse = ApiHttpClient.MakeAPICall(HttpMethod.Get, $"{apiEndpoint}/1/shockers/own", null, logger, new ValueTuple<string, string>("OpenShockToken", apiKey));
if (!ownResponse.IsSuccessStatusCode)
return shockers;
StreamReader ownShockerStreamReader = new(ownResponse.Content.ReadAsStream());
string ownShockerJson = ownShockerStreamReader.ReadToEnd();
logger?.Log(LogLevel.Debug,ownShockerJson);
JObject ownShockerListJObj = JObject.Parse(ownShockerJson);
shockers.AddRange(ownShockerListJObj.SelectTokens("$.data..shockers[*]").Select(t =>
new OpenShockShocker(
api,
t["name"]!.Value<string>()!, t["id"]!.Value<string>()!, t["rfId"]!.Value<short>(),
Enum.Parse<OpenShockShocker.OpenShockModel>(t["model"]!.Value<string>()!),
t["createdOn"]!.ToObject<DateTime>(), t["isPaused"]!.Value<bool>())));
HttpResponseMessage sharedResponse = ApiHttpClient.MakeAPICall(HttpMethod.Get, $"{apiEndpoint}/1/shockers/shared", null, logger, new ValueTuple<string, string>("OpenShockToken", apiKey));
if (!sharedResponse.IsSuccessStatusCode)
return shockers;
StreamReader sharedShockerStreamReader = new(sharedResponse.Content.ReadAsStream());
string sharedShockerJson = sharedShockerStreamReader.ReadToEnd();
logger?.Log(LogLevel.Debug, sharedShockerJson);
JObject sharedShockerListJObj = JObject.Parse(sharedShockerJson);
shockers.AddRange(sharedShockerListJObj.SelectTokens("$.data..shockers[*]").Select(t =>
new OpenShockShocker(
api,
t["name"]!.Value<string>()!, t["id"]!.Value<string>()!, t["rfId"]!.Value<short>(),
Enum.Parse<OpenShockShocker.OpenShockModel>(t["model"]!.Value<string>()!),
t["createdOn"]!.ToObject<DateTime>(), t["isPaused"]!.Value<bool>())));
return shockers;
}
}

View File

@ -0,0 +1,12 @@
using CShocker.Devices.Additional;
using CShocker.Ranges;
using Microsoft.Extensions.Logging;
namespace CShocker.Devices.Abstract;
public abstract class PiShockApi : Api
{
protected PiShockApi(DeviceApi apiType, ILogger? logger = null) : base(apiType, new IntegerRange(0, 100), new IntegerRange(1000, 15000), logger)
{
}
}

View File

@ -0,0 +1,26 @@
namespace CShocker.Devices.Abstract;
public readonly struct SerialPortInfo
{
// ReSharper disable thrice MemberCanBePrivate.Global -> Exposed
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 $"{string.Join("\n\t",
$"{GetType().Name}",
$"PortName: {PortName}",
$"Description: {Description}",
$"Manufacturer: {Manufacturer}",
$"DeviceID: {DeviceID}")}" +
$"\n\r";
}
}

View File

@ -0,0 +1,66 @@
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using Microsoft.Extensions.Logging;
namespace CShocker.Devices.Additional;
public static class ApiHttpClient
{
private static readonly ProductInfoHeaderValue UserAgent = GetUserAgent();
private static ProductInfoHeaderValue GetUserAgent()
{
Assembly assembly = Assembly.GetExecutingAssembly();
FileVersionInfo fvi;
if (assembly.Location == String.Empty)
{
DirectoryInfo dir = new (AppContext.BaseDirectory);
FileInfo? f = dir.GetFiles("*.exe").FirstOrDefault();
if (f is null)
return new("CShocker", "Release");
fvi = FileVersionInfo.GetVersionInfo(f.FullName);
}
else
{
fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
}
return new (fvi.ProductName ?? fvi.FileName, fvi.ProductVersion);
}
internal static HttpResponseMessage MakeAPICall(HttpMethod method, string uri, string? jsonContent, ILogger? logger = null, params ValueTuple<string, string>[] customHeaders)
{
HttpRequestMessage request = new (method, uri)
{
Headers =
{
UserAgent = { UserAgent },
Accept = { new MediaTypeWithQualityHeaderValue("application/json") },
}
};
if (jsonContent is not null && jsonContent.Length > 0)
request.Content = new ByteArrayContent(Encoding.UTF8.GetBytes(jsonContent))
{
Headers = { ContentType = MediaTypeHeaderValue.Parse("application/json") }
};
foreach ((string, string) customHeader in customHeaders)
request.Headers.Add(customHeader.Item1, customHeader.Item2);
logger?.Log(LogLevel.Debug, string.Join("\n\t",
"Request:",
$"\u251c\u2500\u2500 URI: {request.RequestUri}",
$"\u251c\u2500\u2510 Headers: {string.Concat(request.Headers.Select(h => $"\n\t\u2502 {(request.Headers.Last().Key.Equals(h.Key) ? "\u2514" : "\u251c")} {h.Key}: {string.Join(", ", h.Value)}"))}",
$"\u2514\u2500\u2500 Content: {request.Content?.ReadAsStringAsync().Result}"));
HttpClient httpClient = new();
HttpResponseMessage response = httpClient.Send(request);
logger?.Log(!response.IsSuccessStatusCode ? LogLevel.Error : LogLevel.Debug, string.Join("\n\t",
"Response:",
$"\u251c\u2500\u2500 URI: {request.RequestUri}",
$"\u251c\u2500\u2510 Headers: {string.Concat(response.Headers.Select(h => $"\n\t\u2502 {(response.Headers.Last().Key.Equals(h.Key) ? "\u2514" : "\u251c")} {h.Key}: {string.Join(", ", h.Value)}"))}",
$"\u2514\u2500\u2500 Content: {response.Content.ReadAsStringAsync().Result}"));
httpClient.Dispose();
return response;
}
}

View File

@ -0,0 +1,44 @@
using CShocker.Devices.Abstract;
using CShocker.Devices.APIs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CShocker.Devices.Additional;
public class ApiJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(Api));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
DeviceApi apiType = (DeviceApi)jo.SelectToken("ApiType")!.Value<byte>();
switch (apiType)
{
case DeviceApi.OpenShockHttp:
return jo.ToObject<OpenShockHttp>()!;
case DeviceApi.OpenShockSerial:
return jo.ToObject<OpenShockSerial>()!;
case DeviceApi.PiShockHttp:
return jo.ToObject<PiShockHttp>()!;
case DeviceApi.PiShockSerial:
return jo.ToObject<PiShockSerial>()!;
default:
throw new Exception();
}
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}

View File

@ -1,9 +1,9 @@
namespace CShocker.Shockers; namespace CShocker.Devices.Additional;
public enum ControlAction public enum ControlAction
{ {
Beep, Beep,
Vibrate, Vibrate,
Shock, Shock,
Nothing Stop
} }

View File

@ -1,6 +1,6 @@
namespace CShocker.Shockers.Abstract; namespace CShocker.Devices.Additional;
public enum ShockerApi : byte public enum DeviceApi : byte
{ {
OpenShockHttp = 0, OpenShockHttp = 0,
OpenShockSerial = 1, OpenShockSerial = 1,

View File

@ -0,0 +1,47 @@
using System.Management;
using System.Runtime.Versioning;
using CShocker.Devices.Abstract;
using Microsoft.Win32;
namespace CShocker.Devices.Additional;
public static class SerialHelper
{
[SupportedOSPlatform("windows")]
public static List<SerialPortInfo> GetSerialPorts()
{
List<SerialPortInfo> ret = new();
using (ManagementClass entity = new("Win32_PnPEntity"))
{
// ReSharper disable once InconsistentNaming
const string CUR_CTRL = "HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\";
foreach (ManagementObject instance in entity.GetInstances())
{
object oGuid;
oGuid = instance.GetPropertyValue("ClassGuid");
if (oGuid == null || oGuid.ToString()?.ToUpper().Equals("{4D36E978-E325-11CE-BFC1-08002BE10318}") is false)
continue; // Skip all devices except device class "PORTS"
string? caption = instance.GetPropertyValue("Caption")?.ToString();
string? manufacturer = instance.GetPropertyValue("Manufacturer")?.ToString();
string? deviceID = instance.GetPropertyValue("PnpDeviceID")?.ToString();
string regEnum = CUR_CTRL + "Enum\\" + deviceID + "\\Device Parameters";
string? portName = Registry.GetValue(regEnum, "PortName", "")?.ToString();
int? s32Pos = caption?.IndexOf(" (COM");
if (s32Pos > 0) // remove COM port from description
caption = caption?.Substring(0, (int)s32Pos);
ret.Add(new SerialPortInfo(
portName,
caption,
manufacturer,
deviceID));
}
}
return ret;
}
}

View File

@ -1,9 +0,0 @@
namespace CShocker.Ranges;
public class DurationRange : RandomIntegerRange
{
public DurationRange(short min, short max) : base(min ,max , 0, 30000)
{
}
}

View File

@ -0,0 +1,28 @@
namespace CShocker.Ranges;
public readonly struct IntegerRange
{
// ReSharper disable twice MemberCanBePrivate.Global -> Exposed
public readonly int Min, Max;
public IntegerRange(int min, int max)
{
this.Min = min;
this.Max = max;
}
public bool IsValueWithinLimits(int value)
{
return value >= this.Min && value <= this.Max;
}
public int RandomValueWithinLimits()
{
return Random.Shared.Next(this.Min, this.Max);
}
internal string RangeString()
{
return $"{this.Min}-{this.Max}";
}
}

View File

@ -1,9 +0,0 @@
namespace CShocker.Ranges;
public class IntensityRange : RandomIntegerRange
{
public IntensityRange(short min, short max) : base(min , max, 0, 100)
{
}
}

View File

@ -1,27 +0,0 @@
namespace CShocker.Ranges;
public abstract class RandomIntegerRange
{
public short Min, Max;
internal RandomIntegerRange(short min, short max, short minLimit, short maxLimit)
{
if (max - min < 0)
throw new ArgumentException("Min has to be less or equal Max");
if (min < minLimit || min > maxLimit)
throw new ArgumentOutOfRangeException(nameof(min), $"Min has to be withing Range {minLimit}-{maxLimit}");
if (max < minLimit || max > maxLimit)
throw new ArgumentOutOfRangeException(nameof(max), $"Max has to be withing Range {minLimit}-{maxLimit}");
this.Min = min;
this.Max = max;
}
public int GetRandomRangeValue()
{
return Random.Shared.Next(this.Min, this.Max);
}
public override string ToString()
{
return $"Min: {Min} Max: {Max}";
}
}

View File

@ -1,96 +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<string> 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);
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<string> deviceIds = new();
deviceIds.AddRange(deviceListJObj["data"]!.Children()["id"].Values<string>()!);
List<string> 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);
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);
shockerIds.AddRange(shockerListJObj["data"]!.Children()["id"].Values<string>()!);
}
return shockerIds;
}
protected override void ControlInternal(ControlAction action, string shockerId, int intensity, int duration)
{
HttpRequestMessage request = new (HttpMethod.Post, $"{Endpoint}/1/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);
HttpResponseMessage response = HttpClient.Send(request);
this.Logger?.Log(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<string> 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)
{
}
}

View File

@ -1,18 +0,0 @@
using CShocker.Ranges;
using CShocker.Shockers.Abstract;
using Microsoft.Extensions.Logging;
namespace CShocker.Shockers.APIS;
public class OpenShockSerial : SerialShocker
{
public OpenShockSerial(List<string> shockerIds, IntensityRange intensityRange, DurationRange durationRange, ILogger? logger = null) : base(shockerIds, intensityRange, durationRange, ShockerApi.OpenShockSerial, logger)
{
throw new NotImplementedException();
}
protected override void ControlInternal(ControlAction action, string shockerId, int intensity, int duration)
{
throw new NotImplementedException();
}
}

View File

@ -1,51 +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
{
public String Username, ShareCode;
public PiShockHttp(List<string> 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
};
}
}

View File

@ -1,18 +0,0 @@
using CShocker.Ranges;
using CShocker.Shockers.Abstract;
using Microsoft.Extensions.Logging;
namespace CShocker.Shockers.APIS;
public class PiShockSerial : SerialShocker
{
public PiShockSerial(List<string> shockerIds, IntensityRange intensityRange, DurationRange durationRange, ILogger? logger = null) : base(shockerIds, intensityRange, durationRange, ShockerApi.PiShockSerial, logger)
{
throw new NotImplementedException();
}
protected override void ControlInternal(ControlAction action, string shockerId, int intensity, int duration)
{
throw new NotImplementedException();
}
}

View File

@ -1,24 +0,0 @@
using CShocker.Ranges;
using Microsoft.Extensions.Logging;
namespace CShocker.Shockers.Abstract;
public abstract class HttpShocker : Shocker
{
protected readonly HttpClient HttpClient = new();
public string Endpoint { get; init; }
public string ApiKey { get; init; }
protected HttpShocker(List<string> 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}";
}
}

View File

@ -1,11 +0,0 @@
using CShocker.Ranges;
using Microsoft.Extensions.Logging;
namespace CShocker.Shockers.Abstract;
public abstract class SerialShocker : Shocker
{
protected SerialShocker(List<string> shockerIds, IntensityRange intensityRange, DurationRange durationRange, ShockerApi apiType, ILogger? logger = null) : base(shockerIds, intensityRange, durationRange, apiType, logger)
{
}
}

View File

@ -1,52 +1,25 @@
using System.Reflection.Metadata; using CShocker.Devices.Abstract;
using CShocker.Ranges; using CShocker.Devices.Additional;
using Microsoft.Extensions.Logging;
namespace CShocker.Shockers.Abstract; namespace CShocker.Shockers.Abstract;
public abstract class Shocker public abstract class Shocker : IDisposable
{ {
public readonly List<string> ShockerIds; // ReSharper disable once MemberCanBePrivate.Global -> Exposed
public readonly IntensityRange IntensityRange; public Api Api { get; }
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<string> shockerIds, IntensityRange intensityRange, DurationRange durationRange, ShockerApi apiType, ILogger? logger = null) internal Shocker(Api api)
{ {
this.ShockerIds = shockerIds; this.Api = api;
this.IntensityRange = intensityRange;
this.DurationRange = durationRange;
this.ApiType = apiType;
this.Logger = logger;
} }
public void SetLogger(ILogger? logger) public void Control(ControlAction action, int intensity, int duration)
{ {
this.Logger = logger; this.Api.Control(action, intensity, duration, this);
} }
public override string ToString() public void Dispose()
{ {
return $"ShockerType: {Enum.GetName(typeof(ShockerApi), this.ApiType)}\n" + Api.Dispose();
$"Shocker-IDs: {string.Join(", ", this.ShockerIds)}\n" +
$"IntensityRange: {IntensityRange}\n" +
$"DurationRange: {DurationRange}";
} }
} }

View File

@ -0,0 +1,36 @@
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(Shocker));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo.ContainsKey("Model")) //OpenShockShocker
{
return jo.ToObject<OpenShockShocker>(serializer)!;
}
else //PiShockShocker
{
return jo.ToObject<PiShockShocker>(serializer)!;
}
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new NotImplementedException("Dont call this");
}
}

View File

@ -0,0 +1,62 @@
using CShocker.Devices.Abstract;
using CShocker.Shockers.Abstract;
namespace CShocker.Shockers;
public class OpenShockShocker : Shocker
{
// ReSharper disable thrice MemberCanBePrivate.Global -> Exposed
public readonly string Name, ID;
public readonly short RfId;
public readonly OpenShockModel Model;
public readonly DateTime CreatedOn;
public readonly bool IsPaused;
public OpenShockShocker(Api api, string name, string id, short rfId, OpenShockModel model, DateTime createdOn, bool isPaused) : base (api)
{
if (api is not OpenShockApi)
throw new Exception($"API-Type {api.GetType().FullName} is not usable with Shocker {this.GetType().FullName}");
this.Name = name;
this.ID = id;
this.RfId = rfId;
this.Model = model;
this.CreatedOn = createdOn;
this.IsPaused = isPaused;
}
public enum OpenShockModel : byte
{
CaiXianlin = 0,
Petrainer = 1
}
public override string ToString()
{
const int tabWidth = -12;
return $"\t{string.Join("\n\t",
$"\u251c {"Type",tabWidth}: {GetType().Name}",
$"\u251c {"Name",tabWidth}: {Name}",
$"\u251c {"ID",tabWidth}: {ID}",
$"\u251c {"RF-ID",tabWidth}: {RfId}",
$"\u251c {"Model",tabWidth}: {Enum.GetName(Model)}",
$"\u251c {"Created On",tabWidth}: {CreatedOn}",
$"\u2514 {"Paused",tabWidth}: {IsPaused}")}" +
$"\n\r";
}
public override bool Equals(object? obj)
{
return obj is OpenShockShocker oss && Equals(oss);
}
private bool Equals(OpenShockShocker other)
{
return ID == other.ID;
}
public override int GetHashCode()
{
return ID.GetHashCode();
}
}

View File

@ -0,0 +1,40 @@
using CShocker.Devices.Abstract;
using CShocker.Shockers.Abstract;
namespace CShocker.Shockers;
public class PiShockShocker : Shocker
{
public readonly string Code;
public PiShockShocker(Api api, string code) : base(api)
{
if (api is not PiShockApi)
throw new Exception($"API-Type {api.GetType().FullName} is not usable with Shocker {this.GetType().FullName}");
Code = code;
}
public override bool Equals(object? obj)
{
return obj is PiShockShocker pss && Equals(pss);
}
private bool Equals(PiShockShocker other)
{
return Code == other.Code;
}
public override int GetHashCode()
{
return Code.GetHashCode();
}
public override string ToString()
{
const int tabWidth = -5;
return $"{string.Join("\n\t",
$"\u251c {"Type",tabWidth}: {GetType().Name}",
$"\u2514 {"Code",tabWidth}: {Code}")}" +
$"\n\r";
}
}

View File

@ -1,58 +0,0 @@
using CShocker.Ranges;
using CShocker.Shockers.Abstract;
using CShocker.Shockers.APIS;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CShocker.Shockers;
public class ShockerJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(Shocker));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
ShockerApi? apiType = (ShockerApi?)jo.SelectToken("ApiType")?.Value<byte>();
switch (apiType)
{
case ShockerApi.OpenShockHttp:
return new OpenShockHttp(
jo.SelectToken("ShockerIds")!.ToObject<List<string>>()!,
jo.SelectToken("IntensityRange")!.ToObject<IntensityRange>()!,
jo.SelectToken("DurationRange")!.ToObject<DurationRange>()!,
jo.SelectToken("ApiKey")!.Value<string>()!,
jo.SelectToken("Endpoint")!.Value<string>()!
);
case ShockerApi.OpenShockSerial:
case ShockerApi.PiShockHttp:
return new PiShockHttp(
jo.SelectToken("ShockerIds")!.ToObject<List<string>>()!,
jo.SelectToken("IntensityRange")!.ToObject<IntensityRange>()!,
jo.SelectToken("DurationRange")!.ToObject<DurationRange>()!,
jo.SelectToken("ApiKey")!.Value<string>()!,
jo.SelectToken("Username")!.Value<string>()!,
jo.SelectToken("ShareCode")!.Value<string>()!,
jo.SelectToken("Endpoint")!.Value<string>()!
);
case ShockerApi.PiShockSerial:
throw new NotImplementedException();
default:
throw new Exception();
}
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}

View File

@ -1,51 +1,42 @@
Library to interact with Shock-Collars that are remotely controllable via ESP32-Boards. Library to interact with Shock-Collars that are remotely controllable via ESP32-Boards.
[![GitHub License](https://img.shields.io/github/license/c9glax/cshocker)](https://github.com/C9Glax/CShocker) [![GitHub License](https://img.shields.io/github/license/c9glax/cshocker)](https://github.com/C9Glax/CShocker)
[![NuGet Version](https://img.shields.io/nuget/v/CShocker)](https://shields.io/badges/nu-get-version) [![NuGet Version](https://img.shields.io/nuget/v/CShocker)](https://www.nuget.org/packages/CShocker)
# Usage # Usage
```csharp ```csharp
public static void Main(string[] args){ public static void Main(string[] args){
List<string> shockerIds = new();
IntensityRange intensityRange = new IntensityRange(30, 50);
DurationRange durationRange = new DurationRange(1000, 2000);
string apiKey = ":)"; string apiKey = ":)";
OpenShockHttp openShockHttp = new OpenShockHttp(shockerIds, intensityRange, durationRange, apiKey);
openShockHttp.ShockerIds.AddRange(openShockHttp.GetShockers());
string username = "username"; OpenShockHttp openShockHttp = new (apiKey);
string shareCode = "sharecode"; OpenShockShocker shocker1 = openShockHttp.GetShockers().First();
PiShockHttp piShockHttp = new PiShockHttp(shockerIds, intensityRange, durationRange, apiKey, username, shareCode, apiUri); shocker1.Control(ControlAction.Vibrate, 20, 1000);
ControlAction action = ControlAction.Shock; shocker1.Dispose();
openShockHttp.Control(action, shockerIds.First(), 20, 1000);
piShockHttp.Control(action); List<SerialPortInfo> serialPorts = SerialHelper.GetSerialPorts();
int selectedPort = 1;
OpenShockSerial openShockSerial = new(serialPorts[selectedPort], apiKey);
OpenShockShocker shocker2 = openShockSerial.GetShockers().First();
shocker2.Control(ControlAction.Vibrate, 20, 1000);
shocker2.Dispose();
} }
``` ```
## `Shocker.Control` ## `Shocker.Control`
```csharp ```csharp
Control(ControlAction action, string? shockerId = null, int? intensity = null, int? duration = null) Control(ControlAction action, int intensity, int duration)
``` ```
If `shockerId` is `null`, all IDs will be used. If `intensity` or `duration` are `null`, a random value within the
configured range will be used.
### ControlAction ### ControlAction
From [here](https://github.com/C9Glax/CShocker/blob/master/CShocker/Shockers/ControlAction.cs) From [here](https://github.com/C9Glax/CShocker/blob/master/CShocker/Devices/Additional/ControlActionEnum.cs)
## Variables ## Variables
### ApiKey ### ApiKey
- For OpenShock (HTTP) get token [here](https://shocklink.net/#/dashboard/tokens) - For OpenShock (HTTP) get token [here](https://shocklink.net/#/dashboard/tokens)
- For PiShock (HTTP) get information [here](https://apidocs.pishock.com/#header-authenticating)
### ShockerIds
List of Shocker-Ids, comma seperated.
`[ "ID-1-asdasd", "ID-2-fghfgh" ]`
OpenShockHttp also can retrieve IDs automatically.
### Intensity Range ### Intensity Range
in percent in percent
@ -57,8 +48,9 @@ in ms
- `0-30000` OpenShock - `0-30000` OpenShock
- `0-15000` PiShock - `0-15000` PiShock
### Username (PiShockHttp only) ## Future
For PiShock (HTTP) get information [here](https://apidocs.pishock.com/#header-authenticating) ### ~~Username (PiShockHttp only)~~
~~For PiShock (HTTP) get information [here](https://apidocs.pishock.com/#header-authenticating)~~
### Sharecode (PiShockHttp only) ### ~~Sharecode (PiShockHttp only)~~
For PiShock (HTTP) get information [here](https://apidocs.pishock.com/#header-authenticating) ~~For PiShock (HTTP) get information [here](https://apidocs.pishock.com/#header-authenticating)~~

62
TestApp/Program.cs Normal file
View File

@ -0,0 +1,62 @@
using CShocker.Devices.Additional;
using CShocker.Devices.APIs;
using CShocker.Shockers;
using GlaxLogger;
using Microsoft.Extensions.Logging;
Logger logger = new (LogLevel.Trace);
Console.WriteLine("OpenShock API Key:");
string? apiKey = Console.ReadLine();
while(apiKey is null || apiKey.Length < 1)
apiKey = Console.ReadLine();
OpenShockHttp openShockHttp = new (apiKey, logger: logger);
foreach (OpenShockShocker shocker in openShockHttp.GetShockers())
{
shocker.Control(ControlAction.Vibrate, 20, 1000);
shocker.Control(ControlAction.Vibrate, 20, 1000);
shocker.Control(ControlAction.Vibrate, 20, 1000);
Thread.Sleep(1100);
}
/*
File.WriteAllText("shockers.json", JsonConvert.SerializeObject(shocker));
OpenShockShocker deserialized = JsonConvert.DeserializeObject<OpenShockShocker>(File.ReadAllText("shockers.json"), new ApiJsonConverter())!;
Thread.Sleep(1100); //Wait for previous to end
deserialized.Control(ControlAction.Vibrate, 20, 1000);
shocker.Dispose();
deserialized.Dispose();
*/
/*
#pragma warning disable CA1416
List<SerialPortInfo> serialPorts = SerialHelper.GetSerialPorts();
if (serialPorts.Count < 1)
return;
for(int i = 0; i < serialPorts.Count; i++)
Console.WriteLine($"{i}) {serialPorts[i]}");
Console.WriteLine($"Select Serial Port [0-{serialPorts.Count-1}]:");
string? selectedPortStr = Console.ReadLine();
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 openShockSerial = new(serialPorts[selectedPort], apiKey, logger: logger);
OpenShockShocker shocker = openShockSerial.GetShockers().First();
shocker.Control(ControlAction.Vibrate, 20, 1000);
File.WriteAllText("shockers.json", JsonConvert.SerializeObject(shocker));
OpenShockShocker deserialized = JsonConvert.DeserializeObject<OpenShockShocker>(File.ReadAllText("shockers.json"), new ApiJsonConverter())!;
shocker.Dispose();
deserialized.Dispose();
*/
while(!Console.KeyAvailable)
Thread.Sleep(100);

19
TestApp/TestApp.csproj Normal file
View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<LangVersion>latestmajor</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CShocker\CShocker.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GlaxLogger" Version="1.0.7.2" />
</ItemGroup>
</Project>