Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33e289d596 | |||
| 325022d27c | |||
| 84eba7d094 | |||
| 612385af7f |
+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));
|
||||
}
|
||||
}
|
||||
+121
-61
@@ -14,7 +14,6 @@ public class MainForm : Form
|
||||
readonly NotifyIcon _tray;
|
||||
readonly Label _lblBle;
|
||||
readonly Label _lblWs;
|
||||
readonly Label _lblStrength;
|
||||
readonly Label _lblDeviceName;
|
||||
readonly TextBox _txtDeviceName;
|
||||
readonly Button _btnConnect;
|
||||
@@ -25,13 +24,20 @@ public class MainForm : Form
|
||||
|
||||
bool _closingFromTray;
|
||||
|
||||
readonly ProgressBar _gaugeA;
|
||||
readonly ProgressBar _gaugeB;
|
||||
readonly Label _lblGaugeA;
|
||||
readonly Label _lblGaugeB;
|
||||
readonly TrackBar _limitBar;
|
||||
readonly Label _lblLimit;
|
||||
readonly CheckBox _chkScale;
|
||||
readonly HeatMap _heatA;
|
||||
readonly HeatMap _heatB;
|
||||
readonly ProgressBar _gaugeRecvA;
|
||||
readonly ProgressBar _gaugeRecvB;
|
||||
readonly Label _lblSendA;
|
||||
readonly Label _lblSendB;
|
||||
readonly Label _lblRecvA;
|
||||
readonly Label _lblRecvB;
|
||||
readonly TrackBar _limitBarA;
|
||||
readonly TrackBar _limitBarB;
|
||||
readonly Label _lblLimitA;
|
||||
readonly Label _lblLimitB;
|
||||
readonly CheckBox _chkScaleA;
|
||||
readonly CheckBox _chkScaleB;
|
||||
|
||||
readonly Icon _iconNeutral;
|
||||
readonly Icon _iconActive;
|
||||
@@ -51,8 +57,8 @@ public class MainForm : Form
|
||||
_server = server;
|
||||
_loopCts = new CancellationTokenSource();
|
||||
|
||||
Text = "Substation";
|
||||
ClientSize = new Size(360, 340);
|
||||
Text = $"Substation {typeof(MainForm).Assembly.GetName().Version}";
|
||||
ClientSize = new Size(420, 410);
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
MaximizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
@@ -62,7 +68,7 @@ public class MainForm : Form
|
||||
{
|
||||
Text = "BLE: searching...",
|
||||
Location = new Point(16, 16),
|
||||
Size = new Size(328, 20),
|
||||
Size = new Size(388, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
|
||||
@@ -70,34 +76,27 @@ public class MainForm : Form
|
||||
{
|
||||
Text = "WS: idle",
|
||||
Location = new Point(16, 40),
|
||||
Size = new Size(328, 20),
|
||||
Size = new Size(388, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
|
||||
_lblStrength = new Label
|
||||
{
|
||||
Text = "Strength: A=0 B=0",
|
||||
Location = new Point(16, 64),
|
||||
Size = new Size(328, 20)
|
||||
};
|
||||
|
||||
_lblDeviceName = new Label
|
||||
{
|
||||
Text = "Device:",
|
||||
Location = new Point(16, 90),
|
||||
Location = new Point(16, 66),
|
||||
Size = new Size(48, 20)
|
||||
};
|
||||
_txtDeviceName = new TextBox
|
||||
{
|
||||
Text = "47L121000",
|
||||
Location = new Point(68, 88),
|
||||
Location = new Point(68, 64),
|
||||
Size = new Size(160, 20)
|
||||
};
|
||||
|
||||
_btnConnect = new Button
|
||||
{
|
||||
Text = "Connect",
|
||||
Location = new Point(236, 88),
|
||||
Location = new Point(296, 64),
|
||||
Size = new Size(108, 24)
|
||||
};
|
||||
_btnConnect.Click += OnConnect;
|
||||
@@ -105,7 +104,7 @@ public class MainForm : Form
|
||||
_btnTest = new Button
|
||||
{
|
||||
Text = "Test",
|
||||
Location = new Point(16, 120),
|
||||
Location = new Point(16, 96),
|
||||
Size = new Size(100, 32)
|
||||
};
|
||||
_btnTest.Click += OnTest;
|
||||
@@ -113,7 +112,7 @@ public class MainForm : Form
|
||||
_btnMusic = new Button
|
||||
{
|
||||
Text = "Music",
|
||||
Location = new Point(130, 120),
|
||||
Location = new Point(130, 96),
|
||||
Size = new Size(100, 32)
|
||||
};
|
||||
_btnMusic.Click += OnMusic;
|
||||
@@ -121,7 +120,7 @@ public class MainForm : Form
|
||||
_btnStop = new Button
|
||||
{
|
||||
Text = "Stop All",
|
||||
Location = new Point(244, 120),
|
||||
Location = new Point(244, 96),
|
||||
Size = new Size(100, 32)
|
||||
};
|
||||
_btnStop.Click += OnStop;
|
||||
@@ -129,70 +128,120 @@ public class MainForm : Form
|
||||
_lblTrack = new Label
|
||||
{
|
||||
Text = "",
|
||||
Location = new Point(16, 158),
|
||||
Size = new Size(328, 16),
|
||||
Location = new Point(16, 134),
|
||||
Size = new Size(388, 16),
|
||||
ForeColor = Color.DimGray
|
||||
};
|
||||
|
||||
_lblGaugeA = new Label
|
||||
_lblSendA = new Label
|
||||
{
|
||||
Text = "A",
|
||||
Location = new Point(16, 180),
|
||||
Size = new Size(16, 20),
|
||||
Text = "Send A",
|
||||
Location = new Point(16, 160),
|
||||
Size = new Size(48, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
_gaugeA = new ProgressBar
|
||||
_heatA = new HeatMap(60)
|
||||
{
|
||||
Location = new Point(40, 180),
|
||||
Size = new Size(304, 20),
|
||||
Location = new Point(72, 158),
|
||||
Size = new Size(332, 22)
|
||||
};
|
||||
_lblSendB = new Label
|
||||
{
|
||||
Text = "Send B",
|
||||
Location = new Point(16, 188),
|
||||
Size = new Size(48, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
_heatB = new HeatMap(60)
|
||||
{
|
||||
Location = new Point(72, 186),
|
||||
Size = new Size(332, 22)
|
||||
};
|
||||
_lblRecvA = new Label
|
||||
{
|
||||
Text = "Recv A",
|
||||
Location = new Point(16, 216),
|
||||
Size = new Size(48, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
_gaugeRecvA = new ProgressBar
|
||||
{
|
||||
Location = new Point(72, 216),
|
||||
Size = new Size(332, 20),
|
||||
Minimum = 0,
|
||||
Maximum = 200,
|
||||
Style = ProgressBarStyle.Continuous
|
||||
};
|
||||
_lblGaugeB = new Label
|
||||
_lblRecvB = new Label
|
||||
{
|
||||
Text = "B",
|
||||
Location = new Point(16, 208),
|
||||
Size = new Size(16, 20),
|
||||
Text = "Recv B",
|
||||
Location = new Point(16, 242),
|
||||
Size = new Size(48, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
_gaugeB = new ProgressBar
|
||||
_gaugeRecvB = new ProgressBar
|
||||
{
|
||||
Location = new Point(40, 208),
|
||||
Size = new Size(304, 20),
|
||||
Location = new Point(72, 242),
|
||||
Size = new Size(332, 20),
|
||||
Minimum = 0,
|
||||
Maximum = 200,
|
||||
Style = ProgressBarStyle.Continuous
|
||||
};
|
||||
|
||||
_lblLimit = new Label
|
||||
_lblLimitA = new Label
|
||||
{
|
||||
Text = "Limit: 30",
|
||||
Location = new Point(16, 248),
|
||||
Size = new Size(64, 20),
|
||||
Text = "Lim A: 30",
|
||||
Location = new Point(16, 274),
|
||||
Size = new Size(56, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
_limitBar = new TrackBar
|
||||
_limitBarA = new TrackBar
|
||||
{
|
||||
Location = new Point(80, 244),
|
||||
Size = new Size(180, 45),
|
||||
Location = new Point(72, 270),
|
||||
Size = new Size(250, 45),
|
||||
Minimum = 0,
|
||||
Maximum = 200,
|
||||
TickFrequency = 50,
|
||||
Value = 30
|
||||
};
|
||||
_limitBar.ValueChanged += OnLimitChanged;
|
||||
_chkScale = new CheckBox
|
||||
_limitBarA.ValueChanged += OnLimitChanged;
|
||||
_chkScaleA = new CheckBox
|
||||
{
|
||||
Text = "Scale",
|
||||
Location = new Point(268, 248),
|
||||
Location = new Point(328, 274),
|
||||
Size = new Size(76, 24),
|
||||
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);
|
||||
|
||||
Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblStrength, _lblDeviceName, _txtDeviceName, _btnConnect, _btnTest, _btnMusic, _btnStop, _lblTrack, _lblGaugeA, _gaugeA, _lblGaugeB, _gaugeB, _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
|
||||
_iconNeutral = CreateVoltageIcon(Color.Gray);
|
||||
@@ -242,9 +291,9 @@ public class MainForm : Form
|
||||
{
|
||||
try
|
||||
{
|
||||
var (modeA, valA, modeB, valB, freqA, intA, freqB, intB) = _state.ConsumeTick();
|
||||
if (_device.IsConnected)
|
||||
{
|
||||
var (modeA, valA, modeB, valB, freqA, intA, freqB, intB) = _state.ConsumeTick();
|
||||
var frame = B0.Build(0, modeA, modeB, valA, valB, freqA, intA, freqB, intB);
|
||||
await _device.SendB0(frame);
|
||||
}
|
||||
@@ -331,9 +380,16 @@ public class MainForm : Form
|
||||
_lblBle.Text = _device.IsConnected
|
||||
? $"BLE: connected ({_device.DeviceName})"
|
||||
: "BLE: disconnected";
|
||||
_lblStrength.Text = $"Strength: A={_device.StrengthA} B={_device.StrengthB}";
|
||||
_gaugeA.Value = Math.Clamp(_device.StrengthA, 0, 200);
|
||||
_gaugeB.Value = Math.Clamp(_device.StrengthB, 0, 200);
|
||||
_heatA.AddTick(
|
||||
_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);
|
||||
_gaugeRecvB.Value = Math.Clamp(_device.StrengthB, 0, 200);
|
||||
_btnTest.Enabled = !_server.HasClient && !IsTestRunning;
|
||||
_btnMusic.Enabled = !_server.HasClient && !_isMusicRunning;
|
||||
_btnStop.Enabled = true;
|
||||
@@ -416,10 +472,14 @@ public class MainForm : Form
|
||||
|
||||
void OnLimitChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var max = _limitBar.Value;
|
||||
var mode = _chkScale.Checked ? LimitMode.Scale : LimitMode.Clamp;
|
||||
_state.SetLimit(max, mode);
|
||||
_lblLimit.Text = $"Limit: {max}";
|
||||
var maxA = _limitBarA.Value;
|
||||
var maxB = _limitBarB.Value;
|
||||
var modeA = _chkScaleA.Checked ? LimitMode.Scale : LimitMode.Clamp;
|
||||
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 = "";
|
||||
|
||||
@@ -16,17 +16,32 @@ public class State
|
||||
public readonly ConcurrentQueue<WaveFrame> StreamB = new();
|
||||
|
||||
public int ActualA, ActualB;
|
||||
public int LastSentA, LastSentB;
|
||||
public int LastIntensityA, LastIntensityB;
|
||||
public byte[]? LastFreqA, LastIntA, LastFreqB, LastIntB;
|
||||
public bool LastActiveA, LastActiveB;
|
||||
|
||||
public int MaxStrength = 200;
|
||||
public LimitMode LimitMode = LimitMode.Clamp;
|
||||
public int MaxA = 200;
|
||||
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)
|
||||
{
|
||||
MaxStrength = Math.Clamp(max, 0, 200);
|
||||
LimitMode = mode;
|
||||
MaxA = Math.Clamp(max, 0, 200);
|
||||
LimitModeA = mode;
|
||||
DirtyA = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLimitB(int max, LimitMode mode)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
MaxB = Math.Clamp(max, 0, 200);
|
||||
LimitModeB = mode;
|
||||
DirtyB = true;
|
||||
}
|
||||
}
|
||||
@@ -91,20 +106,29 @@ public class State
|
||||
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);
|
||||
byte ApplyLimit(int desired, int max, LimitMode mode) =>
|
||||
mode == LimitMode.Scale
|
||||
? (byte)(desired * max / 200)
|
||||
: (byte)Math.Clamp(desired, 0, max);
|
||||
var valA = ApplyLimit(DesiredA, MaxA, LimitModeA);
|
||||
var valB = ApplyLimit(DesiredB, MaxB, LimitModeB);
|
||||
LastSentA = valA;
|
||||
LastSentB = valB;
|
||||
DirtyA = false;
|
||||
DirtyB = false;
|
||||
|
||||
bool hasA = !StreamA.IsEmpty || (LoopFreqA != null && LoopIntA != null);
|
||||
bool hasB = !StreamB.IsEmpty || (LoopFreqB != null && LoopIntB != null);
|
||||
var (freqA, intA) = PopWave(StreamA, LoopFreqA, LoopIntA);
|
||||
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;
|
||||
LastIntensityB = hasB ? (intB[0] + intB[1] + intB[2] + intB[3]) / 4 : 0;
|
||||
|
||||
return (modeA, valA, modeB, valB, freqA, intA, freqB, intB);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AssemblyName>Substation</AssemblyName>
|
||||
<RootNamespace>Substation</RootNamespace>
|
||||
<Version>0.1.6</Version>
|
||||
<AssemblyVersion>0.1.6</AssemblyVersion>
|
||||
<FileVersion>0.1.6</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user