Add xToys pattern import via URL paste
- XToysPattern.cs: fetch pattern from xtoys.app API (one call), parse script-v3 JSON, resolve slider defaults, evaluate sine/straight steps to WaveFrames at 100ms resolution - Frequency mapping: 0%=1000ms deep, 100%=10ms buzzy - MainForm: Pattern button + input dialog, continuously enqueues pattern frames as a loop, Stop All cancels - Fix: handle JSON values that are numbers vs strings (end/start fields) - NOTES.md: xToys slider parameter intel + frequency mapping docs
This commit is contained in:
+124
-11
@@ -50,11 +50,13 @@ public class MainForm : Form
|
||||
|
||||
readonly Button _btnMusic;
|
||||
readonly Button _btnLive;
|
||||
readonly Button _btnPattern;
|
||||
readonly Label _lblTrack;
|
||||
readonly Label _lblAudioDev;
|
||||
readonly ComboBox _cboAudioDev;
|
||||
bool _isMusicRunning;
|
||||
bool _isLiveRunning;
|
||||
bool _isPatternRunning;
|
||||
WaveOutEvent? _audioOut;
|
||||
Stopwatch? _musicStopwatch;
|
||||
LiveCapture? _liveCapture;
|
||||
@@ -123,31 +125,39 @@ public class MainForm : Form
|
||||
{
|
||||
Text = "Test",
|
||||
Location = new Point(16, 96),
|
||||
Size = new Size(80, 32)
|
||||
Size = new Size(68, 32)
|
||||
};
|
||||
_btnTest.Click += OnTest;
|
||||
|
||||
_btnMusic = new Button
|
||||
{
|
||||
Text = "Music",
|
||||
Location = new Point(104, 96),
|
||||
Size = new Size(80, 32)
|
||||
Location = new Point(92, 96),
|
||||
Size = new Size(68, 32)
|
||||
};
|
||||
_btnMusic.Click += OnMusic;
|
||||
|
||||
_btnLive = new Button
|
||||
{
|
||||
Text = "Live",
|
||||
Location = new Point(192, 96),
|
||||
Size = new Size(80, 32)
|
||||
Location = new Point(168, 96),
|
||||
Size = new Size(68, 32)
|
||||
};
|
||||
_btnLive.Click += OnLive;
|
||||
|
||||
_btnPattern = new Button
|
||||
{
|
||||
Text = "Pattern",
|
||||
Location = new Point(244, 96),
|
||||
Size = new Size(68, 32)
|
||||
};
|
||||
_btnPattern.Click += OnPattern;
|
||||
|
||||
_btnStop = new Button
|
||||
{
|
||||
Text = "Stop All",
|
||||
Location = new Point(280, 96),
|
||||
Size = new Size(80, 32)
|
||||
Location = new Point(320, 96),
|
||||
Size = new Size(76, 32)
|
||||
};
|
||||
_btnStop.Click += OnStop;
|
||||
|
||||
@@ -297,7 +307,7 @@ public class MainForm : Form
|
||||
_chkScaleB.CheckedChanged += OnLimitChanged;
|
||||
OnLimitChanged(null, EventArgs.Empty);
|
||||
|
||||
Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _chkSwap, _btnTest, _btnMusic, _btnLive, _btnStop, _lblAudioDev, _cboAudioDev, _lblTrack, _lblSendA, _heatA, _lblSendB, _heatB, _lblRecvA, _gaugeRecvA, _lblRecvB, _gaugeRecvB, _lblLimitA, _limitBarA, _lblLimitValA, _chkScaleA, _lblLimitB, _limitBarB, _lblLimitValB, _chkScaleB });
|
||||
Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _chkSwap, _btnTest, _btnMusic, _btnLive, _btnPattern, _btnStop, _lblAudioDev, _cboAudioDev, _lblTrack, _lblSendA, _heatA, _lblSendB, _heatB, _lblRecvA, _gaugeRecvA, _lblRecvB, _gaugeRecvB, _lblLimitA, _limitBarA, _lblLimitValA, _chkScaleA, _lblLimitB, _limitBarB, _lblLimitValB, _chkScaleB });
|
||||
|
||||
// Tray icon — three cached variants: neutral=gray, active=gold, hot=red-orange
|
||||
_iconNeutral = CreateVoltageIcon(Color.Gray);
|
||||
@@ -465,9 +475,11 @@ public class MainForm : Form
|
||||
|
||||
void RefreshButtonStates()
|
||||
{
|
||||
_btnTest.Enabled = !_server.HasClient && !IsTestRunning && !_isMusicRunning && !_isLiveRunning;
|
||||
_btnMusic.Enabled = !_server.HasClient && !_isMusicRunning && !IsTestRunning && !_isLiveRunning;
|
||||
_btnLive.Enabled = !_server.HasClient && !_isLiveRunning && !IsTestRunning && !_isMusicRunning;
|
||||
bool busy = IsTestRunning || _isMusicRunning || _isLiveRunning || _isPatternRunning;
|
||||
_btnTest.Enabled = !_server.HasClient && !busy;
|
||||
_btnMusic.Enabled = !_server.HasClient && !busy;
|
||||
_btnLive.Enabled = !_server.HasClient && !busy;
|
||||
_btnPattern.Enabled = !_server.HasClient && !busy;
|
||||
_btnStop.Enabled = true;
|
||||
}
|
||||
|
||||
@@ -484,6 +496,10 @@ public class MainForm : Form
|
||||
{
|
||||
_lblTrack.Text = "Live capture";
|
||||
}
|
||||
else if (_isPatternRunning)
|
||||
{
|
||||
_lblTrack.Text = $"Pattern: {_patternName}";
|
||||
}
|
||||
|
||||
UpdateTrayIcon();
|
||||
}
|
||||
@@ -550,6 +566,7 @@ public class MainForm : Form
|
||||
{
|
||||
StopMusic();
|
||||
StopLive();
|
||||
StopPattern();
|
||||
_state.SetStrength('A', 0);
|
||||
_state.SetStrength('B', 0);
|
||||
_state.Stop('A');
|
||||
@@ -732,6 +749,102 @@ public class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
string _patternName = "";
|
||||
CancellationTokenSource? _patternCts;
|
||||
|
||||
void OnPattern(object? sender, EventArgs e)
|
||||
{
|
||||
if (_isPatternRunning || _server.HasClient) return;
|
||||
|
||||
var url = ShowInputDialog("Paste xToys pattern URL:", "Pattern Import");
|
||||
if (string.IsNullOrWhiteSpace(url)) return;
|
||||
|
||||
_isPatternRunning = true;
|
||||
_btnPattern.Enabled = false;
|
||||
_lblTrack.Text = "Loading pattern...";
|
||||
|
||||
_patternCts = new CancellationTokenSource();
|
||||
Task.Run(() => RunPatternAsync(url, _patternCts.Token));
|
||||
}
|
||||
|
||||
async Task RunPatternAsync(string url, CancellationToken ct)
|
||||
{
|
||||
XToysPattern? pattern = null;
|
||||
try
|
||||
{
|
||||
pattern = await XToysPatternImporter.ImportAsync(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
BeginInvoke(() =>
|
||||
{
|
||||
_lblTrack.Text = $"Pattern failed: {ex.Message?.Split('\n')[0]}";
|
||||
_isPatternRunning = false;
|
||||
RefreshButtonStates();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_patternName = pattern.Name;
|
||||
|
||||
_state.SetStrength('A', 60);
|
||||
_state.SetStrength('B', 60);
|
||||
_state.Stop('A');
|
||||
_state.Stop('B');
|
||||
|
||||
BeginInvoke(() => _lblTrack.Text = $"Pattern: {_patternName}");
|
||||
|
||||
// Continuously enqueue pattern frames at 100ms per frame
|
||||
int idx = 0;
|
||||
while (!ct.IsCancellationRequested && _isPatternRunning)
|
||||
{
|
||||
var frameA = pattern.ChannelA[idx];
|
||||
var frameB = pattern.ChannelB[idx];
|
||||
_state.EnqueueStream('A', new[] { frameA });
|
||||
_state.EnqueueStream('B', new[] { frameB });
|
||||
idx = (idx + 1) % pattern.ChannelA.Count;
|
||||
|
||||
try { await Task.Delay(100, ct); } catch (OperationCanceledException) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
void StopPattern()
|
||||
{
|
||||
_patternCts?.Cancel();
|
||||
_patternCts = null;
|
||||
|
||||
if (_isPatternRunning)
|
||||
{
|
||||
_isPatternRunning = false;
|
||||
if (!IsDisposed)
|
||||
{
|
||||
_lblTrack.Text = "";
|
||||
RefreshButtonStates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string ShowInputDialog(string prompt, string title)
|
||||
{
|
||||
using var dlg = new Form
|
||||
{
|
||||
Text = title,
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog,
|
||||
ClientSize = new Size(380, 100),
|
||||
StartPosition = FormStartPosition.CenterParent,
|
||||
MaximizeBox = false,
|
||||
MinimizeBox = false
|
||||
};
|
||||
var lbl = new Label { Text = prompt, Location = new Point(12, 12), Size = new Size(356, 20) };
|
||||
var txt = new TextBox { Location = new Point(12, 36), Size = new Size(356, 22) };
|
||||
var ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(200, 66), Size = new Size(80, 24) };
|
||||
var cancel = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(288, 66), Size = new Size(80, 24) };
|
||||
dlg.Controls.AddRange(new Control[] { lbl, txt, ok, cancel });
|
||||
dlg.AcceptButton = ok;
|
||||
dlg.CancelButton = cancel;
|
||||
return dlg.ShowDialog(this) == DialogResult.OK ? txt.Text.Trim() : "";
|
||||
}
|
||||
|
||||
void UpdateTrayIcon()
|
||||
{
|
||||
var newState = _state.IsSignaling ? "hot"
|
||||
|
||||
Reference in New Issue
Block a user