diff --git a/WirelessMicSuiteServer/CommandLineOptions.cs b/WirelessMicSuiteServer/CommandLineOptions.cs new file mode 100644 index 0000000..3a8456f --- /dev/null +++ b/WirelessMicSuiteServer/CommandLineOptions.cs @@ -0,0 +1,139 @@ +using System.Reflection; + +namespace WirelessMicSuiteServer; + +public class CommandLineOptions +{ + private Dictionary options; + private Dictionary parsedArgs; + + public Dictionary ParsedArgs => parsedArgs; + + public CommandLineOptions(CommandLineOption[] options, string[]? args) + { + this.parsedArgs = []; + this.options = new( + options.Select(x => new KeyValuePair(x.key, x)) + .Concat( + options.Where(x=>x.alias != null) + .Select(x => new KeyValuePair(x.alias!, x)))); + + this.options.TryAdd("--help", new("--help", action: _=>PrintHelp())); + + if (args != null ) + ParseArgs(args); + } + + private readonly ILogger logger = Program.LoggerFac.CreateLogger(); + public void Log(string? message, LogSeverity severity = LogSeverity.Info) + { + logger.Log(message, severity); + } + + public void ParseArgs(string[] args) + { + CommandLineOption? option = null; + List tmpList = []; + foreach (var arg in args) + { + if (options.TryGetValue(arg, out var nOption)) + { + AddParsedArg(option, tmpList); + + option = nOption; + continue; + } + + if (option == null) + { + Log($"Unrecognised command line argument '{arg}'!", LogSeverity.Error); + Console.WriteLine(); + PrintHelp(); + return; + } + + try + { + switch (option.Value.argType) + { + case CommandLineArgType.None: + break; + case CommandLineArgType.String: + tmpList.Add(arg); + break; + case CommandLineArgType.Int: + tmpList.Add(int.Parse(arg)); + break; + case CommandLineArgType.Uint: + tmpList.Add(uint.Parse(arg)); + break; + case CommandLineArgType.Path: + if (!Path.Exists(arg)) + throw new ArgumentException($"Path '{arg}' does not exist!"); + tmpList.Add(arg); + break; + } + } + catch + { + Log($"Couldn't parse '{arg}' as {option.Value.argType}!", LogSeverity.Error); + Console.WriteLine(); + PrintHelp(); + return; + } + + if (!option.Value.multipleArguments) + option = null; + } + + AddParsedArg(option, tmpList); + + void AddParsedArg(CommandLineOption? option, List tmpList) + { + if (option is CommandLineOption opt) + { + opt.action?.Invoke(opt.multipleArguments ? tmpList.ToArray() : tmpList.FirstOrDefault()); + parsedArgs.Add(option.Value.key, opt.multipleArguments ? tmpList.ToArray() : tmpList.FirstOrDefault()); + tmpList.Clear(); + } + } + } + + public void PrintHelp() + { + var assembly = Assembly.GetExecutingAssembly(); + Console.WriteLine($"##### Wireless Mic Suite Server #####"); + Console.WriteLine($"# version: {assembly.GetName().Version}"); + Console.WriteLine($"# "); + Console.WriteLine($"# Command line options: "); + foreach (var opt in options.Values) + { + Console.WriteLine($"\t{opt.key}{(opt.alias != null?", " + opt.alias:"")} " + + $"{(opt.argType != CommandLineArgType.None ? "<"+opt.argType+">":"")}{(opt.multipleArguments?", ...":"")}"); + Console.WriteLine($"\t\t{opt.help}"); + Console.WriteLine(); + } + } +} + +public struct CommandLineOption(string key, string? alias = null, string? help = null, + CommandLineArgType argType = CommandLineArgType.None, bool multipleArguments = false, + Action? action = null) +{ + public string key = key; + public string? alias = alias; + public string? help = help; + public CommandLineArgType argType = argType; + public bool multipleArguments = multipleArguments; + //public bool positional; + public Action? action = action; +} + +public enum CommandLineArgType +{ + None, + String, + Int, + Uint, + Path +} diff --git a/WirelessMicSuiteServer/IWirelessMicReceiver.cs b/WirelessMicSuiteServer/IWirelessMicReceiver.cs index ea2d2cd..6036fb1 100644 --- a/WirelessMicSuiteServer/IWirelessMicReceiver.cs +++ b/WirelessMicSuiteServer/IWirelessMicReceiver.cs @@ -159,9 +159,13 @@ public interface IWirelessMic : INotifyPropertyChanged /// public struct MeteringData(float rssiA, float rssiB, float audioLevel) { - [JsonInclude] public float rssiA = rssiA; - [JsonInclude] public float rssiB = rssiB; - [JsonInclude] public float audioLevel = audioLevel; + [JsonIgnore] public float rssiA = rssiA; + [JsonIgnore] public float rssiB = rssiB; + [JsonIgnore] public float audioLevel = audioLevel; + + public readonly float RssiA => rssiA; + public readonly float RssiB => rssiB; + public readonly float AudioLevel => audioLevel; } /// @@ -228,16 +232,21 @@ public void StopMetering() } } +/// +/// A frequency range in Hz. +/// +/// The lower bound of the tunable frequency range in Hz. +/// The upper bound of the tunable frequency range in Hz. public struct FrequencyRange(ulong startFreq, ulong endFreq) { /// /// The lower bound of the tunable frequency range in Hz. /// - [JsonInclude] public ulong startFrequency = startFreq; + [JsonInclude] public ulong StartFrequency { get; set; } = startFreq; /// /// The upper bound of the tunable frequency range in Hz. /// - [JsonInclude] public ulong endFrequency = endFreq; + [JsonInclude] public ulong EndFrequency { get; set; } = endFreq; } [JsonConverter(typeof(JsonStringEnumConverter))] @@ -247,6 +256,9 @@ public enum IPMode Manual } +/// +/// Represents an IPv4 address. +/// [StructLayout(LayoutKind.Explicit)] [JsonConverter(typeof(JsonStringConverter))] public struct IPv4Address @@ -281,6 +293,10 @@ public IPv4Address(IPAddress address) throw new ArgumentException($"Failed to convert IP address '{address}' to IPv4Address!"); } + /// + [JsonConstructor] + public IPv4Address(string str) : this(str.AsSpan()) { } + /// /// Parse an IPv4 address in the form 'aaa.bbb.ccc.ddd'. /// @@ -306,6 +322,9 @@ public override readonly string ToString() } } +/// +/// Represents a MAC address. +/// [StructLayout(LayoutKind.Explicit)] [JsonConverter(typeof(JsonStringConverter))] public struct MACAddress @@ -334,6 +353,10 @@ public MACAddress(byte a, byte b, byte c, byte d, byte e, byte f) this.f = f; } + /// + [JsonConstructor] + public MACAddress(string str) : this(str.AsSpan()) { } + /// /// Parse a MAC address in the form 'aa:bb:cc:dd:ee:ff'. /// diff --git a/WirelessMicSuiteServer/JsonStringConverter.cs b/WirelessMicSuiteServer/JsonStringConverter.cs index cc4d864..24b186c 100644 --- a/WirelessMicSuiteServer/JsonStringConverter.cs +++ b/WirelessMicSuiteServer/JsonStringConverter.cs @@ -25,9 +25,7 @@ public JsonStringConverter() public sealed override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { if (typeToConvert != typeof(T)) - { throw new ArgumentOutOfRangeException(); - } return new StringConverter(); } @@ -40,10 +38,10 @@ public JsonStringConverter() public StringConverter() { var str = typeof(T).GetConstructor([typeof(string)]); - var spn = typeof(T).GetConstructor([typeof(ReadOnlySpan)]); - if (spn != null) - strConstructor = spn; - else if (str != null) + //var spn = typeof(T).GetConstructor([typeof(ReadOnlySpan)]); + //if (spn != null) + // strConstructor = spn; + /*else*/ if (str != null) strConstructor = str; else throw new ArgumentException("Target type must have a constructor which takes a single string as a parameter!"); diff --git a/WirelessMicSuiteServer/MessagePipe.cs b/WirelessMicSuiteServer/MessagePipe.cs deleted file mode 100644 index 3a43885..0000000 --- a/WirelessMicSuiteServer/MessagePipe.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Collections.Concurrent; - -namespace WirelessMicSuiteServer; - -public class MessagePipe -{ - private readonly SpinLock bufferLock; - private SemaphoreSlim onDataSem; - private ConcurrentQueue messages = []; - private TData[] buffer; - private int headIndex; - private int tailIndex; - - /// - /// Initializes a new instance of a with a given capacity. - /// - /// - public MessagePipe(int capacity = 8192) - { - buffer = new TData[capacity]; - onDataSem = new SemaphoreSlim(0); - bufferLock = new(); - } - - /* - * Circular buffers - * Buff: - * [ 0 1 2 3 4 5 6 7 8 9 ] - * [ ] [ ] <- Simple case, just an array - * H T - * { } <- new item - * - * [ ] [ ] <- Annoying case, Tail is in front of Head - * T H - * - * [ ] <- We don't allow individual items to wrap around to the beginning, so some space may be wasted - * H T requested > remaining so, the last byte is wasted - * - */ - - private int RemainingSpace => headIndex <= tailIndex ? (buffer.Length - (tailIndex - headIndex)) : (buffer.Length - (headIndex - tailIndex)); - private bool IsEmpty => headIndex == tailIndex; - - /// - /// Gets a segment of memory to write into. must be called after this method has been called. - /// - /// The number of elements to request. - /// An to write into. - public ArraySegment GetBuffer(int size) - { - bool taken = false; - bufferLock.Enter(ref taken); - ArraySegment ret; - - int remainingSpaceToEnd = headIndex <= tailIndex ? (buffer.Length - tailIndex) : (buffer.Length - (headIndex - tailIndex)); - if (remainingSpaceToEnd >= size) - { - // Grow the array - TData[] nbuff = new TData[buffer.Length * 2]; - if (headIndex <= tailIndex) - { - Array.Copy(buffer, nbuff, buffer.Length); - } else - { - Array.Copy(buffer, 0, nbuff, 0, tailIndex); - int sizeDiff = nbuff.Length - buffer.Length; - Array.Copy(buffer, headIndex, nbuff, headIndex + sizeDiff, buffer.Length - headIndex); - headIndex += sizeDiff; - buffer = nbuff; - } - } - - ret = new(buffer, tailIndex, size); - tailIndex += size; - if (tailIndex == buffer.Length) - tailIndex = 0; - - if (taken) - bufferLock.Exit(); - return ret; - } - - /// - /// Pushes a previously allocated segment of data into the pipe. - /// - /// The array segment allocated with . - /// Metadata describing the message. - public void Push(ArraySegment data, TMeta metaData) - { - messages.Enqueue(new(metaData, data)); - onDataSem.Release(); - } - - public bool TryPop(out Message msg) - { - var res = messages.TryDequeue(out msg); - - // TODO: Needs to handle the case where the item at the head of the cbuff has been GetBuffer() but not Push() - // ie: msg refers to a Segment which doesn't start at headIndex... - // I guess we should return false in that case or Spin until the necessary Push() is called, should - // only occur when data is being consumed much faster than it is produced. - // Although, now that I think about it, if we can trust onDataSem to be the count of Push() calls, then - // would that be safe? Nope: if GetData is called twice and the second GetData is Pushed before the first, - // then we have the same issue. - // Similarly, we need to be able to know when we can reclaim the ArraySegment returned to the user... - // Could just copy it into stackalloc mem, but that would limit the max item size... - - return res; - } - - public Message Pop() - { - Message ret; - while (!messages.TryDequeue(out ret)) - onDataSem.Wait(); - return ret; - } - - public readonly struct Message(TMeta metaData, ArraySegment msgData) - { - public readonly TMeta metaData = metaData; - public readonly ArraySegment msgData = msgData; - } -} diff --git a/WirelessMicSuiteServer/Program.cs b/WirelessMicSuiteServer/Program.cs index 65d9830..0d38126 100644 --- a/WirelessMicSuiteServer/Program.cs +++ b/WirelessMicSuiteServer/Program.cs @@ -13,17 +13,33 @@ public class Program public static void Main(string[] args) { + var cli = CreateCommandLineArgs(args); + Log("Starting Wireless Mic Suite server..."); var assembly = Assembly.GetExecutingAssembly(); string? copyright = string.IsNullOrEmpty(assembly.Location) ? "Copyright Thomas Mathieson 2024" : FileVersionInfo.GetVersionInfo(assembly.Location).LegalCopyright; Log($"Version: {assembly.GetName().Version}; {copyright}"); - WirelessMicManager micManager = new([new ShureUHFRManager()]); - if (args.Contains("-m")) + int meterInterval = 50; + if (cli.ParsedArgs.TryGetValue("--meter-interval", out object? arg)) + meterInterval = (int)(uint)arg!; + + WirelessMicManager micManager = new([ + new ShureUHFRManager() { PollingPeriodMS = meterInterval } + ]); + if (cli.ParsedArgs.ContainsKey("--meters")) Task.Run(() => MeterTask(micManager)); StartWebServer(args, micManager); } + private static CommandLineOptions CreateCommandLineArgs(string[] args) + { + return new CommandLineOptions([ + new("--meters", "-m", help: "Displays ASCII-art meters in the terminal for the connected wireless mics. Don't use this in production."), + new("--meter-interval", "-i", argType: CommandLineArgType.Uint, help:"Sets the interval at which metering information should be polled from the wireless receivers. This is specified in milli-seconds.") + ], args); + } + private static void StartWebServer(string[] args, WirelessMicManager micManager) { var builder = WebApplication.CreateBuilder(args); @@ -35,7 +51,7 @@ private static void StartWebServer(string[] args, WirelessMicManager micManager) // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); + WebAPI.AddSwaggerGen(builder.Services); var app = builder.Build(); diff --git a/WirelessMicSuiteServer/ShureUHFR/ShureUHFRManager.cs b/WirelessMicSuiteServer/ShureUHFR/ShureUHFRManager.cs index 5d6e156..760f855 100644 --- a/WirelessMicSuiteServer/ShureUHFR/ShureUHFRManager.cs +++ b/WirelessMicSuiteServer/ShureUHFR/ShureUHFRManager.cs @@ -17,7 +17,7 @@ public class ShureUHFRManager : IWirelessMicReceiverManager private const int UdpPortPrivate = 2201; private const int MaxUDPSize = 0x10000; internal const uint ManagerSnetID = 0x6A05ADAD; - private const int ReceiverDisconnectTimeout = 2000; + private const int ReceiverDisconnectTimeout = 5000; private readonly Socket socket; private readonly Task rxTask; @@ -133,16 +133,19 @@ private void RXTask() var epIp = (IPEndPoint)ep; epIp.Port = UdpPortPrivate; - if (receiversDict.TryGetValue(uid, out var receiver)) + lock (receiversDict) { - receiver.LastPingTime = DateTime.UtcNow; - } - else - { - Log($"[Discovery] Found Shure Receiver @ {epIp} UID=0x{uid:X}", LogSeverity.Info); - receiver = new ShureUHFRReceiver(this, epIp, uid, header.snetIDFrom); - receiversDict.Add(uid, receiver); - Receivers.Add(receiver); + if (receiversDict.TryGetValue(uid, out var receiver)) + { + receiver.LastPingTime = DateTime.UtcNow; + } + else + { + Log($"[Discovery] Found Shure Receiver @ {epIp} UID=0x{uid:X}", LogSeverity.Info); + receiver = new ShureUHFRReceiver(this, epIp, uid, header.snetIDFrom); + receiversDict.Add(uid, receiver); + Receivers.Add(receiver); + } } continue; } diff --git a/WirelessMicSuiteServer/ShureUHFR/ShureUHFRReceiver.cs b/WirelessMicSuiteServer/ShureUHFR/ShureUHFRReceiver.cs index fbfed9b..51a33d4 100644 --- a/WirelessMicSuiteServer/ShureUHFR/ShureUHFRReceiver.cs +++ b/WirelessMicSuiteServer/ShureUHFR/ShureUHFRReceiver.cs @@ -23,6 +23,7 @@ public class ShureUHFRReceiver : IWirelessMicReceiver private IPMode? ipMode; private MACAddress? macAddress; + public ShureUHFRManager Manager => manager; public IPEndPoint Address { get; init; } public DateTime LastPingTime { get; set; } public int NumberOfChannels => mics.Length; diff --git a/WirelessMicSuiteServer/ShureUHFR/ShureWirelessMic.cs b/WirelessMicSuiteServer/ShureUHFR/ShureWirelessMic.cs index 58edd36..c75cfbf 100644 --- a/WirelessMicSuiteServer/ShureUHFR/ShureWirelessMic.cs +++ b/WirelessMicSuiteServer/ShureUHFR/ShureWirelessMic.cs @@ -113,7 +113,7 @@ public ShureWirelessMic(ShureUHFRReceiver receiver, uint uid, int receiverNo) private void SendStartupCommands() { - receiver.Send($"* METER {receiverNo} ALL 1 *"); + receiver.Send($"* METER {receiverNo} ALL {receiver.Manager.PollingPeriodMS / 30} *"); receiver.Send($"* UPDATE {receiverNo} ADD *"); receiver.Send($"* GET {receiverNo} CHAN_NAME *"); diff --git a/WirelessMicSuiteServer/WebAPI.cs b/WirelessMicSuiteServer/WebAPI.cs index fae76db..12ec9a8 100644 --- a/WirelessMicSuiteServer/WebAPI.cs +++ b/WirelessMicSuiteServer/WebAPI.cs @@ -1,44 +1,89 @@ -using System; +using Microsoft.OpenApi.Models; +using System; +using System.Collections.Concurrent; +using System.Reflection; using System.Text; namespace WirelessMicSuiteServer; public static class WebAPI { + public static void AddSwaggerGen(IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "Wireless Mic Suite API", + Description = "An API for interfacing with wireless microphone receivers.", + Contact = new OpenApiContact + { + Name = "Thomas Mathieson", + Email = "thomas@mathieson.dev", + Url = new Uri("https://github.com/space928/WirelessMicSuiteServer/issues") + }, + License = new OpenApiLicense + { + Name = "GPLv3", + Url = new Uri("https://github.com/space928/WirelessMicSuiteServer/blob/main/LICENSE.txt") + }, + }); + + // using System.Reflection; + var xmlFilename = Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"); + if (File.Exists(xmlFilename)) + c.IncludeXmlComments(xmlFilename); + + c.MapType(() => new OpenApiSchema { Type = "string" }); + c.MapType(() => new OpenApiSchema { Type = "string" }); + }); + } + public static void AddWebRoots(WebApplication app, WirelessMicManager micManager) { + #region Getters app.MapGet("/getWirelessReceivers", (HttpContext ctx) => { + SetAPIHeaderOptions(ctx); return micManager.Receivers.Select(x => new WirelessReceiverData(x)); }).WithName("GetWirelessReceivers") + //.WithGroupName("Getters") .WithOpenApi(); app.MapGet("/getWirelessMics", (HttpContext ctx) => { + SetAPIHeaderOptions(ctx); return micManager.WirelessMics.Select(x => new WirelessMicData(x)); }).WithName("GetWirelessMics") + //.WithGroupName("Getters") .WithOpenApi(); app.MapGet("/getWirelessMicReceiver/{uid}", (uint uid, HttpContext ctx) => { + SetAPIHeaderOptions(ctx); var rec = micManager.TryGetWirelessMicReceiver(uid); if (rec == null) return new WirelessReceiverData?(); return new WirelessReceiverData(rec); - }).WithName("getWirelessMicReceiver") + }).WithName("GetWirelessMicReceiver") + //.WithGroupName("Getters") .WithOpenApi(); app.MapGet("/getWirelessMic/{uid}", (uint uid, HttpContext ctx) => { + SetAPIHeaderOptions(ctx); var mic = micManager.TryGetWirelessMic(uid); if (mic == null) return new WirelessMicData?(); return new WirelessMicData(mic); }).WithName("GetWirelessMic") + //.WithGroupName("Getters") .WithOpenApi(); app.MapGet("/getMicMeter/{uid}", (uint uid, HttpContext ctx) => { + SetAPIHeaderOptions(ctx); var mic = micManager.TryGetWirelessMic(uid); if (mic == null || mic.MeterData == null) return null; @@ -49,10 +94,12 @@ public static void AddWebRoots(WebApplication app, WirelessMicManager micManager return samples; }).WithName("GetMicMeter") + //.WithGroupName("Getters") .WithOpenApi(); app.MapGet("/getMicMeterAscii/{uid}", (uint uid, HttpContext ctx) => { + SetAPIHeaderOptions(ctx); var mic = micManager.TryGetWirelessMic(uid); if (mic == null || mic.MeterData == null) return ""; @@ -75,6 +122,182 @@ public static void AddWebRoots(WebApplication app, WirelessMicManager micManager return sb.ToString(); }).WithName("GetMicMeterAscii") + //.WithGroupName("Getters") + .WithOpenApi(); + #endregion + + #region Setters + var receiverProps = typeof(IWirelessMicReceiver).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); + Dictionary receiverSetters = new( + receiverProps.Select(x => new KeyValuePair(x.Name, x)) + .Concat(receiverProps.Select(x => new KeyValuePair(CamelCase(x.Name), x))) + ); + app.MapGet("/setWirelessMicReceiver/{uid}/{param}/{value}", (uint uid, string param, string value, HttpContext ctx) => + { + SetAPIHeaderOptions(ctx); + var mic = micManager.TryGetWirelessMicReceiver(uid); + if (mic == null) + return new APIResult(false, $"Couldn't find wireless mic with UID 0x{uid:X}!"); + + if (!receiverSetters.TryGetValue(param, out var prop)) + return new APIResult(false, $"Property '{param}' does not exist!"); + + try + { + var val = DeserializeSimpleType(value, prop.PropertyType); + prop.SetValue(mic, val); + } + catch (Exception ex) + { + return new APIResult(false, ex.Message); + } + + return new APIResult(true); + }).WithName("SetWirelessMicReceiver") + .WithDescription("Sets a named property to the given value on an IWirelessMicReceiver. See IWirelessMicReceiver for the full list of supported properties.") + //.WithGroupName("Setters") + .WithOpenApi(); + + var micProps = typeof(IWirelessMic).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); + Dictionary micSetters = new( + micProps.Select(x => new KeyValuePair(x.Name, x)) + .Concat(micProps.Select(x => new KeyValuePair(CamelCase(x.Name), x))) + ); + app.MapGet("/setWirelessMic/{uid}/{param}/{value}", (uint uid, string param, string value, HttpContext ctx) => + { + SetAPIHeaderOptions(ctx); + var mic = micManager.TryGetWirelessMic(uid); + if (mic == null) + return new APIResult(false, $"Couldn't find wireless mic with UID 0x{uid:X}!"); + + if (!micSetters.TryGetValue(param, out var prop)) + return new APIResult(false, $"Property '{param}' does not exist!"); + + try + { + var val = DeserializeSimpleType(value, prop.PropertyType); + prop.SetValue(mic, val); + } catch (Exception ex) + { + return new APIResult(false, ex.Message); + } + + return new APIResult(true); + }).WithName("SetWirelessMic") + .WithDescription("Sets a named property to the given value on an IWirelessMic. See IWirelessMic for the full list of supported properties.") + //.WithGroupName("Setters") .WithOpenApi(); + #endregion + } + + private static void SetAPIHeaderOptions(HttpContext ctx) + { + ctx.Response.Headers.AccessControlAllowOrigin = "*"; + ctx.Response.Headers.CacheControl = "no-store"; + } + + private static string CamelCase(string str) + { + if (string.IsNullOrEmpty(str)) + return str; + char l = char.ToLowerInvariant(str[0]); + return $"{l}{str[1..]}"; + } + + private static bool IsSimple(Type type) + { + return type.IsPrimitive + || type.IsEnum + || type.Equals(typeof(string)) + || type.Equals(typeof(decimal)); + } + + private static readonly ConcurrentDictionary stringConstructors = []; + public static object? DeserializeSimpleType(string valueStr, Type targetType) + { + if (Nullable.GetUnderlyingType(targetType) is Type nullableType) + { + if (valueStr == "null") + return null; + else + targetType = nullableType; + } + + if (!IsSimple(targetType)) + { + if (!stringConstructors.TryGetValue(targetType, out var strConstructor)) + { + var str = targetType.GetConstructor([typeof(string)]); + if (str != null) + strConstructor = str; + else + throw new ArgumentException($"Couldn't find a suitable constructor for {targetType.FullName} which takes a single string as a parameter!"); + stringConstructors.TryAdd(targetType, strConstructor); + } + + return strConstructor.Invoke([valueStr]); + } + + var simpleType = targetType; + if (targetType.IsEnum) + simpleType = targetType.GetEnumUnderlyingType(); + object? value; + + try + { + if (simpleType == typeof(bool)) + value = int.Parse(valueStr) != 0; + else if (simpleType == typeof(byte)) + value = byte.Parse(valueStr); + else if (simpleType == typeof(sbyte)) + value = sbyte.Parse(valueStr); + else if (simpleType == typeof(char)) + value = valueStr[0];//char.Parse(line); + else if (simpleType == typeof(decimal)) + value = decimal.Parse(valueStr); + else if (simpleType == typeof(double)) + value = double.Parse(valueStr); + else if (simpleType == typeof(float)) + value = float.Parse(valueStr); + else if (simpleType == typeof(int)) + value = int.Parse(valueStr); + else if (simpleType == typeof(uint)) + value = uint.Parse(valueStr); + else if (simpleType == typeof(nint)) + value = nint.Parse(valueStr); + else if (simpleType == typeof(long)) + value = long.Parse(valueStr); + else if (simpleType == typeof(ulong)) + value = ulong.Parse(valueStr); + else if (simpleType == typeof(short)) + value = short.Parse(valueStr); + else if (simpleType == typeof(ushort)) + value = ushort.Parse(valueStr); + else if (simpleType == typeof(string)) + value = new string(valueStr); + else + throw new ArgumentException($"Fields of type {targetType.Name} are not supported!"); + } + catch (Exception ex) when (ex is FormatException or OverflowException) + { + throw new ArgumentException($"Value of '{valueStr}' couldn't be parsed as a {targetType.Name}!"); + } + + if (targetType.IsEnum) + value = Enum.ToObject(targetType, value); + + return value; + } + + /// + /// Represents the result of an API operation. + /// + /// Whether the operation succeeded. + /// Optionally, an error message if it failed. + [Serializable] + public readonly struct APIResult(bool success, string? message = null) + { + public readonly bool Success { get; init; } = success; + public readonly string? Message { get; init; } = message; } } diff --git a/WirelessMicSuiteServer/WirelessMicSuiteServer.csproj b/WirelessMicSuiteServer/WirelessMicSuiteServer.csproj index 0b66d99..cc06228 100644 --- a/WirelessMicSuiteServer/WirelessMicSuiteServer.csproj +++ b/WirelessMicSuiteServer/WirelessMicSuiteServer.csproj @@ -12,6 +12,8 @@ README.md https://github.com/space928/WirelessMicSuiteServer GPL-3.0-or-later + True + $(NoWarn);1591 @@ -23,7 +25,7 @@ - +