makepad/tools/gbelt_test/GbeltTest.cs
Admin 384d0e031c tools: the Builder replaces makepad_loader, the web server moves to makepad/webserver, fleet scripts, docs and the workspace members
tools/makepad_builder replaces tools/makepad_loader: one build target
shared across app builds, workspace package selection, checkout
progress on the public Git API, detached built apps with a completion
state, waits for Windows security scans, manual retry after compiler
locks, dedicated-folder installer checks, catalog and runtime fixes.
tools/web_server and its scripts leave for github.com/makepad/webserver.
Arch USB clone/restore scripts, the qwen38 box scripts and the G-belt
serial test join tools/. docs/agents records the agent workflow and the
remote-control handoff protocol; AGENTS.md forbids vendored sources and
bulk imports. Cargo.toml lists apps/wm-dyn, libs/code_language,
libs/search, libs/tar, libs/loader_bundle and tools/makepad_builder,
and drops the two removed crates.

Squashed from work:
- Share Builder target across Makepad app builds
- Fix Builder workspace package selection
- Align Builder checkout progress with public Git API
- Detach built apps and show completion state
- Wait for Windows security scans
- Offer manual retry after Windows compiler locks
- docs: the agent workflow of record and the remote-control handoff protocol
- builder: dedicated-folder installer checks, catalog and runtime fixes; Windows job objects hold c_void handles
- tools: Arch USB clone/restore scripts, the qwen38 box scripts, and the G-belt serial test
- tools: the web server moves to makepad/webserver
- AGENTS.md: no vendored sources or bulk imports in the tree

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 12:17:59 +02:00

511 lines
18 KiB
C#

using System;
using System.Drawing;
using System.Globalization;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace GbeltTest
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
sealed class MainForm : Form
{
readonly ComboBox _port = new ComboBox();
readonly ComboBox _baud = new ComboBox();
readonly Button _connect = new Button();
readonly CheckBox _stream = new CheckBox();
readonly CheckBox _invertLeft = new CheckBox();
readonly TrackBar _right = new TrackBar();
readonly TrackBar _left = new TrackBar();
readonly Label _rightVal = new Label();
readonly Label _leftVal = new Label();
readonly Label _pkt = new Label();
readonly Label _tx = new Label();
readonly TextBox _log = new TextBox();
readonly System.Windows.Forms.Timer _timer = new System.Windows.Forms.Timer();
readonly System.Windows.Forms.Timer _tug = new System.Windows.Forms.Timer();
readonly object _io = new object();
SerialPort _sp;
int _txCount;
byte _seq = 1;
bool _ready;
bool _initing;
int _tugWhich; // 1=right 2=left
int _tugSaved;
const int Rest = 16384;
const byte Stx = 0x02;
const byte Etx = 0x03;
const byte Footer = 0x7D;
public MainForm()
{
Text = "G-Belt test v3 (921600, captured init + 64-byte S stream)";
Width = 640;
Height = 720;
FormBorderStyle = FormBorderStyle.FixedSingle;
MaximizeBox = false;
int y = 12;
Controls.Add(Lbl("Port", 12, y + 4));
_port.DropDownStyle = ComboBoxStyle.DropDownList;
_port.SetBounds(52, y, 90, 24);
foreach (var p in SerialPort.GetPortNames()) _port.Items.Add(p);
if (_port.Items.Contains("COM3")) _port.SelectedItem = "COM3";
else if (_port.Items.Count > 0) _port.SelectedIndex = 0;
Controls.Add(_port);
Controls.Add(Lbl("Baud", 150, y + 4));
_baud.DropDownStyle = ComboBoxStyle.DropDownList;
_baud.SetBounds(190, y, 90, 24);
foreach (var b in new[] { 115200, 230400, 460800, 921600 })
_baud.Items.Add(b);
_baud.SelectedItem = 921600;
Controls.Add(_baud);
_connect.Text = "Connect + init";
_connect.SetBounds(300, y, 120, 28);
_connect.Click += ConnectClick;
Controls.Add(_connect);
var refresh = new Button { Text = "Ports", Left = 430, Top = y, Width = 70, Height = 28 };
refresh.Click += (s, e) => RefreshPorts();
Controls.Add(refresh);
y += 36;
_stream.Text = "Stream 50 Hz (S+O+F+F)";
_stream.SetBounds(12, y, 220, 22);
_stream.Checked = true;
Controls.Add(_stream);
_invertLeft.Text = "Invert left (Commander does)";
_invertLeft.SetBounds(240, y, 220, 22);
_invertLeft.Checked = true;
Controls.Add(_invertLeft);
_tx.SetBounds(460, y, 150, 22);
_tx.Text = "TX 0";
Controls.Add(_tx);
y += 28;
Controls.Add(Lbl("G-Belt Right (port 1 / S442)", 12, y));
_right.Minimum = 0;
_right.Maximum = 32767;
_right.TickFrequency = 4096;
_right.SetBounds(12, y + 16, 460, 32);
_right.AutoSize = false;
_right.Value = Rest;
_right.Scroll += (s, e) => { ShowVal(_right, _rightVal); Kick(); };
Controls.Add(_right);
_rightVal.SetBounds(480, y + 20, 130, 22);
Controls.Add(_rightVal);
ShowVal(_right, _rightVal);
y += 56;
Controls.Add(Lbl("G-Belt Left (port 2 / S441)", 12, y));
_left.Minimum = 0;
_left.Maximum = 32767;
_left.TickFrequency = 4096;
_left.SetBounds(12, y + 16, 460, 32);
_left.AutoSize = false;
_left.Value = Rest;
_left.Scroll += (s, e) => { ShowVal(_left, _leftVal); Kick(); };
Controls.Add(_left);
_leftVal.SetBounds(480, y + 20, 130, 22);
Controls.Add(_leftVal);
ShowVal(_left, _leftVal);
y += 56;
AddBtn("Rest 16384", 12, y, (s, e) => { SetBoth(Rest); Kick(true); });
AddBtn("Slack 0", 120, y, (s, e) => { SetBoth(0); Kick(true); });
AddBtn("Tug right", 230, y, (s, e) => Tug(1));
AddBtn("Tug left", 340, y, (s, e) => Tug(2));
AddBtn("Enable live", 450, y, (s, e) =>
{
if (_sp == null || !_sp.IsOpen) { Log("not connected"); return; }
var t = new Thread(() =>
{
byte[] drive = { 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0xFF };
byte[] hwOn = { 0x01, 0xE8, 0x03, 0xC8, 0x01, 0x84, 0x03, 0xB8, 0x0B };
byte[] load = { 0x00, 0x12, 0x01, 0x03, 0x1E, 0x00, 0x01, 0x00, 0x00, 0x02 };
byte[] ang = { 0x5A, 0x28, 0x32, 0x3C, 0x46, 0x50, 0x5A, 0x64, 0x6E };
UiLog("enable live 0x01/0x02");
ApplyLive(0x01, drive, hwOn, load, ang);
ApplyLive(0x02, drive, hwOn, load, ang);
_ready = true;
UiLog("enable live done");
});
t.IsBackground = true;
t.Start();
});
y += 40;
_pkt.SetBounds(12, y, 600, 48);
_pkt.Font = new Font("Consolas", 8);
Controls.Add(_pkt);
y += 52;
_log.Multiline = true;
_log.ReadOnly = true;
_log.ScrollBars = ScrollBars.Vertical;
_log.Font = new Font("Consolas", 8);
_log.SetBounds(12, y, 600, 250);
Controls.Add(_log);
_timer.Interval = 10;
_timer.Tick += (s, e) =>
{
if (_ready && _stream.Checked) Kick(false);
DrainRx();
};
_timer.Start();
_tug.Interval = 400;
_tug.Tick += TugDone;
FormClosing += (s, e) => ClosePort();
Log("Close Sim Commander before Connect — COM3 is exclusive.");
Log("Live capture: 921600 8N1, init R/w/C/D/H/L/d, then 64-byte S+O+F9+F8.");
Log("Rest position in-game was ~16384 (left inverted to -16384).");
}
void AddBtn(string text, int x, int y, EventHandler click)
{
var b = new Button { Text = text, Left = x, Top = y, Width = 100, Height = 28 };
b.Click += click;
Controls.Add(b);
}
static Label Lbl(string t, int x, int y)
{
return new Label { Text = t, Left = x, Top = y, AutoSize = true };
}
static void ShowVal(TrackBar bar, Label lab)
{
lab.Text = bar.Value + " (" + (bar.Value * 100 / 32767) + "%)";
}
void SetBoth(int v)
{
_right.Value = v;
_left.Value = v;
ShowVal(_right, _rightVal);
ShowVal(_left, _leftVal);
}
static byte[] P16(byte addr, byte type, params byte[] payload)
{
var p = new byte[16];
p[0] = Stx;
p[1] = addr;
p[2] = type;
int n = payload.Length;
if (n > 11) n = 11;
if (n > 0) Array.Copy(payload, 0, p, 3, n);
p[14] = Footer;
p[15] = Etx;
return p;
}
static void PutI16(byte[] p, int o, int v)
{
if (v < -32768) v = -32768;
if (v > 32767) v = 32767;
p[o] = (byte)(v & 0xFF);
p[o + 1] = (byte)((v >> 8) & 0xFF);
}
byte[] Stream64()
{
int r = _right.Value;
int l = _left.Value;
if (_invertLeft.Checked) l = -l;
byte seq = _seq;
_seq++;
if (_seq == 0) _seq = 1;
var p = new byte[64];
// S
p[0] = Stx; p[1] = seq; p[2] = 0x53;
PutI16(p, 3, r);
PutI16(p, 5, l);
p[13] = 0x03; p[14] = Footer; p[15] = Etx;
// O
p[16] = Stx; p[17] = seq; p[18] = 0x4F;
p[29] = 0x04; p[30] = Footer; p[31] = Etx;
// F page 9
p[32] = Stx; p[33] = 0x09; p[34] = 0x46;
p[42] = Footer; p[43] = Etx;
// F page 8
p[48] = Stx; p[49] = 0x08; p[50] = 0x46;
p[58] = Footer; p[59] = Etx;
return p;
}
static string Hex(byte[] p, int max)
{
int n = p.Length < max ? p.Length : max;
var sb = new StringBuilder(n * 3);
for (int i = 0; i < n; i++) sb.Append(p[i].ToString("X2")).Append(' ');
if (p.Length > max) sb.Append("...");
return sb.ToString();
}
void RefreshPorts()
{
var cur = _port.SelectedItem as string;
_port.Items.Clear();
foreach (var p in SerialPort.GetPortNames()) _port.Items.Add(p);
if (cur != null && _port.Items.Contains(cur)) _port.SelectedItem = cur;
else if (_port.Items.Contains("COM3")) _port.SelectedItem = "COM3";
else if (_port.Items.Count > 0) _port.SelectedIndex = 0;
Log("ports: " + string.Join(", ", SerialPort.GetPortNames()));
}
void ConnectClick(object sender, EventArgs e)
{
if (_sp != null && _sp.IsOpen) { ClosePort(); return; }
if (_port.SelectedItem == null) { Log("No COM port."); return; }
if (_initing) { Log("init already running"); return; }
try
{
_sp = new SerialPort((string)_port.SelectedItem, (int)_baud.SelectedItem, Parity.None, 8, StopBits.One);
_sp.Handshake = Handshake.None;
_sp.DtrEnable = true;
_sp.RtsEnable = true;
_sp.ReadTimeout = 50;
_sp.WriteTimeout = 500;
_sp.Open();
_connect.Text = "Disconnect";
_txCount = 0;
_seq = 1;
_ready = false;
Log("opened " + _sp.PortName + " @ " + _sp.BaudRate);
_initing = true;
var t = new Thread(InitThread);
t.IsBackground = true;
t.Start();
}
catch (Exception ex)
{
Log("open failed: " + ex.Message);
Log("If access denied, close Sim Commander first.");
ClosePort();
}
}
void InitThread()
{
try
{
UiLog("init: reset");
Send(P16(0x00, 0x52), true);
Thread.Sleep(20);
UiLog("init: query interfaces 1-4");
Send(P16(0x81, 0x77), true); Thread.Sleep(100);
Send(P16(0x82, 0x77), true); Thread.Sleep(100);
Send(P16(0x83, 0x77), true); Thread.Sleep(100);
Send(P16(0x84, 0x77), true); Thread.Sleep(150);
byte[] drive = { 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0xFF };
byte[] hwOn = { 0x01, 0xE8, 0x03, 0xC8, 0x01, 0x84, 0x03, 0xB8, 0x0B };
byte[] hwOff = { 0x00, 0xE8, 0x03, 0xC8, 0x01, 0x84, 0x03, 0xB8, 0x0B };
byte[] load = { 0x00, 0x12, 0x01, 0x03, 0x1E, 0x00, 0x01, 0x00, 0x00, 0x02 };
byte[] ang = { 0x5A, 0x28, 0x32, 0x3C, 0x46, 0x50, 0x5A, 0x64, 0x6E };
UiLog("init: right (port 1) D/H/L/d");
Send(P16(0x07, 0x43, 0xFF), true); Thread.Sleep(20);
ConfigPort(0x41, drive, hwOn, load, ang);
UiLog("init: left (port 2) D/H/L/d");
Send(P16(0x07, 0x43, 0xFF), true); Thread.Sleep(20);
ConfigPort(0x42, drive, hwOn, load, ang);
UiLog("init: calibrate (wait 4.5s for home)");
Send(P16(0x03, 0x43, 0x02, 0x00, 0x40, 0x64, 0x00, 0x1E, 0x1E, 0xB4, 0x14), true);
Send(P16(0x05, 0x43, 0x01, 0x00, 0x40, 0x64, 0x00, 0x1E, 0x1E, 0xB4, 0x14), true);
Send(P16(0x86, 0x43), true);
Thread.Sleep(4500);
UiLog("init: post-home config 0x41/0x42 H=00");
ConfigPort(0x41, drive, hwOff, load, ang);
ConfigPort(0x42, drive, hwOff, load, ang);
// Game-start: apply LIVE interface addrs 0x01/0x02 (not 0x41/0x42)
// order L, d, D, H with hardware-enable=01.
UiLog("init: enable live ifaces 0x01/0x02 (L/d/D/H=01)");
ApplyLive(0x01, drive, hwOn, load, ang);
ApplyLive(0x02, drive, hwOn, load, ang);
BeginInvoke(new Action(() =>
{
SetBoth(Rest);
_ready = true;
Kick(true);
Log("init done — sliders live. Rest=16384. Tug should move now.");
}));
}
catch (Exception ex)
{
UiLog("init failed: " + ex.Message);
}
finally
{
_initing = false;
}
}
void ConfigPort(byte addr, byte[] drive, byte[] hw, byte[] load, byte[] ang)
{
Send(P16(addr, 0x44, drive), true); Thread.Sleep(10);
Send(P16(addr, 0x48, hw), true); Thread.Sleep(10);
Send(P16(addr, 0x4C, load), true); Thread.Sleep(10);
Send(P16(addr, 0x64, ang), true); Thread.Sleep(15);
}
void ApplyLive(byte addr, byte[] drive, byte[] hw, byte[] load, byte[] ang)
{
Send(P16(addr, 0x4C, load), true); Thread.Sleep(8);
Send(P16(addr, 0x64, ang), true); Thread.Sleep(8);
Send(P16(addr, 0x44, drive), true); Thread.Sleep(8);
Send(P16(addr, 0x48, hw), true); Thread.Sleep(10);
}
void Kick(bool log)
{
if (_sp == null || !_sp.IsOpen || !_ready) return;
Send(Stream64(), log);
}
void Kick()
{
Kick(false);
}
void Tug(int which)
{
_tug.Stop();
_tugWhich = which;
var bar = which == 1 ? _right : _left;
_tugSaved = bar.Value;
int v = _tugSaved + 4000;
if (v > 32767) v = 32767;
bar.Value = v;
ShowVal(bar, which == 1 ? _rightVal : _leftVal);
Kick(true);
Log("tug " + (which == 1 ? "right" : "left") + " " + _tugSaved + " -> " + v);
_tug.Start();
}
void TugDone(object sender, EventArgs e)
{
_tug.Stop();
var bar = _tugWhich == 1 ? _right : _left;
var lab = _tugWhich == 1 ? _rightVal : _leftVal;
if (_tugSaved < bar.Minimum) _tugSaved = bar.Minimum;
if (_tugSaved > bar.Maximum) _tugSaved = bar.Maximum;
bar.Value = _tugSaved;
ShowVal(bar, lab);
Kick(true);
_tugWhich = 0;
}
void Send(byte[] p, bool log)
{
_pkt.Text = Hex(p, 64);
if (_sp == null || !_sp.IsOpen) return;
lock (_io)
{
try
{
_sp.Write(p, 0, p.Length);
_txCount++;
UiTx();
if (log) UiLog("TX " + p.Length + "B " + Hex(p, 32));
}
catch (Exception ex)
{
UiLog("write failed: " + ex.Message);
}
}
}
void DrainRx()
{
if (_sp == null || !_sp.IsOpen) return;
lock (_io)
{
try
{
int n = _sp.BytesToRead;
if (n <= 0) return;
var buf = new byte[Math.Min(n, 256)];
int r = _sp.Read(buf, 0, buf.Length);
if (r > 0) UiLog("RX " + r + "B " + Hex(Slice(buf, r), 48));
}
catch { }
}
}
static byte[] Slice(byte[] b, int n)
{
var o = new byte[n];
Array.Copy(b, o, n);
return o;
}
void ClosePort()
{
_ready = false;
_initing = false;
try
{
if (_sp != null && _sp.IsOpen)
{
try
{
SetBoth(Rest);
_sp.Write(Stream64(), 0, 64);
}
catch { }
_sp.Close();
}
}
catch { }
_sp = null;
_connect.Text = "Connect + init";
_tx.Text = "TX " + _txCount + " (disconnected)";
}
void UiLog(string s)
{
if (InvokeRequired) { BeginInvoke(new Action<string>(UiLog), s); return; }
Log(s);
}
void UiTx()
{
if (InvokeRequired) { BeginInvoke(new Action(UiTx)); return; }
_tx.Text = "TX " + _txCount;
}
void Log(string s)
{
if (_log.TextLength > 40000) _log.Clear();
_log.AppendText(DateTime.Now.ToString("HH:mm:ss.fff ", CultureInfo.InvariantCulture) + s + "\r\n");
}
}
}