Skip to content

Commit

Permalink
API improvements and features:
Browse files Browse the repository at this point in the history
 - Added some extra try/catches to prevent failures
 - Added support for special sNet messages
 - Added identify command
 - Added reboot command
 - [Shure] Returned frequency range is now correctly in Hz
 - Added diversity indicator to metering data
 - Fixed CLI option parsing
 - Improved API docs
 - Added static file host
 - Added file saving web API
 - Other minor improvements
  • Loading branch information
space928 committed Jun 29, 2024
1 parent ff20f7b commit 11b848c
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 69 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,7 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd

# Default save directory
WirelessMicSuiteServer/save/
42 changes: 36 additions & 6 deletions WirelessMicSuiteServer/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public void ParseArgs(string[] args)
Log($"Unrecognised command line argument '{arg}'!", LogSeverity.Error);
Console.WriteLine();
PrintHelp();
parsedArgs.TryAdd("--help", null);
return;
}

Expand Down Expand Up @@ -83,11 +84,18 @@ public void ParseArgs(string[] args)
}

if (!option.Value.multipleArguments)
{
AddParsedArg(option, tmpList);
option = null;
}
}

AddParsedArg(option, tmpList);

foreach (var arg in options.Where(x => x.Key == x.Value.key).Select(x => x.Value))
if (arg.defaultValue != null && !parsedArgs.ContainsKey(arg.key))
parsedArgs.Add(arg.key, arg.defaultValue);

void AddParsedArg(CommandLineOption? option, List<object> tmpList)
{
if (option is CommandLineOption opt)
Expand All @@ -103,30 +111,52 @@ 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($" version: {assembly.GetName().Version}");
Console.WriteLine($"");
Console.WriteLine($"Command line options: ");
foreach (var kvp in options)
{
if (kvp.Key != kvp.Value.key)
continue;
var opt = kvp.Value;
Console.WriteLine($"\t{opt.key}{(opt.alias != null?", " + opt.alias:"")} " +
$"{(opt.argType != CommandLineArgType.None ? "<"+opt.argType+">":"")}{(opt.multipleArguments?", ...":"")}");
if (opt.defaultValue != null)
Console.WriteLine($"\t\tDefault: {opt.defaultValue}");
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<object?>? action = null)
CommandLineArgType argType = CommandLineArgType.None, bool multipleArguments = false,
object? defaultValue = null, Action<object?>? action = null)
{
public string key = key;
public string? alias = alias;
public string? help = help;
public CommandLineArgType argType = argType;
public bool multipleArguments = multipleArguments;
public object? defaultValue = CastToArg(defaultValue, argType);
//public bool positional;
public Action<object?>? action = action;

private static object? CastToArg(object? arg, CommandLineArgType argType)
{
if (arg == null)
return null;

return argType switch
{
CommandLineArgType.None => null,
CommandLineArgType.String => (string)arg,
CommandLineArgType.Int => (int)arg,
CommandLineArgType.Uint => (uint)arg,
CommandLineArgType.Path => (string)arg,
_ => throw new NotImplementedException(),
};
}
}

public enum CommandLineArgType
Expand Down
58 changes: 57 additions & 1 deletion WirelessMicSuiteServer/IWirelessMicReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ public interface IWirelessMicReceiver : IDisposable, INotifyPropertyChanged
/// The MAC address of the wireless receiver.
/// </summary>
public abstract MACAddress? MACAddress { get; }

/// <summary>
/// Flashses the LEDs on the device to identify it.
/// </summary>
public void Identify();
/// <summary>
/// Sends a reboot command to the wireless receiver.
/// </summary>
public void Reboot();
}

public interface IWirelessMic : INotifyPropertyChanged
Expand Down Expand Up @@ -160,21 +169,37 @@ public interface IWirelessMic : INotifyPropertyChanged
public Task<RFScanData> StartRFScan(FrequencyRange range, ulong stepSize);
}

/// <summary>
/// Represents which antenna(e) is currently active.
/// </summary>
[Flags]
public enum DiversityIndicator
{
None = 0,
A = 1 << 0,
B = 1 << 1,
C = 1 << 2,
D = 1 << 3,
}

/// <summary>
/// A data structure representing a single sample of metering data.
/// </summary>
/// <param name="rssiA"></param>
/// <param name="rssiB"></param>
/// <param name="audioLevel"></param>
public struct MeteringData(float rssiA, float rssiB, float audioLevel)
/// <param name="diversity"></param>
public struct MeteringData(float rssiA, float rssiB, float audioLevel, DiversityIndicator diversity)
{
[JsonIgnore] public float rssiA = rssiA;
[JsonIgnore] public float rssiB = rssiB;
[JsonIgnore] public float audioLevel = audioLevel;
[JsonIgnore] public DiversityIndicator diversity = diversity;

public readonly float RssiA => rssiA;
public readonly float RssiB => rssiB;
public readonly float AudioLevel => audioLevel;
public readonly DiversityIndicator Diversity => diversity;
}

/// <summary>
Expand All @@ -186,24 +211,42 @@ public struct WirelessReceiverData(IWirelessMicReceiver other) : IWirelessMicRec
[JsonIgnore] public IPEndPoint Address { get; init; } = other.Address;
[JsonIgnore] public IWirelessMic[] WirelessMics { get; init; } = other.WirelessMics;

/// <inheritdoc />
public uint UID { get; init; } = other.UID;

/// <inheritdoc />
public int NumberOfChannels { get; init; } = other.NumberOfChannels;
/// <inheritdoc />
public readonly IEnumerable<uint> WirelessMicIDs => WirelessMics.Select(x => x.UID);
/// <inheritdoc />
public string? ModelName { get; init; } = other.ModelName;
/// <inheritdoc />
public string? Manufacturer { get; init; } = other.Manufacturer;
/// <inheritdoc />
public string? FreqBand { get; init; } = other.FreqBand;
/// <inheritdoc />
public FrequencyRange[]? FrequencyRanges { get; init; } = other.FrequencyRanges;
/// <inheritdoc />
public string? FirmwareVersion { get; init; } = other.FirmwareVersion;
/// <inheritdoc />
public IPv4Address IPAddress { get; set; } = other.IPAddress;
/// <inheritdoc />
public IPv4Address? Subnet { get; set; } = other.Subnet;
/// <inheritdoc />
public IPv4Address? Gateway { get; set; } = other.Gateway;
/// <inheritdoc />
public IPMode? IPMode { get; set; } = other.IPMode;
/// <inheritdoc />
public MACAddress? MACAddress { get; init; } = other.MACAddress;

public event PropertyChangedEventHandler? PropertyChanged;

public void Dispose() { }

/// <inheritdoc />
public void Identify() { }
/// <inheritdoc />
public void Reboot() { }
}

/// <summary>
Expand All @@ -217,16 +260,29 @@ public struct WirelessMicData(IWirelessMic other) : IWirelessMic
[JsonIgnore] public RFScanData RFScanData { get; init; } = other.RFScanData;
[JsonInclude] public MeteringData? LastMeterData { get; init; } = other.LastMeterData;

/// <summary>
/// The UID of the receiver this mic is connected too.
/// </summary>
[JsonInclude] public readonly uint ReceiverID => Receiver.UID;
/// <inheritdoc />
[JsonInclude] public uint UID { get; init; } = other.UID;
/// <inheritdoc />
[JsonInclude] public string? Name { get; set; } = other.Name;
/// <inheritdoc />
[JsonInclude] public int? Gain { get; set; } = other.Gain;
/// <inheritdoc />
[JsonInclude] public int? OutputGain { get; set; } = other.OutputGain;
/// <inheritdoc />
[JsonInclude] public bool? Mute { get; set; } = other.Mute;
/// <inheritdoc />
[JsonInclude] public ulong? Frequency { get; set; } = other.Frequency;
/// <inheritdoc />
[JsonInclude] public int? Group { get; set; } = other.Group;
/// <inheritdoc />
[JsonInclude] public int? Channel { get; set; } = other.Channel;
/// <inheritdoc />
[JsonInclude] public string? TransmitterType { get; init; } = other.TransmitterType;
/// <inheritdoc />
[JsonInclude] public float? BatteryLevel { get; init; } = other.BatteryLevel;

public event PropertyChangedEventHandler? PropertyChanged;
Expand Down
32 changes: 27 additions & 5 deletions WirelessMicSuiteServer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Microsoft.AspNetCore.RateLimiting;
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
Expand All @@ -14,6 +15,8 @@ public class Program
public static void Main(string[] args)
{
var cli = CreateCommandLineArgs(args);
if (cli.ParsedArgs.ContainsKey("--help"))
return;

Log("Starting Wireless Mic Suite server...");
var assembly = Assembly.GetExecutingAssembly();
Expand All @@ -23,25 +26,27 @@ public static void Main(string[] args)
int meterInterval = 50;
if (cli.ParsedArgs.TryGetValue("--meter-interval", out object? arg))
meterInterval = (int)(uint)arg!;
Log($"List options: \n{string.Join('\n', cli.ParsedArgs.Select(x => $"\t{x.Key} = {x.Value}"))}");

WirelessMicManager micManager = new([
new ShureUHFRManager() { PollingPeriodMS = meterInterval }
]);
WebSocketAPIManager wsAPIManager = new(micManager, meterInterval);
if (cli.ParsedArgs.ContainsKey("--meters"))
Task.Run(() => MeterTask(micManager));
StartWebServer(args, micManager, wsAPIManager);
StartWebServer(args, micManager, wsAPIManager, cli);
}

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.")
new("--meter-interval", "-i", argType: CommandLineArgType.Uint, defaultValue: 50u, help:"Sets the interval at which metering information should be polled from the wireless receivers. This is specified in milli-seconds."),
new("--save-dir", "-s", argType: CommandLineArgType.String, defaultValue: "save", help:"Specifies a directory to save.")
], args);
}

private static void StartWebServer(string[] args, WirelessMicManager micManager, WebSocketAPIManager wsAPIManager)
private static void StartWebServer(string[] args, WirelessMicManager micManager, WebSocketAPIManager wsAPIManager, CommandLineOptions cli)
{
var builder = WebApplication.CreateBuilder(args);
//builder.Logging.ClearProviders();
Expand All @@ -50,6 +55,17 @@ private static void StartWebServer(string[] args, WirelessMicManager micManager,
// Add services to the container.
builder.Services.AddAuthorization();

builder.Services.AddRateLimiter(x =>
{
x.AddFixedWindowLimiter("files", options =>
{
options.PermitLimit = 4;
options.QueueLimit = 8;
options.QueueProcessingOrder = System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst;
options.Window = TimeSpan.FromSeconds(1);
});
});

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
WebAPI.AddSwaggerGen(builder.Services);
Expand All @@ -74,7 +90,13 @@ private static void StartWebServer(string[] args, WirelessMicManager micManager,

app.UseWebSockets(webSocketOptions);

WebAPI.AddWebRoots(app, micManager, wsAPIManager);
//app.Environment.WebRootPath = "static";
app.UseStaticFiles(new StaticFileOptions() { });
app.UseDefaultFiles();

app.UseRateLimiter();

WebAPI.AddWebRoots(app, micManager, wsAPIManager, cli);

app.Run();
}
Expand Down
33 changes: 27 additions & 6 deletions WirelessMicSuiteServer/ShureUHFR/ShureUHFRManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -148,6 +147,11 @@ private void RXTask()
}
}
continue;
} else if (header.type == ShureSNetHeader.SnetType.Special)
{
// string bmsg = string.Join(" ", buffer[ShureSNetHeader.HEADER_SIZE..read].Select(x => x.ToString("X2")));
// Log($"Received special message: '{bmsg}'", LogSeverity.Info);
continue;
}

int charsRead = decoder.GetChars(buffer, ShureSNetHeader.HEADER_SIZE, read-ShureSNetHeader.HEADER_SIZE, charBuffer, 0);
Expand All @@ -166,8 +170,14 @@ private void TXTask()
ByteMessage msg;
while (!txPipe.TryDequeue(out msg))
txAvailableSem.Wait(1000);
socket.SendTo(msg.Buffer, msg.endPoint);
msg.Dispose();
try
{
socket.SendTo(msg.Buffer, msg.endPoint);
msg.Dispose();
} catch (Exception ex)
{
Log($"Error while sending message: {ex.Message}!", LogSeverity.Warning);
}
/*var msg = await txPipe.Reader.ReadAsync(cancellationToken);
foreach (var part in msg.Buffer)
await socket.SendAsync(part);*/
Expand Down Expand Up @@ -292,12 +302,22 @@ public ShureSNetHeader(ReadOnlySpan<byte> bytes)
checksum = BinaryPrimitives.ReadUInt16BigEndian(bytes[14..16]);
}

/// <summary>
/// CRC-16-CCITT lookup table
/// </summary>
static readonly ushort[] CHECKSUM_LUT = [0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240, 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040];
/// <summary>
/// CRC-16-CCITT
/// </summary>
/// <param name="data">The data to checksum.</param>
/// <param name="prev">The initial value, should be 0.</param>
/// <param name="len">How many bytes to checksum.</param>
/// <returns></returns>
public static ushort ComputeChecksum(ReadOnlySpan<byte> data, ushort prev, int len)
{
ushort sum = (ushort)~prev;
if (len == 0)
return (ushort)~sum;
return prev;
ushort sum = (ushort)~prev;

for (int i = 0; i < len; i++)
{
Expand All @@ -324,6 +344,7 @@ public void WriteToSpan(Span<byte> dst)
public enum SnetType : ushort
{
Discovery = 1,
Message = 3
Message = 3,
Special = 4
}
}
Loading

0 comments on commit 11b848c

Please sign in to comment.