using SimpleCore; using SimpleCore.PropType; using StandardScene.Magnetic.Protocol; using System; using System.Collections.Generic; namespace StandardScene.Magnetic.Tasking { /// /// 将站点路径与站点/边 fields 合并为 FASS 2.0 0xB1 节点序列。 /// 对齐 backend CarRequestService.GetSendNodes:边属性挂在节点出边上。 /// public static class Fass2TaskBuilder { public const byte StartStopPass = 1; public const byte StartStopStop = 2; public const byte StartStopControlStart = 11; public const byte StartStopControlStop = 12; public const byte StartStopPrecision = 22; public static Fass2TaskPlan BuildPath(int startSiteId, int goalSiteId, Fass2TaskBuildOptions options, Func resolveNodeId, AbstractCar routingCar = null) { if (resolveNodeId == null) { throw new ArgumentNullException(nameof(resolveNodeId)); } options ??= new Fass2TaskBuildOptions(); var siteIds = Fass2RouteHelper.GetSitesBetween(startSiteId, goalSiteId, routingCar); if (siteIds.Count == 0) { throw new InvalidOperationException($"no path from site {startSiteId} to {goalSiteId}"); } var nodes = BuildNodesForSites(siteIds, options, resolveNodeId); return new Fass2TaskPlan { StartSiteId = startSiteId, GoalSiteId = goalSiteId, SiteIds = siteIds, Nodes = nodes, Batches = SplitBatches(nodes, options.MaxNodesPerFrame), FieldsSignature = Fass2SiteFieldReader.BuildFieldsSignature(siteIds, 0, SimpleLib.GetSite) }; } public static Fass2NodeMessage[] BuildSegment(int srcSiteId, int dstSiteId, Fass2TaskBuildOptions options, Func resolveNodeId, AbstractCar routingCar = null) { var plan = BuildPath(srcSiteId, dstSiteId, options, resolveNodeId, routingCar); var nodes = plan.Nodes; var array = new Fass2NodeMessage[nodes.Count]; for (var i = 0; i < nodes.Count; i++) { array[i] = nodes[i]; } return array; } public static IReadOnlyList SplitBatches(IReadOnlyList nodes, int maxPerFrame = 10) { if (nodes == null || nodes.Count == 0) { return Array.Empty(); } if (maxPerFrame <= 0 || maxPerFrame > 10) { maxPerFrame = 10; } var batches = new List(); for (var offset = 0; offset < nodes.Count; offset += maxPerFrame) { var count = Math.Min(maxPerFrame, nodes.Count - offset); var batch = new Fass2NodeMessage[count]; for (var i = 0; i < count; i++) { batch[i] = nodes[offset + i]; } batches.Add(batch); } return batches; } private static List BuildNodesForSites(IReadOnlyList siteIds, Fass2TaskBuildOptions options, Func resolveNodeId) { var nodes = new List(siteIds.Count); for (var i = 0; i < siteIds.Count; i++) { var siteId = siteIds[i]; var site = SimpleLib.GetSite(siteId); var siteAction = Fass2SiteFieldReader.Read(site); var isLast = i == siteIds.Count - 1; var nextSiteId = isLast ? -1 : siteIds[i + 1]; Fass2TrackMotionData trackMotion = null; Site currentSite = site; Site nextSite = null; if (!isLast) { nextSite = SimpleLib.GetSite(nextSiteId); var track = Fass2RouteHelper.FindTrack(siteId, nextSiteId); trackMotion = Fass2TrackFieldReader.Read(track); } var node = new Fass2NodeMessage { Node = resolveNodeId(siteId), StartStop = ResolveStartStop(siteAction, isLast), Distance = !isLast ? ComputeEdgeDistance(currentSite, nextSite) : (ushort)0, Speed = !isLast ? SpeedToProtocol(trackMotion?.Speed ?? options.DefaultSpeed) : (ushort)0, Orientation = !isLast ? ResolveOrientation(currentSite, nextSite, trackMotion) : (byte)0, Byroad = siteAction.Byroad ?? trackMotion?.Byroad ?? 0, Direction = siteAction.Direction ?? trackMotion?.Direction ?? 0, Lift = siteAction.Lift ?? 0, Roll = siteAction.Roll ?? 0, Charge = siteAction.Charge ?? 0, Obstacle = siteAction.Obstacle ?? 0, Audio = siteAction.Audio ?? 0, Light = siteAction.Light ?? 0, Rest = siteAction.Rest ?? 0, Clamp = siteAction.Clamp ?? 0, Tray = siteAction.Tray ?? 0, Shutdown = siteAction.Shutdown ?? 0 }; if (siteAction.Orientation.HasValue) { node.Orientation = siteAction.Orientation.Value; } nodes.Add(node); } return nodes; } private static byte ResolveStartStop(Fass2SiteActionData siteAction, bool isLast) { if (siteAction.StartStop.HasValue) { return siteAction.StartStop.Value; } if (isLast) { return siteAction.PrecisionStop ? StartStopPrecision : StartStopStop; } return StartStopPass; } private static byte ResolveOrientation(Site src, Site dst, Fass2TrackMotionData trackMotion) { if (trackMotion?.Orientation != null) { return trackMotion.Orientation.Value; } if (src == null || dst == null) { return 0; } var angle = trackMotion != null && trackMotion.EnableCarAbsoluteDirection ? trackMotion.CarAbsoluteDirection : Math.Atan2(dst.y - src.y, dst.x - src.x) / Math.PI * 180.0; if (trackMotion?.Reverse == true) { angle += 180; } angle += trackMotion?.CarDirectionBias ?? 0; angle %= 360; if (angle < 0) { angle += 360; } return (byte)((int)Math.Round(angle / 360.0 * 256) % 256); } private static ushort ComputeEdgeDistance(Site src, Site dst) { var dx = dst.x - src.x; var dy = dst.y - src.y; var length = Math.Sqrt(dx * dx + dy * dy); return (ushort)Math.Min(ushort.MaxValue, Math.Max(1, Math.Round(length))); } /// /// 协议速度单位 0.1 m/min;地图 Speed 默认 m/min,≤1 视为旧版 m/s 兼容。 /// public static ushort SpeedToProtocol(double speed) { if (speed > 0 && speed <= 1) { speed *= 60; } return (ushort)Math.Max(1, Math.Min(10000, Math.Round(speed * 10))); } } }