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:
@@ -0,0 +1,124 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Substation;
|
||||
|
||||
public class State
|
||||
{
|
||||
public readonly object Lock = new();
|
||||
|
||||
public bool DirtyA, DirtyB;
|
||||
public int DesiredA, DesiredB;
|
||||
|
||||
public byte[]? LoopFreqA, LoopIntA;
|
||||
public byte[]? LoopFreqB, LoopIntB;
|
||||
|
||||
public readonly ConcurrentQueue<WaveFrame> StreamA = new();
|
||||
public readonly ConcurrentQueue<WaveFrame> StreamB = new();
|
||||
|
||||
public int ActualA, ActualB;
|
||||
|
||||
public int MaxStrength = 200;
|
||||
public LimitMode LimitMode = LimitMode.Clamp;
|
||||
|
||||
public void SetLimit(int max, LimitMode mode)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
MaxStrength = Math.Clamp(max, 0, 200);
|
||||
LimitMode = mode;
|
||||
DirtyA = true;
|
||||
DirtyB = true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSignaling
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
bool hasPattern = LoopFreqA != null || LoopFreqB != null
|
||||
|| !StreamA.IsEmpty || !StreamB.IsEmpty;
|
||||
bool hasStrength = DesiredA > 0 || DesiredB > 0;
|
||||
return hasPattern && hasStrength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetStrength(char ch, int value)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
var clamped = Math.Clamp(value, 0, 200);
|
||||
if (ch == 'A') { DesiredA = clamped; DirtyA = true; }
|
||||
else { DesiredB = clamped; DirtyB = true; }
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLoop(char ch, byte[] freq, byte[] intensity)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
if (ch == 'A') { LoopFreqA = freq; LoopIntA = intensity; }
|
||||
else { LoopFreqB = freq; LoopIntB = intensity; }
|
||||
}
|
||||
}
|
||||
|
||||
public void EnqueueStream(char ch, IEnumerable<WaveFrame> frames)
|
||||
{
|
||||
var queue = ch == 'A' ? StreamA : StreamB;
|
||||
foreach (var f in frames)
|
||||
queue.Enqueue(f);
|
||||
}
|
||||
|
||||
public void Stop(char ch)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
if (ch == 'A') { LoopFreqA = null; LoopIntA = null; }
|
||||
else { LoopFreqB = null; LoopIntB = null; }
|
||||
}
|
||||
var queue = ch == 'A' ? StreamA : StreamB;
|
||||
while (queue.TryDequeue(out _)) { }
|
||||
}
|
||||
|
||||
public (StrengthMode modeA, byte valA, StrengthMode modeB, byte valB,
|
||||
byte[] freqA, byte[] intA, byte[] freqB, byte[] intB)
|
||||
ConsumeTick()
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
var modeA = DirtyA ? StrengthMode.Abs : StrengthMode.None;
|
||||
var modeB = DirtyB ? StrengthMode.Abs : StrengthMode.None;
|
||||
|
||||
var limit = MaxStrength;
|
||||
var limMode = LimitMode;
|
||||
byte ApplyLimit(int desired) =>
|
||||
limMode == LimitMode.Scale
|
||||
? (byte)(desired * limit / 200)
|
||||
: (byte)Math.Clamp(desired, 0, limit);
|
||||
var valA = ApplyLimit(DesiredA);
|
||||
var valB = ApplyLimit(DesiredB);
|
||||
DirtyA = false;
|
||||
DirtyB = false;
|
||||
|
||||
var (freqA, intA) = PopWave(StreamA, LoopFreqA, LoopIntA);
|
||||
var (freqB, intB) = PopWave(StreamB, LoopFreqB, LoopIntB);
|
||||
|
||||
return (modeA, valA, modeB, valB, freqA, intA, freqB, intB);
|
||||
}
|
||||
}
|
||||
|
||||
static (byte[] freq, byte[] intensity) PopWave(
|
||||
ConcurrentQueue<WaveFrame> stream,
|
||||
byte[]? loopFreq, byte[]? loopInt)
|
||||
{
|
||||
if (stream.TryDequeue(out var frame))
|
||||
return (frame.Freq, frame.Intensity);
|
||||
|
||||
if (loopFreq != null && loopInt != null)
|
||||
return (loopFreq, loopInt);
|
||||
|
||||
return (B0.ChannelOff, B0.IntensityOff);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user