Add per-channel independent limit controls
- Separate Lim A / Lim B trackbars (0-200, default 30 each) - Separate Scale A / Scale B checkboxes (clamp vs scale per channel) - State.SetLimitA/SetLimitB with independent LimitModeA/LimitModeB - ConsumeTick applies per-channel limit and mode
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
|||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
|
||||||
|
namespace Substation;
|
||||||
|
|
||||||
|
public class HeatMap : Panel
|
||||||
|
{
|
||||||
|
record Tick(byte[] Freq, byte[] Intensity);
|
||||||
|
|
||||||
|
readonly List<Tick> _history = new();
|
||||||
|
readonly int _maxTicks;
|
||||||
|
|
||||||
|
public HeatMap(int maxTicks = 60)
|
||||||
|
{
|
||||||
|
_maxTicks = maxTicks;
|
||||||
|
DoubleBuffered = true;
|
||||||
|
BackColor = Color.FromArgb(20, 20, 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTick(byte[] freq, byte[] intensity, bool active)
|
||||||
|
{
|
||||||
|
if (_history.Count >= _maxTicks)
|
||||||
|
_history.RemoveAt(0);
|
||||||
|
|
||||||
|
if (active)
|
||||||
|
_history.Add(new Tick(freq, intensity));
|
||||||
|
else
|
||||||
|
_history.Add(new Tick(Array.Empty<byte>(), Array.Empty<byte>()));
|
||||||
|
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPaint(e);
|
||||||
|
var g = e.Graphics;
|
||||||
|
g.SmoothingMode = SmoothingMode.None;
|
||||||
|
|
||||||
|
int w = Width;
|
||||||
|
int h = Height;
|
||||||
|
int colW = Math.Max(1, w / _maxTicks);
|
||||||
|
|
||||||
|
// Draw axis labels
|
||||||
|
using var font = new Font(FontFamily.GenericMonospace, 7);
|
||||||
|
using var textBrush = new SolidBrush(Color.FromArgb(100, 100, 100));
|
||||||
|
g.DrawString("100Hz", font, textBrush, 2, 2);
|
||||||
|
g.DrawString("1Hz", font, textBrush, 2, h - 14);
|
||||||
|
|
||||||
|
int plotX = 36;
|
||||||
|
int plotW = w - plotX - 4;
|
||||||
|
int plotH = h - 4;
|
||||||
|
colW = Math.Max(1, plotW / _maxTicks);
|
||||||
|
|
||||||
|
for (int i = 0; i < _history.Count; i++)
|
||||||
|
{
|
||||||
|
var tick = _history[i];
|
||||||
|
int xPos = plotX + i * colW;
|
||||||
|
|
||||||
|
if (tick.Freq.Length == 0)
|
||||||
|
{
|
||||||
|
// Inactive — dark cell
|
||||||
|
using var dark = new SolidBrush(Color.FromArgb(25, 25, 28));
|
||||||
|
g.FillRectangle(dark, xPos, 2, colW, plotH);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4 sub-ticks side by side within the column
|
||||||
|
int subW = Math.Max(1, colW / 4);
|
||||||
|
for (int s = 0; s < 4; s++)
|
||||||
|
{
|
||||||
|
int freq = tick.Freq[s]; // 10-240
|
||||||
|
int inten = tick.Intensity[s]; // 0-100
|
||||||
|
if (inten > 100) inten = 0; // 101 sentinel = off
|
||||||
|
|
||||||
|
// Map freq byte (10-240) to Y position (top=high freq, bottom=low freq)
|
||||||
|
float normFreq = (freq - 10f) / (240f - 10f);
|
||||||
|
int y = (int)(2 + plotH * (1f - normFreq));
|
||||||
|
if (y < 2) y = 2;
|
||||||
|
if (y > plotH) y = plotH;
|
||||||
|
|
||||||
|
var color = HeatColor(inten);
|
||||||
|
using var brush = new SolidBrush(color);
|
||||||
|
g.FillRectangle(brush, xPos + s * subW, y, subW, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color HeatColor(int intensity)
|
||||||
|
{
|
||||||
|
// 0=black, 30=blue, 60=green, 80=gold, 100=red
|
||||||
|
if (intensity <= 0) return Color.FromArgb(20, 20, 22);
|
||||||
|
if (intensity < 30)
|
||||||
|
{
|
||||||
|
float t = intensity / 30f;
|
||||||
|
return Color.FromArgb(0, (int)(50 * t), (int)(80 + 80 * t));
|
||||||
|
}
|
||||||
|
if (intensity < 60)
|
||||||
|
{
|
||||||
|
float t = (intensity - 30) / 30f;
|
||||||
|
return Color.FromArgb((int)(60 * t), (int)(130 + 80 * t), (int)(160 - 100 * t));
|
||||||
|
}
|
||||||
|
if (intensity < 80)
|
||||||
|
{
|
||||||
|
float t = (intensity - 60) / 20f;
|
||||||
|
return Color.FromArgb((int)(60 + 180 * t), 210, (int)(60 - 30 * t));
|
||||||
|
}
|
||||||
|
float t2 = (intensity - 80) / 20f;
|
||||||
|
return Color.FromArgb(255, (int)(240 - 140 * t2), (int)(30 - 10 * t2));
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
-41
@@ -24,17 +24,20 @@ public class MainForm : Form
|
|||||||
|
|
||||||
bool _closingFromTray;
|
bool _closingFromTray;
|
||||||
|
|
||||||
readonly ProgressBar _gaugeSendA;
|
readonly HeatMap _heatA;
|
||||||
readonly ProgressBar _gaugeSendB;
|
readonly HeatMap _heatB;
|
||||||
readonly ProgressBar _gaugeRecvA;
|
readonly ProgressBar _gaugeRecvA;
|
||||||
readonly ProgressBar _gaugeRecvB;
|
readonly ProgressBar _gaugeRecvB;
|
||||||
readonly Label _lblSendA;
|
readonly Label _lblSendA;
|
||||||
readonly Label _lblSendB;
|
readonly Label _lblSendB;
|
||||||
readonly Label _lblRecvA;
|
readonly Label _lblRecvA;
|
||||||
readonly Label _lblRecvB;
|
readonly Label _lblRecvB;
|
||||||
readonly TrackBar _limitBar;
|
readonly TrackBar _limitBarA;
|
||||||
readonly Label _lblLimit;
|
readonly TrackBar _limitBarB;
|
||||||
readonly CheckBox _chkScale;
|
readonly Label _lblLimitA;
|
||||||
|
readonly Label _lblLimitB;
|
||||||
|
readonly CheckBox _chkScaleA;
|
||||||
|
readonly CheckBox _chkScaleB;
|
||||||
|
|
||||||
readonly Icon _iconNeutral;
|
readonly Icon _iconNeutral;
|
||||||
readonly Icon _iconActive;
|
readonly Icon _iconActive;
|
||||||
@@ -55,7 +58,7 @@ public class MainForm : Form
|
|||||||
_loopCts = new CancellationTokenSource();
|
_loopCts = new CancellationTokenSource();
|
||||||
|
|
||||||
Text = $"Substation {typeof(MainForm).Assembly.GetName().Version}";
|
Text = $"Substation {typeof(MainForm).Assembly.GetName().Version}";
|
||||||
ClientSize = new Size(420, 360);
|
ClientSize = new Size(420, 410);
|
||||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
@@ -137,39 +140,33 @@ public class MainForm : Form
|
|||||||
Size = new Size(48, 20),
|
Size = new Size(48, 20),
|
||||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||||
};
|
};
|
||||||
_gaugeSendA = new ProgressBar
|
_heatA = new HeatMap(60)
|
||||||
{
|
{
|
||||||
Location = new Point(72, 160),
|
Location = new Point(72, 158),
|
||||||
Size = new Size(332, 20),
|
Size = new Size(332, 22)
|
||||||
Minimum = 0,
|
|
||||||
Maximum = 100,
|
|
||||||
Style = ProgressBarStyle.Continuous
|
|
||||||
};
|
};
|
||||||
_lblSendB = new Label
|
_lblSendB = new Label
|
||||||
{
|
{
|
||||||
Text = "Send B",
|
Text = "Send B",
|
||||||
Location = new Point(16, 184),
|
Location = new Point(16, 188),
|
||||||
Size = new Size(48, 20),
|
Size = new Size(48, 20),
|
||||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||||
};
|
};
|
||||||
_gaugeSendB = new ProgressBar
|
_heatB = new HeatMap(60)
|
||||||
{
|
{
|
||||||
Location = new Point(72, 184),
|
Location = new Point(72, 186),
|
||||||
Size = new Size(332, 20),
|
Size = new Size(332, 22)
|
||||||
Minimum = 0,
|
|
||||||
Maximum = 100,
|
|
||||||
Style = ProgressBarStyle.Continuous
|
|
||||||
};
|
};
|
||||||
_lblRecvA = new Label
|
_lblRecvA = new Label
|
||||||
{
|
{
|
||||||
Text = "Recv A",
|
Text = "Recv A",
|
||||||
Location = new Point(16, 212),
|
Location = new Point(16, 216),
|
||||||
Size = new Size(48, 20),
|
Size = new Size(48, 20),
|
||||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||||
};
|
};
|
||||||
_gaugeRecvA = new ProgressBar
|
_gaugeRecvA = new ProgressBar
|
||||||
{
|
{
|
||||||
Location = new Point(72, 212),
|
Location = new Point(72, 216),
|
||||||
Size = new Size(332, 20),
|
Size = new Size(332, 20),
|
||||||
Minimum = 0,
|
Minimum = 0,
|
||||||
Maximum = 200,
|
Maximum = 200,
|
||||||
@@ -178,47 +175,73 @@ public class MainForm : Form
|
|||||||
_lblRecvB = new Label
|
_lblRecvB = new Label
|
||||||
{
|
{
|
||||||
Text = "Recv B",
|
Text = "Recv B",
|
||||||
Location = new Point(16, 236),
|
Location = new Point(16, 242),
|
||||||
Size = new Size(48, 20),
|
Size = new Size(48, 20),
|
||||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||||
};
|
};
|
||||||
_gaugeRecvB = new ProgressBar
|
_gaugeRecvB = new ProgressBar
|
||||||
{
|
{
|
||||||
Location = new Point(72, 236),
|
Location = new Point(72, 242),
|
||||||
Size = new Size(332, 20),
|
Size = new Size(332, 20),
|
||||||
Minimum = 0,
|
Minimum = 0,
|
||||||
Maximum = 200,
|
Maximum = 200,
|
||||||
Style = ProgressBarStyle.Continuous
|
Style = ProgressBarStyle.Continuous
|
||||||
};
|
};
|
||||||
|
|
||||||
_lblLimit = new Label
|
_lblLimitA = new Label
|
||||||
{
|
{
|
||||||
Text = "Limit: 30",
|
Text = "Lim A: 30",
|
||||||
Location = new Point(16, 272),
|
Location = new Point(16, 274),
|
||||||
Size = new Size(64, 20),
|
Size = new Size(56, 20),
|
||||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||||
};
|
};
|
||||||
_limitBar = new TrackBar
|
_limitBarA = new TrackBar
|
||||||
{
|
{
|
||||||
Location = new Point(80, 268),
|
Location = new Point(72, 270),
|
||||||
Size = new Size(240, 45),
|
Size = new Size(250, 45),
|
||||||
Minimum = 0,
|
Minimum = 0,
|
||||||
Maximum = 200,
|
Maximum = 200,
|
||||||
TickFrequency = 50,
|
TickFrequency = 50,
|
||||||
Value = 30
|
Value = 30
|
||||||
};
|
};
|
||||||
_limitBar.ValueChanged += OnLimitChanged;
|
_limitBarA.ValueChanged += OnLimitChanged;
|
||||||
_chkScale = new CheckBox
|
_chkScaleA = new CheckBox
|
||||||
{
|
{
|
||||||
Text = "Scale",
|
Text = "Scale",
|
||||||
Location = new Point(328, 272),
|
Location = new Point(328, 274),
|
||||||
Size = new Size(76, 24),
|
Size = new Size(76, 24),
|
||||||
Checked = false
|
Checked = false
|
||||||
};
|
};
|
||||||
_chkScale.CheckedChanged += OnLimitChanged;
|
_chkScaleA.CheckedChanged += OnLimitChanged;
|
||||||
|
|
||||||
|
_lblLimitB = new Label
|
||||||
|
{
|
||||||
|
Text = "Lim B: 30",
|
||||||
|
Location = new Point(16, 312),
|
||||||
|
Size = new Size(56, 20),
|
||||||
|
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||||
|
};
|
||||||
|
_limitBarB = new TrackBar
|
||||||
|
{
|
||||||
|
Location = new Point(72, 308),
|
||||||
|
Size = new Size(250, 45),
|
||||||
|
Minimum = 0,
|
||||||
|
Maximum = 200,
|
||||||
|
TickFrequency = 50,
|
||||||
|
Value = 30
|
||||||
|
};
|
||||||
|
_limitBarB.ValueChanged += OnLimitChanged;
|
||||||
|
_chkScaleB = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Scale",
|
||||||
|
Location = new Point(328, 312),
|
||||||
|
Size = new Size(76, 24),
|
||||||
|
Checked = false
|
||||||
|
};
|
||||||
|
_chkScaleB.CheckedChanged += OnLimitChanged;
|
||||||
OnLimitChanged(null, EventArgs.Empty);
|
OnLimitChanged(null, EventArgs.Empty);
|
||||||
|
|
||||||
Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _btnTest, _btnMusic, _btnStop, _lblTrack, _lblSendA, _gaugeSendA, _lblSendB, _gaugeSendB, _lblRecvA, _gaugeRecvA, _lblRecvB, _gaugeRecvB, _lblLimit, _limitBar, _chkScale });
|
Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _btnTest, _btnMusic, _btnStop, _lblTrack, _lblSendA, _heatA, _lblSendB, _heatB, _lblRecvA, _gaugeRecvA, _lblRecvB, _gaugeRecvB, _lblLimitA, _limitBarA, _chkScaleA, _lblLimitB, _limitBarB, _chkScaleB });
|
||||||
|
|
||||||
// Tray icon — three cached variants: neutral=gray, active=gold, hot=red-orange
|
// Tray icon — three cached variants: neutral=gray, active=gold, hot=red-orange
|
||||||
_iconNeutral = CreateVoltageIcon(Color.Gray);
|
_iconNeutral = CreateVoltageIcon(Color.Gray);
|
||||||
@@ -357,8 +380,14 @@ public class MainForm : Form
|
|||||||
_lblBle.Text = _device.IsConnected
|
_lblBle.Text = _device.IsConnected
|
||||||
? $"BLE: connected ({_device.DeviceName})"
|
? $"BLE: connected ({_device.DeviceName})"
|
||||||
: "BLE: disconnected";
|
: "BLE: disconnected";
|
||||||
_gaugeSendA.Value = Math.Clamp(_state.LastIntensityA, 0, 100);
|
_heatA.AddTick(
|
||||||
_gaugeSendB.Value = Math.Clamp(_state.LastIntensityB, 0, 100);
|
_state.LastFreqA ?? Array.Empty<byte>(),
|
||||||
|
_state.LastIntA ?? Array.Empty<byte>(),
|
||||||
|
_state.LastActiveA);
|
||||||
|
_heatB.AddTick(
|
||||||
|
_state.LastFreqB ?? Array.Empty<byte>(),
|
||||||
|
_state.LastIntB ?? Array.Empty<byte>(),
|
||||||
|
_state.LastActiveB);
|
||||||
_gaugeRecvA.Value = Math.Clamp(_device.StrengthA, 0, 200);
|
_gaugeRecvA.Value = Math.Clamp(_device.StrengthA, 0, 200);
|
||||||
_gaugeRecvB.Value = Math.Clamp(_device.StrengthB, 0, 200);
|
_gaugeRecvB.Value = Math.Clamp(_device.StrengthB, 0, 200);
|
||||||
_btnTest.Enabled = !_server.HasClient && !IsTestRunning;
|
_btnTest.Enabled = !_server.HasClient && !IsTestRunning;
|
||||||
@@ -443,10 +472,14 @@ public class MainForm : Form
|
|||||||
|
|
||||||
void OnLimitChanged(object? sender, EventArgs e)
|
void OnLimitChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var max = _limitBar.Value;
|
var maxA = _limitBarA.Value;
|
||||||
var mode = _chkScale.Checked ? LimitMode.Scale : LimitMode.Clamp;
|
var maxB = _limitBarB.Value;
|
||||||
_state.SetLimit(max, mode);
|
var modeA = _chkScaleA.Checked ? LimitMode.Scale : LimitMode.Clamp;
|
||||||
_lblLimit.Text = $"Limit: {max}";
|
var modeB = _chkScaleB.Checked ? LimitMode.Scale : LimitMode.Clamp;
|
||||||
|
_state.SetLimitA(maxA, modeA);
|
||||||
|
_state.SetLimitB(maxB, modeB);
|
||||||
|
_lblLimitA.Text = $"Lim A: {maxA}";
|
||||||
|
_lblLimitB.Text = $"Lim B: {maxB}";
|
||||||
}
|
}
|
||||||
|
|
||||||
string _musicFilePath = "";
|
string _musicFilePath = "";
|
||||||
|
|||||||
@@ -18,17 +18,30 @@ public class State
|
|||||||
public int ActualA, ActualB;
|
public int ActualA, ActualB;
|
||||||
public int LastSentA, LastSentB;
|
public int LastSentA, LastSentB;
|
||||||
public int LastIntensityA, LastIntensityB;
|
public int LastIntensityA, LastIntensityB;
|
||||||
|
public byte[]? LastFreqA, LastIntA, LastFreqB, LastIntB;
|
||||||
|
public bool LastActiveA, LastActiveB;
|
||||||
|
|
||||||
public int MaxStrength = 200;
|
public int MaxA = 200;
|
||||||
public LimitMode LimitMode = LimitMode.Clamp;
|
public int MaxB = 200;
|
||||||
|
public LimitMode LimitModeA = LimitMode.Clamp;
|
||||||
|
public LimitMode LimitModeB = LimitMode.Clamp;
|
||||||
|
|
||||||
public void SetLimit(int max, LimitMode mode)
|
public void SetLimitA(int max, LimitMode mode)
|
||||||
{
|
{
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
MaxStrength = Math.Clamp(max, 0, 200);
|
MaxA = Math.Clamp(max, 0, 200);
|
||||||
LimitMode = mode;
|
LimitModeA = mode;
|
||||||
DirtyA = true;
|
DirtyA = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLimitB(int max, LimitMode mode)
|
||||||
|
{
|
||||||
|
lock (Lock)
|
||||||
|
{
|
||||||
|
MaxB = Math.Clamp(max, 0, 200);
|
||||||
|
LimitModeB = mode;
|
||||||
DirtyB = true;
|
DirtyB = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,14 +106,12 @@ public class State
|
|||||||
var modeA = DirtyA ? StrengthMode.Abs : StrengthMode.None;
|
var modeA = DirtyA ? StrengthMode.Abs : StrengthMode.None;
|
||||||
var modeB = DirtyB ? StrengthMode.Abs : StrengthMode.None;
|
var modeB = DirtyB ? StrengthMode.Abs : StrengthMode.None;
|
||||||
|
|
||||||
var limit = MaxStrength;
|
byte ApplyLimit(int desired, int max, LimitMode mode) =>
|
||||||
var limMode = LimitMode;
|
mode == LimitMode.Scale
|
||||||
byte ApplyLimit(int desired) =>
|
? (byte)(desired * max / 200)
|
||||||
limMode == LimitMode.Scale
|
: (byte)Math.Clamp(desired, 0, max);
|
||||||
? (byte)(desired * limit / 200)
|
var valA = ApplyLimit(DesiredA, MaxA, LimitModeA);
|
||||||
: (byte)Math.Clamp(desired, 0, limit);
|
var valB = ApplyLimit(DesiredB, MaxB, LimitModeB);
|
||||||
var valA = ApplyLimit(DesiredA);
|
|
||||||
var valB = ApplyLimit(DesiredB);
|
|
||||||
LastSentA = valA;
|
LastSentA = valA;
|
||||||
LastSentB = valB;
|
LastSentB = valB;
|
||||||
DirtyA = false;
|
DirtyA = false;
|
||||||
@@ -111,6 +122,10 @@ public class State
|
|||||||
var (freqA, intA) = PopWave(StreamA, LoopFreqA, LoopIntA);
|
var (freqA, intA) = PopWave(StreamA, LoopFreqA, LoopIntA);
|
||||||
var (freqB, intB) = PopWave(StreamB, LoopFreqB, LoopIntB);
|
var (freqB, intB) = PopWave(StreamB, LoopFreqB, LoopIntB);
|
||||||
|
|
||||||
|
LastActiveA = hasA;
|
||||||
|
LastActiveB = hasB;
|
||||||
|
LastFreqA = freqA; LastIntA = intA;
|
||||||
|
LastFreqB = freqB; LastIntB = intB;
|
||||||
LastIntensityA = hasA ? (intA[0] + intA[1] + intA[2] + intA[3]) / 4 : 0;
|
LastIntensityA = hasA ? (intA[0] + intA[1] + intA[2] + intA[3]) / 4 : 0;
|
||||||
LastIntensityB = hasB ? (intB[0] + intB[1] + intB[2] + intB[3]) / 4 : 0;
|
LastIntensityB = hasB ? (intB[0] + intB[1] + intB[2] + intB[3]) / 4 : 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user