Implement Substation: BLE-WS bridge with music-reactive e-stim
- Renamed from CoyoteBridge to Substation (namespace, classes, UI) - BLE connection via WinRT with graceful disconnect handling - WebSocket server (127.0.0.1:8765) with single-client, push events for BLE connect/disconnect transitions - Operator strength limiter (clamp/scale modes, default 30) - Tray icon with 3 states: neutral/active/hot (runtime-drawn voltage glyph) - A/B strength gauges, hide-on-minimise to tray - Music mode: MP3 decode + FFT analysis (NAudio + FftSharp), rhythm→ch A, melody→ch B, pre-analyzed with synced audio playback - Test/Stop All work without BLE connected (dev mode) - WS device-driving commands error when BLE not connected; ping/status/ connect always work - TreatWarningsAsErrors, LangVersion=latest - .gitignore, README with full API docs + attribution
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Substation;
|
||||
|
||||
public static class Freq
|
||||
{
|
||||
public static byte Compress(int ms)
|
||||
{
|
||||
if (ms is >= 10 and <= 100) return (byte)ms;
|
||||
if (ms is > 100 and <= 600) return (byte)((ms - 100) / 5 + 100);
|
||||
if (ms is > 600 and <= 1000) return (byte)((ms - 600) / 10 + 200);
|
||||
return 10;
|
||||
}
|
||||
|
||||
public static byte[] Compress4(int[] ms)
|
||||
{
|
||||
if (ms.Length != 4) throw new ArgumentException("freq must have exactly 4 values", nameof(ms));
|
||||
return new byte[]
|
||||
{
|
||||
Compress(ms[0]), Compress(ms[1]), Compress(ms[2]), Compress(ms[3])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class Intensity
|
||||
{
|
||||
public static byte Clamp(int v) => (byte)Math.Clamp(v, 0, 100);
|
||||
|
||||
public static byte[] Clamp4(int[] v)
|
||||
{
|
||||
if (v.Length != 4) throw new ArgumentException("intensity must have exactly 4 values", nameof(v));
|
||||
return new byte[] { Clamp(v[0]), Clamp(v[1]), Clamp(v[2]), Clamp(v[3]) };
|
||||
}
|
||||
}
|
||||
|
||||
public enum StrengthMode : byte
|
||||
{
|
||||
None = 0b00,
|
||||
Add = 0b01,
|
||||
Sub = 0b10,
|
||||
Abs = 0b11
|
||||
}
|
||||
|
||||
public enum LimitMode
|
||||
{
|
||||
Clamp = 0,
|
||||
Scale = 1
|
||||
}
|
||||
|
||||
public static class B0
|
||||
{
|
||||
public static byte PackModes(StrengthMode a, StrengthMode b) =>
|
||||
(byte)(((int)a << 2) | (int)b);
|
||||
|
||||
public static byte[] Build(
|
||||
byte seq,
|
||||
StrengthMode modeA, StrengthMode modeB,
|
||||
byte strengthA, byte strengthB,
|
||||
byte[] freqA, byte[] intA,
|
||||
byte[] freqB, byte[] intB)
|
||||
{
|
||||
if (freqA.Length != 4 || intA.Length != 4 || freqB.Length != 4 || intB.Length != 4)
|
||||
throw new ArgumentException("wave arrays must be 4 bytes each");
|
||||
|
||||
var f = new byte[20];
|
||||
f[0] = 0xB0;
|
||||
f[1] = (byte)((seq << 4) | (PackModes(modeA, modeB) & 0x0F));
|
||||
f[2] = strengthA;
|
||||
f[3] = strengthB;
|
||||
Buffer.BlockCopy(freqA, 0, f, 4, 4);
|
||||
Buffer.BlockCopy(intA, 0, f, 8, 4);
|
||||
Buffer.BlockCopy(freqB, 0, f, 12, 4);
|
||||
Buffer.BlockCopy(intB, 0, f, 16, 4);
|
||||
return f;
|
||||
}
|
||||
|
||||
public static readonly byte[] ChannelOff =
|
||||
{ 0, 0, 0, 0 };
|
||||
|
||||
public static readonly byte[] IntensityOff =
|
||||
{ 0, 0, 0, 101 };
|
||||
}
|
||||
|
||||
public static class BF
|
||||
{
|
||||
public static byte[] Build(
|
||||
byte capA, byte capB,
|
||||
byte freqBalA, byte freqBalB,
|
||||
byte intBalA, byte intBalB)
|
||||
{
|
||||
return new byte[]
|
||||
{
|
||||
0xBF, capA, capB, freqBalA, freqBalB, intBalA, intBalB
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public record WaveFrame(byte[] Freq, byte[] Intensity);
|
||||
|
||||
public class Command
|
||||
{
|
||||
[JsonPropertyName("op")] public string Op { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("channel")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Channel { get; set; }
|
||||
|
||||
[JsonPropertyName("mode")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Mode { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? Value { get; set; }
|
||||
|
||||
[JsonPropertyName("freq")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int[]? Freq { get; set; }
|
||||
|
||||
[JsonPropertyName("intensity")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int[]? Intensity { get; set; }
|
||||
|
||||
[JsonPropertyName("frames")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public FrameDto[]? Frames { get; set; }
|
||||
|
||||
[JsonPropertyName("softcapA")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? SoftCapA { get; set; }
|
||||
|
||||
[JsonPropertyName("softcapB")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? SoftCapB { get; set; }
|
||||
|
||||
[JsonPropertyName("freqBalA")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? FreqBalA { get; set; }
|
||||
|
||||
[JsonPropertyName("freqBalB")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? FreqBalB { get; set; }
|
||||
|
||||
[JsonPropertyName("intBalA")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? IntBalA { get; set; }
|
||||
|
||||
[JsonPropertyName("intBalB")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? IntBalB { get; set; }
|
||||
}
|
||||
|
||||
public class FrameDto
|
||||
{
|
||||
[JsonPropertyName("freq")] public int[]? Freq { get; set; }
|
||||
[JsonPropertyName("intensity")] public int[]? Intensity { get; set; }
|
||||
}
|
||||
|
||||
public class OkResponse
|
||||
{
|
||||
[JsonPropertyName("ok")] public bool Ok => true;
|
||||
[JsonPropertyName("msg")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Msg { get; set; }
|
||||
}
|
||||
|
||||
public class ErrResponse
|
||||
{
|
||||
[JsonPropertyName("ok")] public bool Ok => false;
|
||||
[JsonPropertyName("error")] public string Error { get; set; } = "";
|
||||
}
|
||||
|
||||
public class StatusResponse
|
||||
{
|
||||
[JsonPropertyName("ok")] public bool Ok => true;
|
||||
[JsonPropertyName("event")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Event { get; set; }
|
||||
[JsonPropertyName("connected")] public bool Connected { get; set; }
|
||||
[JsonPropertyName("strengthA")] public int StrengthA { get; set; }
|
||||
[JsonPropertyName("strengthB")] public int StrengthB { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user