From ad1ba01c06adcdbf6e7065843482db3c74d45ce3 Mon Sep 17 00:00:00 2001 From: Thomas Mathieson Date: Tue, 25 Jun 2024 11:48:42 +0200 Subject: [PATCH] UHF-R & General API/Interfaces Improvements: - Moved Shure UHF-R classes into their own folder - Added more code docs to mic interfaces - Added a bunch of receiver specific properties - Many properties are now nullable - Added metering - Json serialization for some of the new structs - Ascii art metering - Added new API methods - Fixed a load of UHF-R parsing bugs - Implemented a few more UHF-R properties - Refactoring --- .../.config/dotnet-tools.json | 13 + WirelessMicSuiteServer/ByteMessage.cs | 24 ++ .../IWirelessMicReceiver.cs | 313 ++++++++++++-- WirelessMicSuiteServer/JsonStringConverter.cs | 65 +++ WirelessMicSuiteServer/Program.cs | 72 +++- .../Properties/launchSettings.json | 1 + .../{ => ShureUHFR}/ShureUHFRManager.cs | 82 ++-- .../{ => ShureUHFR}/ShureUHFRReceiver.cs | 393 ++++++++---------- .../ShureUHFR/ShureWirelessMic.cs | 365 ++++++++++++++++ WirelessMicSuiteServer/WebAPI.cs | 71 +++- WirelessMicSuiteServer/WirelessMicManager.cs | 27 +- 11 files changed, 1134 insertions(+), 292 deletions(-) create mode 100644 WirelessMicSuiteServer/.config/dotnet-tools.json create mode 100644 WirelessMicSuiteServer/ByteMessage.cs create mode 100644 WirelessMicSuiteServer/JsonStringConverter.cs rename WirelessMicSuiteServer/{ => ShureUHFR}/ShureUHFRManager.cs (89%) rename WirelessMicSuiteServer/{ => ShureUHFR}/ShureUHFRReceiver.cs (52%) create mode 100644 WirelessMicSuiteServer/ShureUHFR/ShureWirelessMic.cs diff --git a/WirelessMicSuiteServer/.config/dotnet-tools.json b/WirelessMicSuiteServer/.config/dotnet-tools.json new file mode 100644 index 0000000..4f257cf --- /dev/null +++ b/WirelessMicSuiteServer/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "8.0.6", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/WirelessMicSuiteServer/ByteMessage.cs b/WirelessMicSuiteServer/ByteMessage.cs new file mode 100644 index 0000000..3dd379f --- /dev/null +++ b/WirelessMicSuiteServer/ByteMessage.cs @@ -0,0 +1,24 @@ +using System.Buffers; +using System.Net; + +namespace WirelessMicSuiteServer; + +public readonly struct ByteMessage : IDisposable +{ + public readonly IPEndPoint endPoint; + public readonly ArraySegment Buffer => new(buffer, 0, length); + private readonly byte[] buffer; + private readonly int length; + + public ByteMessage(IPEndPoint endPoint, int size) + { + this.endPoint = endPoint; + this.length = size; + this.buffer = ArrayPool.Shared.Rent(size); + } + + public void Dispose() + { + ArrayPool.Shared.Return(buffer); + } +} diff --git a/WirelessMicSuiteServer/IWirelessMicReceiver.cs b/WirelessMicSuiteServer/IWirelessMicReceiver.cs index 80bb67e..ea2d2cd 100644 --- a/WirelessMicSuiteServer/IWirelessMicReceiver.cs +++ b/WirelessMicSuiteServer/IWirelessMicReceiver.cs @@ -1,10 +1,14 @@ using System; +using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Net; +using System.Runtime.InteropServices; using System.Text; +using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -15,26 +19,92 @@ public interface IWirelessMicReceiverManager : IDisposable public abstract int PollingPeriodMS { get; set; } public abstract ObservableCollection Receivers { get; init; } - //public void DiscoverDevices(); - // public IEnumerable EnumerateDevices(); + /// + /// Tries to get a by it's UID (in O(1) time). + /// + /// The UID of the receiver to find. + /// The receiver if it exists or null. + public IWirelessMicReceiver? TryGetWirelessMicReceiver(uint uid); + public IWirelessMic? TryGetWirelessMic(uint uid); } -public interface IWirelessMicReceiver : IDisposable +public interface IWirelessMicReceiver : IDisposable, INotifyPropertyChanged { - //public static abstract IEnumerable EnumerateDevices(); - + /// + /// A unique identifier for this wireless receiver unit. Note that this is associated with the + /// physical receiver unit, and is persistant between reboots/ip address changes. + /// public abstract uint UID { get; } + /// + /// The IP address of the wireless receiver; note that on DHCP networks this may not be constant. + /// public abstract IPEndPoint Address { get; } + /// + /// The number of receiver channels this receiver supports. Usually either 1, 2, or 4. + /// public abstract int NumberOfChannels { get; } + /// + /// The array of wireless mics connected to this receiver, the length here is equal to . + /// public abstract IWirelessMic[] WirelessMics { get; } + + /// + /// The model name of the wireless receiver. + /// + public abstract string? ModelName { get; } + /// + /// The name of the manufacturer of the wireless receiver. + /// + public abstract string? Manufacturer { get; } + /// + /// The name of the frequency band the receiver operates on (manufacturer specific). + /// + public abstract string? FreqBand { get; } + /// + /// The range of frequencies the receiver operates on in Hz. + /// + public abstract FrequencyRange[]? FrequencyRanges { get; } + /// + /// The firmware version of the wireless receiver. + /// + public abstract string? FirmwareVersion { get; } + + /// + /// The current IP address of the wireless receiver. + /// + public abstract IPv4Address IPAddress { get; set; } + /// + /// The subnet mask of the wireless receiver. + /// + public abstract IPv4Address? Subnet { get; set; } + /// + /// The network gateway of the wireless receiver. + /// + public abstract IPv4Address? Gateway { get; set; } + /// + /// The network mode of the wireless receiver, either manual network configuration or DHCP. + /// + public abstract IPMode? IPMode { get; set; } + /// + /// The MAC address of the wireless receiver. + /// + public abstract MACAddress? MACAddres { get; } } public interface IWirelessMic : INotifyPropertyChanged { + public const int MAX_METER_SAMPLES = 1024; + public abstract IWirelessMicReceiver Receiver { get; } /// - /// A unnique identifier for this wireless transmitter. Note that this is associated with the + /// A concurrrent queue containing the metering data. + /// + public abstract ConcurrentQueue? MeterData { get; } + public abstract MeteringData? LastMeterData { get; } + + /// + /// A unique identifier for this wireless transmitter. Note that this is associated with the /// physical receiver channel of the receiver unit, not the physical transmitter. /// public abstract uint UID { get; } @@ -45,23 +115,27 @@ public interface IWirelessMic : INotifyPropertyChanged /// /// Transmitter gain/trim in dB. /// - public abstract int Gain { get; set; } + public abstract int? Gain { get; set; } + /// + /// Receiver output gain in dB. + /// + public abstract int? OutputGain { get; set; } /// /// Transmitter mute. /// - public abstract bool Mute { get; set; } + public abstract bool? Mute { get; set; } /// /// Transmitter RF frequency in Hz. /// - public abstract ulong Frequency { get; set; } + public abstract ulong? Frequency { get; set; } /// /// Transmitter frequency group. /// - public abstract int Group { get; set; } + public abstract int? Group { get; set; } /// /// Transmitter frequency channel, within the group. /// - public abstract int Channel { get; set; } + public abstract int? Channel { get; set; } /// /// Transmitter model type identifier, ie: UR1, UR1H, UR2. /// @@ -69,7 +143,7 @@ public interface IWirelessMic : INotifyPropertyChanged /// /// Battery percentage. /// - public abstract float BatteryLevel { get; } + public abstract float? BatteryLevel { get; } //public void Subscribe(); //public void Unsubscribe(); @@ -77,37 +151,71 @@ public interface IWirelessMic : INotifyPropertyChanged public void StopMetering(); } -public struct WirelessMicData : IWirelessMic +/// +/// A data structure representing a single sample of metering data. +/// +/// +/// +/// +public struct MeteringData(float rssiA, float rssiB, float audioLevel) { - [JsonIgnore] public IWirelessMicReceiver Receiver { get; init; } + [JsonInclude] public float rssiA = rssiA; + [JsonInclude] public float rssiB = rssiB; + [JsonInclude] public float audioLevel = audioLevel; +} - [JsonInclude] public readonly uint ReceiverID => Receiver.UID; - [JsonInclude] public uint UID { get; init; } - [JsonInclude] public string? Name { get; set; } - [JsonInclude] public int Gain { get; set; } - [JsonInclude] public bool Mute { get; set; } - [JsonInclude] public ulong Frequency { get; set; } - [JsonInclude] public int Group { get; set; } - [JsonInclude] public int Channel { get; set; } - [JsonInclude] public string? TransmitterType { get; init; } - [JsonInclude] public float BatteryLevel { get; init; } +/// +/// Implements the interface in a struct that's easy to serialize as JSON. +/// +[Serializable] +public struct WirelessReceiverData(IWirelessMicReceiver other) : IWirelessMicReceiver +{ + [JsonIgnore] public IPEndPoint Address { get; init; } = other.Address; + [JsonIgnore] public IWirelessMic[] WirelessMics { get; init; } = other.WirelessMics; + + public uint UID { get; init; } = other.UID; + public int NumberOfChannels { get; init; } = other.NumberOfChannels; + public readonly IEnumerable WirelessMicIDs => WirelessMics.Select(x => x.UID); + public string? ModelName { get; init; } = other.ModelName; + public string? Manufacturer { get; init; } = other.Manufacturer; + public string? FreqBand { get; init; } = other.FreqBand; + public FrequencyRange[]? FrequencyRanges { get; init; } = other.FrequencyRanges; + public string? FirmwareVersion { get; init; } = other.FirmwareVersion; + public IPv4Address IPAddress { get; set; } = other.IPAddress; + public IPv4Address? Subnet { get; set; } = other.Subnet; + public IPv4Address? Gateway { get; set; } = other.Gateway; + public IPMode? IPMode { get; set; } = other.IPMode; + public MACAddress? MACAddres { get; init; } = other.MACAddres; public event PropertyChangedEventHandler? PropertyChanged; - public WirelessMicData(IWirelessMic other) - { - Receiver = other.Receiver; - UID = other.UID; - Name = other.Name; - Gain = other.Gain; - Mute = other.Mute; - Frequency = other.Frequency; - Group = other.Group; - Channel = other.Channel; - TransmitterType = other.TransmitterType; - BatteryLevel = other.BatteryLevel; - } + public void Dispose() { } +} + +/// +/// Implements the interface in a struct that's easy to serialize as JSON. +/// +[Serializable] +public struct WirelessMicData(IWirelessMic other) : IWirelessMic +{ + [JsonIgnore] public IWirelessMicReceiver Receiver { get; init; } = other.Receiver; + [JsonIgnore] public ConcurrentQueue? MeterData { get; init; } = other.MeterData; + [JsonInclude] public MeteringData? LastMeterData { get; init; } = other.LastMeterData; + + [JsonInclude] public readonly uint ReceiverID => Receiver.UID; + [JsonInclude] public uint UID { get; init; } = other.UID; + [JsonInclude] public string? Name { get; set; } = other.Name; + [JsonInclude] public int? Gain { get; set; } = other.Gain; + [JsonInclude] public int? OutputGain { get; set; } = other.OutputGain; + [JsonInclude] public bool? Mute { get; set; } = other.Mute; + [JsonInclude] public ulong? Frequency { get; set; } = other.Frequency; + [JsonInclude] public int? Group { get; set; } = other.Group; + [JsonInclude] public int? Channel { get; set; } = other.Channel; + [JsonInclude] public string? TransmitterType { get; init; } = other.TransmitterType; + [JsonInclude] public float? BatteryLevel { get; init; } = other.BatteryLevel; + + public event PropertyChangedEventHandler? PropertyChanged; public void StartMetering(int periodMS) { @@ -119,3 +227,134 @@ public void StopMetering() throw new NotImplementedException(); } } + +public struct FrequencyRange(ulong startFreq, ulong endFreq) +{ + /// + /// The lower bound of the tunable frequency range in Hz. + /// + [JsonInclude] public ulong startFrequency = startFreq; + /// + /// The upper bound of the tunable frequency range in Hz. + /// + [JsonInclude] public ulong endFrequency = endFreq; +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum IPMode +{ + DHCP, + Manual +} + +[StructLayout(LayoutKind.Explicit)] +[JsonConverter(typeof(JsonStringConverter))] +public struct IPv4Address +{ + [FieldOffset(0)] public byte a; + [FieldOffset(1)] public byte b; + [FieldOffset(2)] public byte c; + [FieldOffset(3)] public byte d; + + [FieldOffset(0)] public uint _data; + + public IPv4Address(uint data) + { + _data = data; + } + + public IPv4Address(byte a, byte b, byte c, byte d, byte e, byte f) + { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + public IPv4Address(IPAddress address) + { + if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + throw new ArgumentException("Expected an IPv4 address!"); + + var dst = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref _data, 1)); + if (!address.TryWriteBytes(dst, out int _)) + throw new ArgumentException($"Failed to convert IP address '{address}' to IPv4Address!"); + } + + /// + /// Parse an IPv4 address in the form 'aaa.bbb.ccc.ddd'. + /// + /// + /// + [JsonConstructor] + public IPv4Address(ReadOnlySpan str) + { + Span seps = stackalloc Range[4]; + int written = str.Split(seps, '.'); + if (written != 4) + throw new ArgumentException($"Input string '{str}' is too short to parse as an IP address!"); + a = byte.Parse(str[seps[0]]); + b = byte.Parse(str[seps[1]]); + c = byte.Parse(str[seps[2]]); + d = byte.Parse(str[seps[3]]); + return; + } + + public override readonly string ToString() + { + return $"{a}.{b}.{c}.{d}"; + } +} + +[StructLayout(LayoutKind.Explicit)] +[JsonConverter(typeof(JsonStringConverter))] +public struct MACAddress +{ + [FieldOffset(0)] public byte a; + [FieldOffset(1)] public byte b; + [FieldOffset(2)] public byte c; + [FieldOffset(3)] public byte d; + [FieldOffset(4)] public byte e; + [FieldOffset(5)] public byte f; + + [FieldOffset(0)] public ulong _data; + + public MACAddress(ulong data) + { + _data = data; + } + + public MACAddress(byte a, byte b, byte c, byte d, byte e, byte f) + { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + } + + /// + /// Parse a MAC address in the form 'aa:bb:cc:dd:ee:ff'. + /// + /// + /// + public MACAddress(ReadOnlySpan str) + { + if (str.Length < 6 * 2 + 5) + throw new ArgumentException($"Input string '{str}' is too short to parse as a MAC address!"); + + var hex = System.Globalization.NumberStyles.HexNumber; + a = byte.Parse(str[0..2], hex); + b = byte.Parse(str[3..5], hex); + c = byte.Parse(str[6..8], hex); + d = byte.Parse(str[9..11], hex); + e = byte.Parse(str[12..14], hex); + f = byte.Parse(str[15..17], hex); + } + + public override readonly string ToString() + { + return $"{a:X2}:{b:X2}:{c:X2}:{d:X2}:{e:X2}:{f:X2}"; + } +} diff --git a/WirelessMicSuiteServer/JsonStringConverter.cs b/WirelessMicSuiteServer/JsonStringConverter.cs new file mode 100644 index 0000000..cc4d864 --- /dev/null +++ b/WirelessMicSuiteServer/JsonStringConverter.cs @@ -0,0 +1,65 @@ +using static System.ComponentModel.TypeConverter; +using System.Collections; +using System.ComponentModel.Design.Serialization; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Text.Json; +using System; + +namespace WirelessMicSuiteServer; + +public class JsonStringConverter : JsonConverterFactory where T : new() +{ + public JsonStringConverter() + { + + } + + /// + public sealed override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T); + + /// + public sealed override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert != typeof(T)) + { + throw new ArgumentOutOfRangeException(); + } + + return new StringConverter(); + } +} + +public class StringConverter : JsonConverter where T : new() +{ + private ConstructorInfo strConstructor; + + 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) + strConstructor = str; + else + throw new ArgumentException("Target type must have a constructor which takes a single string as a parameter!"); + } + + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var s = reader.GetString(); + if (s == null) + return default; + + return (T)strConstructor.Invoke([s]); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString()); + } +} diff --git a/WirelessMicSuiteServer/Program.cs b/WirelessMicSuiteServer/Program.cs index e58a836..65d9830 100644 --- a/WirelessMicSuiteServer/Program.cs +++ b/WirelessMicSuiteServer/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; @@ -19,6 +19,8 @@ public static void Main(string[] args) Log($"Version: {assembly.GetName().Version}; {copyright}"); WirelessMicManager micManager = new([new ShureUHFRManager()]); + if (args.Contains("-m")) + Task.Run(() => MeterTask(micManager)); StartWebServer(args, micManager); } @@ -58,6 +60,74 @@ public static void Log(string? message, LogSeverity severity = LogSeverity.Info) logger.Log(message, severity); } + private static void MeterTask(WirelessMicManager manager) + { + while (true) + { + // This code is very racey, no good way to lock the console logger though... + int i = 0; + int leftPos = Console.BufferWidth - 8 - 1; + int sl = Console.CursorLeft; + int st = Console.CursorTop; + try + { + foreach (var mic in manager.WirelessMics) + { + MeteringData data = mic.LastMeterData ?? default; + DrawMeter(leftPos, 0, i, data); + + i++; + leftPos -= 8; + } + } + catch { } + Console.CursorLeft = sl; + Console.CursorTop = st; + + Thread.Sleep(20); + } + } + + /* + * ┌──────┐ + * │ ║║ ║ │ + * │ ║║ ║ │ + * │ ║║ ║ │ + * │ ║║ ║ │ + * │ ║║ ║ │ + * │ AB L │ + * │ 02_S │ + * └──────┘ + */ + private static void DrawMeter(int left, int top, int index, MeteringData sample) + { + //int width = 8; + //int height = 8; + + Console.CursorLeft = left; + Console.CursorTop = top; + Console.Write("┌──────┐"); + Console.CursorTop++; + Console.CursorLeft = left; + int meterHeight = 10; + for (int i = meterHeight-1; i >= 0; i--) + { + char a = sample.rssiA * (meterHeight - 0.5) > i ? '║' : ' '; + char b = sample.rssiB * (meterHeight - 0.5) > i ? '║' : ' '; + char l = Math.Sqrt(sample.audioLevel) * (meterHeight - 0.5) > i ? '║' : ' '; + Console.Write($"│ {a}{b} {l} │"); + Console.CursorTop++; + Console.CursorLeft = left; + } + Console.Write("│ AB L │"); + Console.CursorTop++; + Console.CursorLeft = left; + Console.Write($"│ {index,4} │"); + Console.CursorTop++; + Console.CursorLeft = left; + Console.Write("└──────┘"); + } + /*public static void Log(object? message, LogSeverity severity = LogSeverity.Info, [CallerMemberName] string? caller = null, string? className = "Main") { lock (logLock) diff --git a/WirelessMicSuiteServer/Properties/launchSettings.json b/WirelessMicSuiteServer/Properties/launchSettings.json index 79a62f1..ab01ecc 100644 --- a/WirelessMicSuiteServer/Properties/launchSettings.json +++ b/WirelessMicSuiteServer/Properties/launchSettings.json @@ -20,6 +20,7 @@ } }, "https": { + //"commandLineArgs": "-m", "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, diff --git a/WirelessMicSuiteServer/ShureUHFRManager.cs b/WirelessMicSuiteServer/ShureUHFR/ShureUHFRManager.cs similarity index 89% rename from WirelessMicSuiteServer/ShureUHFRManager.cs rename to WirelessMicSuiteServer/ShureUHFR/ShureUHFRManager.cs index beef847..5d6e156 100644 --- a/WirelessMicSuiteServer/ShureUHFRManager.cs +++ b/WirelessMicSuiteServer/ShureUHFR/ShureUHFRManager.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using System.Buffers; using System.Buffers.Binary; using System.Collections.Concurrent; using System.Collections.ObjectModel; @@ -31,6 +30,7 @@ public class ShureUHFRManager : IWirelessMicReceiverManager private readonly CancellationToken cancellationToken; private readonly CancellationTokenSource cancellationTokenSource; private readonly Dictionary receiversDict; + private readonly Dictionary micsDict; private readonly ConcurrentQueue txPipe; private readonly SemaphoreSlim txAvailableSem; @@ -43,6 +43,7 @@ public ShureUHFRManager() { Receivers = []; receiversDict = []; + micsDict = []; cancellationTokenSource = new(); cancellationToken = cancellationTokenSource.Token; txPipe = new(); @@ -69,9 +70,35 @@ public void Log(string? message, LogSeverity severity = LogSeverity.Info) logger.Log(message, severity); } - private uint ComputeReceiverUID(uint snetID) + public IWirelessMicReceiver? TryGetWirelessMicReceiver(uint uid) { - return unchecked((uint)HashCode.Combine(snetID, typeof(ShureUHFRReceiver))); + if (receiversDict.TryGetValue(uid, out var receiver)) + return receiver; + return null; + } + + public IWirelessMic? TryGetWirelessMic(uint uid) + { + if (micsDict.TryGetValue(uid, out var mic)) + return mic; + return null; + } + + internal static uint CombineHash(uint a, uint b) + { + unchecked + { + uint hash = 17; + hash = hash * 31 + a; + hash = hash * 31 + b; + return hash; + } + } + + private static uint ComputeReceiverUID(uint snetID) + { + var typeHash = typeof(ShureUHFRReceiver).GUID.GetHashCode(); + return CombineHash(snetID, unchecked((uint)typeHash)); } private void RXTask() @@ -122,7 +149,7 @@ private void RXTask() int charsRead = decoder.GetChars(buffer, ShureSNetHeader.HEADER_SIZE, read-ShureSNetHeader.HEADER_SIZE, charBuffer, 0); Span str = charBuffer.AsSpan()[..charsRead]; - Log($"Received: '{str}'", LogSeverity.Info); + Log($"Received: '{str}'", LogSeverity.Debug); if(receiversDict.TryGetValue(uid, out var rec)) rec.Receive(str, header); @@ -161,11 +188,13 @@ private void PingTask() { foreach (var rem in toRemove) { - if (receiversDict.Remove(rem, out var rec)) + if (receiversDict.TryGetValue(rem, out var rec)) { - Log($"[Discovery] Receiver 0x{rec.UID:X} has not pinged in {ReceiverDisconnectTimeout} ms, marking as disconnected!"); Receivers.Remove(rec); + rec.Dispose(); + Log($"[Discovery] Receiver 0x{rec.UID:X} has not pinged in {ReceiverDisconnectTimeout} ms, marking as disconnected!"); } + receiversDict.Remove(rem); } toRemove.Clear(); } @@ -182,6 +211,16 @@ internal void SendMessage(ByteMessage message) txAvailableSem.Release(); } + internal void RegisterWirelessMic(ShureWirelessMic mic) + { + micsDict.Add(mic.UID, mic); + } + + internal void UnregisterWirelessMic(ShureWirelessMic mic) + { + micsDict.Remove(mic.UID); + } + public void Dispose() { cancellationTokenSource.Cancel(); @@ -194,6 +233,17 @@ public void Dispose() } } +enum ShureCommandType +{ + GET, + SET, + METER, + REPORT, + SAMPLE, + NOTE, + NOTED +} + public readonly record struct ShureSNetHeader { public const int HEADER_SIZE = 16; @@ -271,23 +321,3 @@ public enum SnetType : ushort Message = 3 } } - -public readonly struct ByteMessage : IDisposable -{ - public readonly IPEndPoint endPoint; - public readonly ArraySegment Buffer => new(buffer, 0, length); - private readonly byte[] buffer; - private readonly int length; - - public ByteMessage(IPEndPoint endPoint, int size) - { - this.endPoint = endPoint; - this.length = size; - this.buffer = ArrayPool.Shared.Rent(size); - } - - public void Dispose() - { - ArrayPool.Shared.Return(buffer); - } -} diff --git a/WirelessMicSuiteServer/ShureUHFRReceiver.cs b/WirelessMicSuiteServer/ShureUHFR/ShureUHFRReceiver.cs similarity index 52% rename from WirelessMicSuiteServer/ShureUHFRReceiver.cs rename to WirelessMicSuiteServer/ShureUHFR/ShureUHFRReceiver.cs index 60cb759..fbfed9b 100644 --- a/WirelessMicSuiteServer/ShureUHFRReceiver.cs +++ b/WirelessMicSuiteServer/ShureUHFR/ShureUHFRReceiver.cs @@ -1,6 +1,5 @@ using System.Buffers.Binary; using System.ComponentModel; -using System.Data; using System.Net; using System.Runtime.CompilerServices; @@ -13,12 +12,77 @@ public class ShureUHFRReceiver : IWirelessMicReceiver private readonly uint uid; private readonly uint snetID; + private string? modelName; + private readonly string manufacturer = "Shure"; + private string? freqBand; + private FrequencyRange[]? frequencyRanges; + private string? firmwareVersion; + private IPv4Address ipAddress; + private IPv4Address? subnet; + private IPv4Address? gateway; + private IPMode? ipMode; + private MACAddress? macAddress; + public IPEndPoint Address { get; init; } public DateTime LastPingTime { get; set; } public int NumberOfChannels => mics.Length; public IWirelessMic[] WirelessMics => mics; public uint UID => uid; + public string? ModelName => modelName; + public string? Manufacturer => manufacturer; + public string? FreqBand => freqBand; + public FrequencyRange[]? FrequencyRanges => frequencyRanges; + public string? FirmwareVersion => firmwareVersion; + public IPv4Address IPAddress + { + get => ipAddress; + set + { + SetAsync("IP_ADDR", value.ToString()); + } + } + public IPv4Address? Subnet + { + get => subnet; + set + { + if (value != null) + SetAsync("SUBNET", value.Value.ToString()); + } + } + public IPv4Address? Gateway + { + get => gateway; + set + { + if (value != null) + SetAsync("GATEWAY", value.Value.ToString()); + } + } + public IPMode? IPMode + { + get => ipMode; + set + { + if (value != null) + { + switch (value) + { + case WirelessMicSuiteServer.IPMode.DHCP: + SetAsync("IP_MODE", "DHCP"); + break; + case WirelessMicSuiteServer.IPMode.Manual: + SetAsync("IP_MODE", "Manual"); + break; + } + } + } + } + public MACAddress? MACAddres => macAddress; + + public event PropertyChangedEventHandler? PropertyChanged; + public ShureUHFRReceiver(ShureUHFRManager manager, IPEndPoint address, uint uid, uint snetID) { LastPingTime = DateTime.UtcNow; @@ -27,9 +91,11 @@ public ShureUHFRReceiver(ShureUHFRManager manager, IPEndPoint address, uint uid, this.snetID = snetID; Address = new(address.Address, address.Port); mics = [ - new ShureWirelessMic(this, unchecked((uint)HashCode.Combine(uid, 1u)), 1), - new ShureWirelessMic(this, unchecked((uint)HashCode.Combine(uid, 2u)), 2) + new ShureWirelessMic(this, ShureUHFRManager.CombineHash(uid, 1u), 1), + new ShureWirelessMic(this, ShureUHFRManager.CombineHash(uid, 2u), 2) ]; + manager.RegisterWirelessMic(mics[0]); + manager.RegisterWirelessMic(mics[1]); SendDiscoveryMsg(); SendStartupMessages(); @@ -43,7 +109,11 @@ public void Log(string? message, LogSeverity severity = LogSeverity.Info) logger.Log(message, severity); } - public void Dispose() { } + public void Dispose() + { + manager.UnregisterWirelessMic(mics[0]); + manager.UnregisterWirelessMic(mics[1]); + } private void SendStartupMessages() { @@ -126,6 +196,11 @@ public void Receive(ReadOnlySpan msg, ShureSNetHeader header) type = ShureCommandType.NOTE; msg = msg[4..]; } + else if (msg.StartsWith("UPDATE")) + { + // This is just an acknowledgement of the UPDATE command, we can safely ignore it... + return; + } else { Log($"Unknown command type '{msg.ToString().Split(' ')[0]}'", LogSeverity.Warning); @@ -136,13 +211,13 @@ public void Receive(ReadOnlySpan msg, ShureSNetHeader header) int note = -1; if (type == ShureCommandType.NOTE) { - int noteEnd = msg[1..].IndexOf(' '); + int noteEnd = msg[1..].IndexOf(' ')+1; if (!int.TryParse(msg[1..noteEnd], out note)) { - Log($"Error parsing note numebr in '{fullMsg}'", LogSeverity.Warning); + Log($"Error parsing note number in '{fullMsg}'", LogSeverity.Warning); return; } - msg = msg[(noteEnd + 1)..]; + msg = msg[noteEnd..]; } // This is followed by 1 or 2 for the receiver number @@ -184,141 +259,6 @@ public void Receive(ReadOnlySpan msg, ShureSNetHeader header) } } - private void ParseCommand(ShureCommandType type, ReadOnlySpan cmd, ReadOnlySpan args, ReadOnlySpan fullMsg) - { - - } - - private void CommandError(ReadOnlySpan str) - { - Log($"Unsupported command '{str}'", LogSeverity.Warning); - } -} - -enum ShureCommandType -{ - GET, - SET, - METER, - REPORT, - SAMPLE, - NOTE, - NOTED -} - -public class ShureWirelessMic : IWirelessMic -{ - private readonly ShureUHFRReceiver receiver; - private readonly int receiverNo; - - private uint uid; - private string? name; - private int gain; - private bool mute; - private ulong frequency; - private int group; - private int channel; - private string? transmitterType; - private float batteryLevel; - - public IWirelessMicReceiver Receiver => receiver; - - public uint UID => uid; - public string? Name - { - get => name; - set - { - if (value != null && value.Length < 12) - SetAsync("CHAN_NAME", value); - } - } - public int Gain - { - get => gain; - set - { - if (value >= 0 && value <= 32) - SetAsync("GAIN", value.ToString("00")); - } - } - public bool Mute - { - get => mute; - set - { - SetAsync("MUTE", value ? "ON" : "OFF"); - } - } - public ulong Frequency - { - get => frequency; - set - { - SetAsync("FREQUENCY", (value / 1000).ToString("000000")); - } - } - public int Group - { - get => group; - set - { - SetAsync("GROUP_CHAN", $"{value:00} {channel:00}"); - } - } - public int Channel - { - get => channel; - set - { - SetAsync("GROUP_CHAN", $"{group:00} {value:00}"); - } - } - public string? TransmitterType => transmitterType; - public float BatteryLevel => batteryLevel; - - public event PropertyChangedEventHandler? PropertyChanged; - - private readonly ILogger logger = Program.LoggerFac.CreateLogger(); - public void Log(string? message, LogSeverity severity = LogSeverity.Info) - { - logger.Log(message, severity); - } - - public ShureWirelessMic(ShureUHFRReceiver receiver, uint uid, int receiverNo) - { - this.receiver = receiver; - this.uid = uid; - this.receiverNo = receiverNo; - - SendStartupCommands(); - } - - private void SendStartupCommands() - { - receiver.Send($"* METER {receiverNo} ALL 500 *"); - receiver.Send($"* UPDATE {receiverNo} ADD *"); - - receiver.Send($"* GET {receiverNo} CHAN_NAME *"); - receiver.Send($"* GET {receiverNo} FRONT_PANEL_LOCK *"); - receiver.Send($"* GET {receiverNo} AUDIO_GAIN *"); - receiver.Send($"* GET {receiverNo} MUTE *"); - receiver.Send($"* GET {receiverNo} SQUELCH *"); - receiver.Send($"* GET {receiverNo} GROUP_CHAN *"); - receiver.Send($"* GET {receiverNo} FREQUENCY *"); - receiver.Send($"* GET {receiverNo} TX_IR_LOCK *"); - receiver.Send($"* GET {receiverNo} TX_IR_GAIN *"); - receiver.Send($"* GET {receiverNo} TX_IR_POWER *"); - receiver.Send($"* GET {receiverNo} TX_IR_TRIM *"); - receiver.Send($"* GET {receiverNo} TX_IR_BAT_TYPE *"); - receiver.Send($"* GET {receiverNo} TX_IR_CUSTOM_GPS *"); - receiver.Send($"* GET {receiverNo} AUDIO_INDICATOR *"); - receiver.Send($"* GET {receiverNo} TX_TYPE *"); - receiver.Send($"* GET {receiverNo} TX_BAT *"); - receiver.Send($"* GET {receiverNo} TX_BAT_MINS *"); - receiver.Send($"* GET {receiverNo} TX_POWER *"); - } - private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); @@ -326,112 +266,129 @@ private void OnPropertyChanged([CallerMemberName] string? propertyName = null) private void SetAsync(string cmd, string args) { - receiver.Send($"* SET {receiverNo} {cmd} {args} *"); + Send($"* SET {cmd} {args} *"); } - public void StartMetering(int periodMS) - { - throw new NotImplementedException(); - } - - public void StopMetering() - { - throw new NotImplementedException(); - } - - internal void ParseCommand(ShureCommandType type, ReadOnlySpan cmd, ReadOnlySpan args, ReadOnlySpan fullMsg) + private void ParseCommand(ShureCommandType type, ReadOnlySpan cmd, ReadOnlySpan args, ReadOnlySpan fullMsg) { - if (type == ShureCommandType.SAMPLE) - { - CommandError(fullMsg); - } - else if (type != ShureCommandType.REPORT && type != ShureCommandType.NOTE) + if (type != ShureCommandType.REPORT && type != ShureCommandType.NOTE) { CommandError(fullMsg); return; } switch (cmd) { - case "CHAN_NAME": - name = args.ToString(); - OnPropertyChanged(nameof(Name)); + case "MODEL_NAME": + modelName = args.ToString(); + OnPropertyChanged(nameof(ModelName)); break; - case "MUTE": - if (args.SequenceEqual("ON")) + case "FREQ_BAND": + freqBand = args.ToString(); + OnPropertyChanged(nameof(FreqBand)); + break; + case "BANDLIMITS": { - mute = true; - OnPropertyChanged(nameof(Mute)); + Span seps = stackalloc Range[4]; + int words = args.Split(seps, ' '); + if (words != 4) + { + CommandError(fullMsg, "Expected 4 args for band limits!"); + return; + } + try + { + frequencyRanges = [ + new FrequencyRange(ulong.Parse(args[seps[0]]), ulong.Parse(args[seps[1]])), + new FrequencyRange(ulong.Parse(args[seps[2]]), ulong.Parse(args[seps[3]])) + ]; + OnPropertyChanged(nameof(FrequencyRanges)); + } + catch + { + CommandError(fullMsg, "Expected 4 uints for band limits!"); + return; + } + break; } - else if (args.SequenceEqual("OFF")) + case "SW_VERSION": + firmwareVersion = args.ToString(); + OnPropertyChanged(nameof(FirmwareVersion)); + break; + case "MAC_ADDR": + try + { + macAddress = new MACAddress(args); + OnPropertyChanged(nameof(MACAddres)); + } catch { - mute = false; - OnPropertyChanged(nameof(Mute)); + CommandError(fullMsg, "Expected a MAC address in the form aa:bb:cc:dd:ee:ff!"); + return; } - else - CommandError(fullMsg); break; - case "AUDIO_GAIN": - if (int.TryParse(args, out int ngain) && ngain is >= 0 and <= 32) + case "IP_MODE": + if (args.SequenceEqual("DHCP")) + { + ipMode = WirelessMicSuiteServer.IPMode.DHCP; + OnPropertyChanged(nameof(IPMode)); + } else if (args.SequenceEqual("Manual")) + { + ipMode = WirelessMicSuiteServer.IPMode.Manual; + OnPropertyChanged(nameof(IPMode)); + } else { - gain = ngain; - OnPropertyChanged(nameof(Gain)); - } - else CommandError(fullMsg); + return; + } break; - case "GROUP_CHAN": - int sep = args.IndexOf(' '); - if (sep != -1 && int.TryParse(args[..sep], out int g) && int.TryParse(args[(sep + 1)..], out int c)) + case "CURRENT_IP_ADDR": + try { - group = g; - channel = c; - OnPropertyChanged(nameof(Group)); - OnPropertyChanged(nameof(Channel)); - } else if (args.SequenceEqual("-- --")) + ipAddress = new IPv4Address(args); + OnPropertyChanged(nameof(IPAddress)); + } + catch { - group = -1; - channel = -1; - OnPropertyChanged(nameof(Group)); - OnPropertyChanged(nameof(Channel)); + CommandError(fullMsg, "Expected an IP address in the form aaa.bbb.ccc.ddd!"); + return; } - else - CommandError(fullMsg); break; - case "FREQUENCY": - if (ulong.TryParse(args, out ulong freq)) + case "CURRENT_SUBNET": + try { - frequency = freq*1000; - OnPropertyChanged(nameof(Frequency)); - } else - CommandError(fullMsg); - break; - case "TX_BAT": - if (args.Length < 1) + subnet = new IPv4Address(args); + OnPropertyChanged(nameof(Subnet)); + } + catch { - CommandError(fullMsg); - break; + CommandError(fullMsg, "Expected an IP address in the form aaa.bbb.ccc.ddd!"); + return; } - if (args[0] == 'U') + break; + case "CURRENT_GATEWAY": + try { - batteryLevel = 0; - OnPropertyChanged(nameof(BatteryLevel)); + gateway = new IPv4Address(args); + OnPropertyChanged(nameof(Gateway)); } - else + catch { - int level = (byte)args[0] - (byte)'0'; - if (level is >= 1 and <= 5) - { - batteryLevel = level/5f; - OnPropertyChanged(nameof(BatteryLevel)); - } else - { - CommandError(fullMsg, "Battery level out of range 1-5."); - } + CommandError(fullMsg, "Expected an IP address in the form aaa.bbb.ccc.ddd!"); + return; } break; - case "TX_TYPE": - transmitterType = args.ToString(); - OnPropertyChanged(nameof(TransmitterType)); + case "HARDWARE_ID": + case "IP_ADDR": + case "SUBNET": + case "GATEWAY": + case "CUSTOM_GROUP_C1": + case "CUSTOM_GROUP_C2": + case "CUSTOM_GROUP_C3": + case "CUSTOM_GROUP_C4": + case "CUSTOM_GROUP_C5": + case "CUSTOM_GROUP_C6": + break; + default: + CommandError(fullMsg); break; } } diff --git a/WirelessMicSuiteServer/ShureUHFR/ShureWirelessMic.cs b/WirelessMicSuiteServer/ShureUHFR/ShureWirelessMic.cs new file mode 100644 index 0000000..58edd36 --- /dev/null +++ b/WirelessMicSuiteServer/ShureUHFR/ShureWirelessMic.cs @@ -0,0 +1,365 @@ +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace WirelessMicSuiteServer; + +public class ShureWirelessMic : IWirelessMic +{ + private readonly ShureUHFRReceiver receiver; + private readonly int receiverNo; + private readonly ConcurrentQueue meterData; + private MeteringData? lastMeterData; + + private readonly uint uid; + private string? name; + private int? gain; + private int? outputGain; + private bool? mute; + private ulong? frequency; + private int? group; + private int? channel; + private string? transmitterType; + private float? batteryLevel; + + public IWirelessMicReceiver Receiver => receiver; + public ConcurrentQueue MeterData => meterData; + public MeteringData? LastMeterData => lastMeterData; + + public uint UID => uid; + public string? Name + { + get => name; + set + { + if (value != null && value.Length < 12) + SetAsync("CHAN_NAME", value); + } + } + public int? Gain + { + get => gain; + set + { + if (value != null && value >= 0 && value <= 32) + SetAsync("TX_IR_GAIN", value.Value.ToString()); + } + } + public int? OutputGain + { + get => outputGain; + set + { + if (value != null && value >= 0 && value <= 32) + SetAsync("AUDIO_GAIN", value.Value.ToString()); + } + } + public bool? Mute + { + get => mute; + set + { + if (value != null) + SetAsync("MUTE", value.Value ? "ON" : "OFF"); + } + } + public ulong? Frequency + { + get => frequency; + set + { + if (value != null) + SetAsync("FREQUENCY", (value.Value / 1000).ToString("000000")); + } + } + public int? Group + { + get => group; + set + { + if (value != null) + SetAsync("GROUP_CHAN", $"{value:00} {channel:00}"); + } + } + public int? Channel + { + get => channel; + set + { + if (value != null) + SetAsync("GROUP_CHAN", $"{group:00} {value:00}"); + } + } + public string? TransmitterType => transmitterType; + public float? BatteryLevel => batteryLevel; + + public event PropertyChangedEventHandler? PropertyChanged; + + private readonly ILogger logger = Program.LoggerFac.CreateLogger(); + public void Log(string? message, LogSeverity severity = LogSeverity.Info) + { + logger.Log(message, severity); + } + + public ShureWirelessMic(ShureUHFRReceiver receiver, uint uid, int receiverNo) + { + this.receiver = receiver; + this.uid = uid; + this.receiverNo = receiverNo; + meterData = []; + + SendStartupCommands(); + } + + private void SendStartupCommands() + { + receiver.Send($"* METER {receiverNo} ALL 1 *"); + receiver.Send($"* UPDATE {receiverNo} ADD *"); + + receiver.Send($"* GET {receiverNo} CHAN_NAME *"); + receiver.Send($"* GET {receiverNo} FRONT_PANEL_LOCK *"); + receiver.Send($"* GET {receiverNo} AUDIO_GAIN *"); + receiver.Send($"* GET {receiverNo} MUTE *"); + receiver.Send($"* GET {receiverNo} SQUELCH *"); + receiver.Send($"* GET {receiverNo} GROUP_CHAN *"); + receiver.Send($"* GET {receiverNo} FREQUENCY *"); + receiver.Send($"* GET {receiverNo} TX_IR_LOCK *"); + receiver.Send($"* GET {receiverNo} TX_IR_GAIN *"); + receiver.Send($"* GET {receiverNo} TX_IR_POWER *"); + receiver.Send($"* GET {receiverNo} TX_IR_TRIM *"); + receiver.Send($"* GET {receiverNo} TX_IR_BAT_TYPE *"); + receiver.Send($"* GET {receiverNo} TX_IR_CUSTOM_GPS *"); + receiver.Send($"* GET {receiverNo} AUDIO_INDICATOR *"); + receiver.Send($"* GET {receiverNo} TX_TYPE *"); + receiver.Send($"* GET {receiverNo} TX_BAT *"); + receiver.Send($"* GET {receiverNo} TX_BAT_MINS *"); + receiver.Send($"* GET {receiverNo} TX_POWER *"); + } + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void SetAsync(string cmd, string args) + { + receiver.Send($"* SET {receiverNo} {cmd} {args} *"); + } + + public void StartMetering(int periodMS) + { + throw new NotImplementedException(); + } + + public void StopMetering() + { + throw new NotImplementedException(); + } + + internal void ParseCommand(ShureCommandType type, ReadOnlySpan cmd, ReadOnlySpan args, ReadOnlySpan fullMsg) + { + if (type == ShureCommandType.SAMPLE) + { + ParseSampleCommand(args, fullMsg); + return; + } + else if (type != ShureCommandType.REPORT && type != ShureCommandType.NOTE) + { + CommandError(fullMsg, $"Unexpected command type '{type}'."); + return; + } + switch (cmd) + { + case "CHAN_NAME": + name = args.ToString(); + OnPropertyChanged(nameof(Name)); + break; + case "MUTE": + if (args.SequenceEqual("ON")) + { + mute = true; + OnPropertyChanged(nameof(Mute)); + } + else if (args.SequenceEqual("OFF")) + { + mute = false; + OnPropertyChanged(nameof(Mute)); + } + else + CommandError(fullMsg, "Mute value wasn't 'ON' or 'OFF'."); + break; + case "AUDIO_GAIN": + if (int.TryParse(args, out int nOutGain) && nOutGain is >= 0 and <= 32) + { + outputGain = -nOutGain; + OnPropertyChanged(nameof(OutputGain)); + } + else + CommandError(fullMsg, "Couldn't parse output gain, or gain was out of the range 0-32."); + break; + case "TX_GAIN": + case "TX_IR_GAIN": + if (int.TryParse(args, out int ngain) && ngain is >= -10 and <= 20) + { + gain = ngain; + OnPropertyChanged(nameof(Gain)); + } + else if (args.SequenceEqual("UNKNOWN")) + { + gain = null; + OnPropertyChanged(nameof(Gain)); + } + else + CommandError(fullMsg, "Couldn't parse transmitter gain, or gain was out of the range -10:20."); + break; + case "SQUELCH": + if (int.TryParse(args, out int nsquelch)) + { + //outputGain = -ngain; + //OnPropertyChanged(nameof(OutputGain)); + } + else + CommandError(fullMsg, "Couldn't parse squelch."); + break; + case "GROUP_CHAN": + int sep = args.IndexOf(' '); + if (sep != -1 && int.TryParse(args[..sep], out int g) && int.TryParse(args[(sep + 1)..], out int c)) + { + group = g; + channel = c; + OnPropertyChanged(nameof(Group)); + OnPropertyChanged(nameof(Channel)); + } else if (args.SequenceEqual("-- --")) + { + group = null; + channel = null; + OnPropertyChanged(nameof(Group)); + OnPropertyChanged(nameof(Channel)); + } + else + CommandError(fullMsg, "Couldn't parse group or channel arguments."); + break; + case "FREQUENCY": + if (ulong.TryParse(args, out ulong freq)) + { + frequency = freq*1000; + OnPropertyChanged(nameof(Frequency)); + } else + CommandError(fullMsg, "Couldn't parse frequency as a ulong."); + break; + case "TX_BAT": + if (args.Length < 1) + { + CommandError(fullMsg, "Expected argument in TX_BAT command."); + break; + } + if (args[0] == 'U') + { + batteryLevel = 0; + OnPropertyChanged(nameof(BatteryLevel)); + } + else + { + int level = (byte)args[0] - (byte)'0'; + if (level is >= 1 and <= 5) + { + batteryLevel = level/5f; + OnPropertyChanged(nameof(BatteryLevel)); + } else + { + CommandError(fullMsg, "Battery level out of range 1-5."); + } + } + break; + case "TX_TYPE": + transmitterType = args.ToString(); + OnPropertyChanged(nameof(TransmitterType)); + break; + case "FRONT_PANEL_LOCK": + case "TX_IR_LOCK": + case "TX_IR_POWER": + case "TX_IR_TRIM": + case "TX_IR_BAT_TYPE": + case "TX_IR_CUSTOM_GPS": + case "AUDIO_INDICATOR": + case "TX_BAT_MINS": + case "TX_BAT_TYPE": + case "TX_POWER": + case "TX_CHANGE_BAT": + case "TX_EXT_DC": + case "TX_TRIM": + case "TX_LOCK": + // Unimplemented for now + break; + default: + CommandError(fullMsg); + break; + } + } + + private void ParseSampleCommand(ReadOnlySpan args, ReadOnlySpan fullMsg) + { + Span seps = stackalloc Range[16]; + int n = args.Split(seps, ' '); + seps = seps[..n]; + + float rssiA = -1; + float rssiB = -1; + float audio = -1; + for (int i = 0; i < seps.Length; i++) + { + var word = args[seps[i]]; + switch (word) + { + case "RSSI": + if (int.TryParse(args[seps[++i]], out int rssiAInt) + && int.TryParse(args[seps[++i]], out int rssiBInt)) + { + rssiA = 1 - (rssiAInt - 50) / 50f; + rssiB = 1 - (rssiBInt - 50) / 50f; + } + else + { + CommandError(fullMsg, "Couldn't parse RF strength."); + return; + } + break; + case "AUDIO_INDICATOR": + if (int.TryParse(args[seps[++i]], out int audioInt)) + { + audio = audioInt / 255f; + } + else + { + CommandError(fullMsg, "Couldn't parse audio level."); + return; + } + break; + default: + break; + } + } + + if (meterData.Count > IWirelessMic.MAX_METER_SAMPLES) + { + // Purge the last 16 samples if the meter queue is getting full... + for (int i = 0; i < 16; i++) + meterData.TryDequeue(out _); + } + var meter = new MeteringData(rssiA, rssiB, audio); + meterData.Enqueue(meter); + lastMeterData = meter; + } + + private void CommandError(ReadOnlySpan str, string? details = null) + { + if (details != null) + { + Log($"Error while parsing command '{str}'. {details}", LogSeverity.Warning); + } + else + { + Log($"Error while parsing command '{str}'", LogSeverity.Warning); + } + } +} diff --git a/WirelessMicSuiteServer/WebAPI.cs b/WirelessMicSuiteServer/WebAPI.cs index 0c4402f..fae76db 100644 --- a/WirelessMicSuiteServer/WebAPI.cs +++ b/WirelessMicSuiteServer/WebAPI.cs @@ -1,21 +1,80 @@ -namespace WirelessMicSuiteServer; +using System; +using System.Text; + +namespace WirelessMicSuiteServer; public static class WebAPI { public static void AddWebRoots(WebApplication app, WirelessMicManager micManager) { - /*app.MapGet("/wirelessReceivers", (HttpContext ctx) => + app.MapGet("/getWirelessReceivers", (HttpContext ctx) => { - return micManager.Receivers; + return micManager.Receivers.Select(x => new WirelessReceiverData(x)); }).WithName("GetWirelessReceivers") - .WithOpenApi();*/ + .WithOpenApi(); - app.MapGet("/wirelessMics", (HttpContext ctx) => + app.MapGet("/getWirelessMics", (HttpContext ctx) => { return micManager.WirelessMics.Select(x => new WirelessMicData(x)); }).WithName("GetWirelessMics") .WithOpenApi(); - //app.MapGet("/wirelessMic") + app.MapGet("/getWirelessMicReceiver/{uid}", (uint uid, HttpContext ctx) => + { + var rec = micManager.TryGetWirelessMicReceiver(uid); + if (rec == null) + return new WirelessReceiverData?(); + return new WirelessReceiverData(rec); + }).WithName("getWirelessMicReceiver") + .WithOpenApi(); + + app.MapGet("/getWirelessMic/{uid}", (uint uid, HttpContext ctx) => + { + var mic = micManager.TryGetWirelessMic(uid); + if (mic == null) + return new WirelessMicData?(); + return new WirelessMicData(mic); + }).WithName("GetWirelessMic") + .WithOpenApi(); + + app.MapGet("/getMicMeter/{uid}", (uint uid, HttpContext ctx) => + { + var mic = micManager.TryGetWirelessMic(uid); + if (mic == null || mic.MeterData == null) + return null; + + var samples = mic.MeterData.ToArray(); + // By not locking the collection, some samples could be lost... + mic.MeterData.Clear(); + + return samples; + }).WithName("GetMicMeter") + .WithOpenApi(); + + app.MapGet("/getMicMeterAscii/{uid}", (uint uid, HttpContext ctx) => + { + var mic = micManager.TryGetWirelessMic(uid); + if (mic == null || mic.MeterData == null) + return ""; + + var sample = mic.LastMeterData ?? default; + + int meterHeight = 10; + StringBuilder sb = new(); + sb.AppendLine("┌──────┐"); + for (int i = meterHeight - 1; i >= 0; i--) + { + char a = sample.rssiA * (meterHeight - 0.5) > i ? '║' : ' '; + char b = sample.rssiB * (meterHeight - 0.5) > i ? '║' : ' '; + char l = Math.Sqrt(sample.audioLevel) * (meterHeight - 0.5) > i ? '║' : ' '; + sb.AppendLine($"│ {a}{b} {l} │"); + } + sb.AppendLine("│ AB L │"); + sb.AppendLine($"│ {uid.ToString()[..4],4} │"); + sb.AppendLine("└──────┘"); + + return sb.ToString(); + }).WithName("GetMicMeterAscii") + .WithOpenApi(); } } diff --git a/WirelessMicSuiteServer/WirelessMicManager.cs b/WirelessMicSuiteServer/WirelessMicManager.cs index 41a5b0c..653f5de 100644 --- a/WirelessMicSuiteServer/WirelessMicManager.cs +++ b/WirelessMicSuiteServer/WirelessMicManager.cs @@ -5,17 +5,15 @@ namespace WirelessMicSuiteServer; public class WirelessMicManager : IDisposable { - private bool shouldQuit; - //private readonly Dictionary receivers; private readonly List receiverManagers; public IEnumerable Receivers => receiverManagers.SelectMany(x=>x.Receivers); + // TODO: There's a race condition where if receivers get added or removed while this is being enumerated an exception is thrown. public IEnumerable WirelessMics => new WirelessMicEnumerator(Receivers); public WirelessMicManager(IEnumerable? receiverManagers) { this.receiverManagers = receiverManagers?.ToList() ?? []; - //receivers = []; } private readonly ILogger logger = Program.LoggerFac.CreateLogger(); @@ -26,11 +24,32 @@ public void Log(string? message, LogSeverity severity = LogSeverity.Info) public void Dispose() { - shouldQuit = true; foreach (var man in receiverManagers) man.Dispose(); } + public IWirelessMicReceiver? TryGetWirelessMicReceiver(uint uid) + { + foreach (var manager in receiverManagers) + { + var r = manager.TryGetWirelessMicReceiver(uid); + if (r != null) + return r; + } + return null; + } + + public IWirelessMic? TryGetWirelessMic(uint uid) + { + foreach (var manager in receiverManagers) + { + var r = manager.TryGetWirelessMic(uid); + if (r != null) + return r; + } + return null; + } + private struct WirelessMicEnumerator : IEnumerable, IEnumerator, IEnumerator { private readonly IEnumerator receivers;