Use binary multi-vehicle sync transport

This commit is contained in:
2026-07-01 13:27:15 +08:00
parent 3aa979be8b
commit c9cb3a5df3
2 changed files with 348 additions and 71 deletions
+99 -71
View File
@@ -17,7 +17,6 @@ using CommonUsage.Mathematics;
using FundamentalLib;
using FundamentalLib.Utilities;
using MDCSToolBox.Clumsy.Pilot.MultiWheel;
using Newtonsoft.Json;
using Vector = ClumsyCore.Utilities.Vector;
namespace MultiWheelC;
@@ -426,73 +425,32 @@ public class PilotDefinition : MultiWheelPilotDefinition<PilotConfig, PilotDefin
var hc = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
PicoHttpServer.AddGetHandler("/multi-vehicle-register", new { CarNum = 0, Info = "" }, query =>
PicoHttpServer.AddPostByteHandler("/multi-vehicle-register-bin", body =>
{
try
{
var infoJson = Uri.UnescapeDataString(query.Info ?? "");
var info = JsonConvert.DeserializeObject<VehicleSyncInfo>(infoJson)
?? throw new Exception("VehicleSyncInfo is null");
lock (FleetLock)
{
MultiVehicleFleet[query.CarNum] = info;
_multiVehicleFleetSeen[query.CarNum] = DateTime.Now; // C: 刷新存活时刻
}
return JsonConvert.SerializeObject(new { code = 200, message = "ok" });
var (carNum, info) = VehicleSyncBinaryCodec.DecodeRegister(body);
ApplyVehicleSyncRegister(carNum, info);
return "ok";
}
catch (Exception e)
{
DLog.Log($"/multi-vehicle-register error: {e.FormatEx()}", "MultiVehicle");
return JsonConvert.SerializeObject(new { code = 500, message = e.Message });
DLog.Log($"/multi-vehicle-register-bin error: {e.FormatEx()}", "MultiVehicle");
throw;
}
});
// F: notify 改用 POST + JSON body(取代 GET query 串),避免整队 Fleet 字典撑爆 URL 长度上限;
// 用 Seq 丢弃乱序到达的旧包,避免从车短暂套用过期指令。
PicoHttpServer.AddPostTextHandler("/multi-vehicle-notify", body =>
PicoHttpServer.AddPostByteHandler("/multi-vehicle-notify-bin", body =>
{
try
{
var notification = JsonConvert.DeserializeObject<VehicleSyncNotification>(body ?? "")
?? throw new Exception("notification is null");
// 乱序丢弃:仅应用序列号大于已应用值的包。Seq==0 视为旧版无序列号始终接受;
// 若 Seq 明显回退(差值>100),判定为主车重启的新会话,重新接受并对齐序列号。
if (notification.Seq != 0 && notification.Seq <= _multiVehicleAppliedSeq &&
notification.Seq > _multiVehicleAppliedSeq - 100)
return JsonConvert.SerializeObject(new { code = 200, message = "stale" });
_multiVehicleAppliedSeq = notification.Seq;
MultiVehicleAligned = notification.Aligned;
lock (FleetLock)
{
MultiVehicleFleet = notification.Fleet ?? new Dictionary<int, VehicleSyncInfo>();
// C: notify 内含整队成员,逐一刷新其本地存活时刻。
var nowSeen = DateTime.Now;
foreach (var key in MultiVehicleFleet.Keys)
_multiVehicleFleetSeen[key] = nowSeen;
}
lock (_multiVehicleNotificationLock)
{
MultiVehicleNotification = notification;
_multiVehicleLastNotifyTime = DateTime.Now;
}
var applyNow = DateTime.Now;
if ((applyNow - _mvNotifyApplyLastLog).TotalMilliseconds >= 200)
{
_mvNotifyApplyLastLog = applyNow;
DLog.Log(
$"car{CarNum} NOTIFY_APPLY seq={notification.Seq} mode={notification.Mode} " +
$"omega={notification.FleetOmega:0.000} reqOmega={notification.RequestedFleetOmega:0.000} " +
$"released={notification.FleetMotionReleased} stop={notification.FleetStopActive} " +
$"reason={notification.FleetStopReason}",
"MultiVehicleRemoteDbg");
}
return JsonConvert.SerializeObject(new { code = 200, message = "ok" });
var notification = VehicleSyncBinaryCodec.DecodeNotification(body);
return ApplyVehicleSyncNotification(notification);
}
catch (Exception e)
{
DLog.Log($"/multi-vehicle-notify error: {e.FormatEx()}", "MultiVehicle");
return JsonConvert.SerializeObject(new { code = 500, message = e.Message });
DLog.Log($"/multi-vehicle-notify-bin error: {e.FormatEx()}", "MultiVehicle");
throw;
}
});
@@ -501,6 +459,53 @@ public class PilotDefinition : MultiWheelPilotDefinition<PilotConfig, PilotDefin
new Thread(() => MultiVehicleLoop(hc)) { Name = "MultiVehicle", IsBackground = true }.Start();
}
private void ApplyVehicleSyncRegister(int carNum, VehicleSyncInfo info)
{
lock (FleetLock)
{
MultiVehicleFleet[carNum] = info;
_multiVehicleFleetSeen[carNum] = DateTime.Now;
}
}
private string ApplyVehicleSyncNotification(VehicleSyncNotification notification)
{
// Keep the same stale-packet semantics as the previous transport.
if (notification.Seq != 0 && notification.Seq <= _multiVehicleAppliedSeq &&
notification.Seq > _multiVehicleAppliedSeq - 100)
return "stale";
_multiVehicleAppliedSeq = notification.Seq;
MultiVehicleAligned = notification.Aligned;
lock (FleetLock)
{
MultiVehicleFleet = notification.Fleet ?? new Dictionary<int, VehicleSyncInfo>();
var nowSeen = DateTime.Now;
foreach (var key in MultiVehicleFleet.Keys)
_multiVehicleFleetSeen[key] = nowSeen;
}
lock (_multiVehicleNotificationLock)
{
MultiVehicleNotification = notification;
_multiVehicleLastNotifyTime = DateTime.Now;
}
var applyNow = DateTime.Now;
if ((applyNow - _mvNotifyApplyLastLog).TotalMilliseconds >= 200)
{
_mvNotifyApplyLastLog = applyNow;
DLog.Log(
$"car{CarNum} NOTIFY_APPLY seq={notification.Seq} mode={notification.Mode} " +
$"omega={notification.FleetOmega:0.000} reqOmega={notification.RequestedFleetOmega:0.000} " +
$"released={notification.FleetMotionReleased} stop={notification.FleetStopActive} " +
$"reason={notification.FleetStopReason}",
"MultiVehicleRemoteDbg");
}
return "ok";
}
private void MultiVehicleLoop(HttpClient hc)
{
var carLength = CarLength;
@@ -1524,29 +1529,52 @@ public class PilotDefinition : MultiWheelPilotDefinition<PilotConfig, PilotDefin
private void FireAndForgetRegister(HttpClient hc, VehicleSyncInfo info)
{
ParseMasterEndpoint(out var masterIp, out var masterPort);
var infoJson = JsonConvert.SerializeObject(info);
var url =
$"http://{masterIp}:{masterPort}/multi-vehicle-register?CarNum={CarNum}&Info={Uri.EscapeDataString(infoJson)}";
_ = hc.GetStringAsync(url).ContinueWith(t =>
try
{
if (t.IsFaulted)
DLog.Log($"register failed: {t.Exception?.GetBaseException().Message}", "MultiVehicle");
}, TaskScheduler.Default);
ParseMasterEndpoint(out var masterIp, out var masterPort);
var payload = VehicleSyncBinaryCodec.EncodeRegister(CarNum, info);
var url = $"http://{masterIp}:{masterPort}/multi-vehicle-register-bin";
_ = PostBinaryAsync(hc, url, payload, "register");
}
catch (Exception e)
{
DLog.Log($"register binary encode failed: {e.GetBaseException().Message}", "MultiVehicle");
}
}
private static void FireAndForgetNotify(HttpClient hc, string ip, int port, VehicleSyncNotification notification)
private void FireAndForgetNotify(HttpClient hc, string ip, int port, VehicleSyncNotification notification)
{
// F: POST + JSON bodypayload 不再受 URL 长度限制;fire-and-forget 但记录失败。
var notifyJson = JsonConvert.SerializeObject(notification);
var url = $"http://{ip}:{port}/multi-vehicle-notify";
var content = new StringContent(notifyJson, System.Text.Encoding.UTF8, "application/json");
_ = hc.PostAsync(url, content).ContinueWith(t =>
try
{
content.Dispose();
if (t.IsFaulted)
DLog.Log($"notify {ip}:{port} failed: {t.Exception?.GetBaseException().Message}", "MultiVehicle");
}, TaskScheduler.Default);
var payload = VehicleSyncBinaryCodec.EncodeNotification(notification);
var url = $"http://{ip}:{port}/multi-vehicle-notify-bin";
_ = PostBinaryAsync(hc, url, payload, $"notify {ip}:{port}");
}
catch (Exception e)
{
DLog.Log($"notify {ip}:{port} binary encode failed: {e.GetBaseException().Message}", "MultiVehicle");
}
}
private static async Task PostBinaryAsync(HttpClient hc, string url, byte[] payload, string description)
{
try
{
using var content = new ByteArrayContent(payload);
content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
using var response = await hc.PostAsync(url, content).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
return;
DLog.Log(
$"{description} binary failed: HTTP {(int)response.StatusCode} {response.ReasonPhrase}",
"MultiVehicle");
}
catch (Exception e)
{
DLog.Log($"{description} binary failed: {e.GetBaseException().Message}", "MultiVehicle");
}
}
private void ResolveMultiVehicleSelfEndpoint(out string ip, out int port)