using RobotNet.MapShares.Dtos; using RobotNet.RobotManager.Services.OpenACS; using RobotNet.RobotManager.Services.Planner.Space; using RobotNet.RobotManager.Services.Simulation.Algorithm; using RobotNet.RobotManager.Services.Simulation.Models; using RobotNet.RobotManager.Services.Traffic; using RobotNet.RobotShares.Dtos; using RobotNet.RobotShares.Enums; using RobotNet.Shares; namespace RobotNet.RobotManager.Services.Simulation; public abstract class NavigationService : INavigationService, IDisposable { protected readonly VisualizationService Visualization; protected readonly VelocityController VelocityController; protected readonly RobotSimulationModel RobotModel; protected readonly TrafficManager TrafficManager; protected readonly TrafficACS TrafficACS; protected readonly PathPlanner PathPlanner; protected readonly MapManager MapManager; protected readonly LoggerController Logger; private WatchTimer? navTimer; protected const int SampleTimeMilliseconds = 50; private WatchTimerAsync? checkingTimer; private const int CheckingTimeMilliseconds = 1000; protected double TargetAngle = 0; protected PID? RotatePID; protected readonly double AngularVelocity; protected PID? MovePID; protected FuzzyLogic? MoveFuzzy; protected PurePursuit? MovePurePursuit; private Guid TrafficManagerGoalId = Guid.Empty; private Guid TrafficACSGoalId = Guid.Empty; protected NavigationNode? NavigationGoal; protected NavigationNode? FinalGoal; protected List? NavigationPath; protected List CheckingNodes = []; private List RefreshPath = []; private List BaseNodes = []; private bool IsWaittingStop; private double RotateAngle; protected Dictionary Zones = []; protected NavigationAction Action = NavigationAction.Stop; public NavigationNode? InNode { get; set; } = null; public NavigationStateType NavigationState { get; set; } = NavigationStateType.Ready; public TrafficSolutionState TrafficSolutionState { get; private set; } = TrafficSolutionState.None; public bool IsError { get; private set; } public bool IsCompleted { get; private set; } = true; public bool IsProcessing { get; private set; } public bool IsCanceled { get; private set; } public string[] Errors => []; public NavigationPathEdge[] FullPath => GetFullPath(); public NavigationPathEdge[] BasePath => GetBasePath(); public Guid MapId { get; set; } = Guid.Empty; public List CurrentZones { get; set; } = []; public NavigationService(VisualizationService visualization, RobotSimulationModel model, IServiceProvider ServiceProvider) { Visualization = visualization; VelocityController = new VelocityController(Visualization).WithDeceleration(model.Deceleration).WithAcceleration(model.Acceleration).WithSampleTime(SampleTimeMilliseconds / 1000.0); RobotModel = model; AngularVelocity = MathExtension.CheckLimit(RobotModel.MaxAngularVelocity * RobotModel.Width / RobotModel.RadiusWheel, 2 * Math.PI, 0); TrafficManager = ServiceProvider.GetRequiredService(); TrafficACS = ServiceProvider.GetRequiredService(); PathPlanner = ServiceProvider.GetRequiredService(); MapManager = ServiceProvider.GetRequiredService(); Logger = ServiceProvider.GetRequiredService>(); } public virtual MessageResult Rotate(double angle) { RotatePID = new PID().WithKp(10).WithKi(0.01).WithKd(0.1); TargetAngle = MathExtension.CheckLimit(angle, 180, -180); Action = NavigationAction.Rotate; NavStart(); CheckingStart(); return new(true); } public virtual MessageResult MoveStraight(NavigationNode[] path) { if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); NavigationPath = [.. path]; CheckingNodes = [NavigationPath[0], NavigationPath[^1]]; InNode = CheckingNodes[0]; MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); MoveFuzzy = new FuzzyLogic().WithGainP(1.1); MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath(path); double Angle = Math.Atan2(path[1].Y - path[0].Y, path[1].X - path[0].X); Rotate(Angle * 180 / Math.PI); return new(true); } public virtual MessageResult Move(NavigationNode[] path) { if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); NavigationPath = [.. path]; CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; if (CheckingNodes.Count < 2) return new(false, "Đường dẫn traffic không hợp lệ"); InNode = CheckingNodes[0]; MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); MoveFuzzy = new FuzzyLogic().WithGainP(1.1); MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); double Angle = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X); Rotate(Angle * 180 / Math.PI); return new(true); } public virtual MessageResult Move(NodeDto[] nodes, EdgeDto[] edges) { if (nodes.Length < 2) return new(false, "Đường dẫn không hợp lệ"); var pathNodes = PathPlanner.CalculatorDirection(MapCompute.GetRobotDirection(nodes[0].Direction), nodes, edges); var pathSplit = PathPlanner.PathSplit(pathNodes, edges); if (!pathSplit.IsSuccess) return new(false, pathSplit.Message); if (pathSplit.Data is null) return new(false, "Đường dẫn không hợp lệ"); var getZones = PathPlanner.GetZones(MapId, nodes); getZones.Wait(); Zones = getZones.Result.Data ?? []; NavigationPath = [..pathSplit.Data.Select(n => new NavigationNode() { Id = n.Id, X = n.X, Y = n.Y, Theta = n.Theta, Direction = MapCompute.GetRobotDirection(n.Direction), Actions = n.Actions, })]; CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; InNode = CheckingNodes[0]; FinalGoal = CheckingNodes[^1]; MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); MoveFuzzy = new FuzzyLogic().WithGainP(1.1); MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI; double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI; Rotate(CheckingNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward); return new(true); } public MessageResult Cancel() { Dispose(); VelocityController.Stop(); NavigationState = NavigationStateType.Ready; IsCanceled = true; return new(true); } protected virtual void NavigationHandler() { } private void HandleRefreshPath(TrafficSolution trafficSolution) { var edgesDto = trafficSolution.Edges.Select(e => new EdgeDto { Id = e.Id, StartNodeId = e.StartNodeId, EndNodeId = e.EndNodeId, TrajectoryDegree = e.TrajectoryDegree, ControlPoint1X = e.ControlPoint1X, ControlPoint1Y = e.ControlPoint1Y, ControlPoint2X = e.ControlPoint2X, ControlPoint2Y = e.ControlPoint2Y, }); List nodesDto = [..trafficSolution.Nodes.Select(n => new NodeDto { Id = n.Id, X = n.X, Y = n.Y, Name = n.Name, Direction = MapCompute.GetNodeDirection(n.Direction), })]; var splitPath = PathPlanner.PathSplit([.. nodesDto], [.. edgesDto]); if (splitPath.IsSuccess) { if (splitPath.Data != null) { RefreshPath = [..splitPath.Data.Select(n => new NavigationNode() { Id = n.Id, X = n.X, Y = n.Y, Theta = n.Theta, Direction = MapCompute.GetRobotDirection(n.Direction), Actions = n.Actions, })]; } } } private void HandleCompleteState(TrafficSolution trafficSolution) { if (trafficSolution.State == TrafficSolutionState.Complete && RefreshPath.Count > 0) { NavigationPath = RefreshPath; CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; if (MovePurePursuit is not null) { MovePurePursuit.UpdatePath([.. NavigationPath]); MovePurePursuit.OnNodeIndex = MovePurePursuit.GetOnNode(Visualization.X, Visualization.Y).index; } RefreshPath = []; double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI; double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI; TargetAngle = MathExtension.CheckLimit(CheckingNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward, 180, -180); Action = NavigationAction.Rotate; } } private void HandleGivewayState(TrafficSolution trafficSolution) { if (trafficSolution.GivewayNodes[^1].Id == trafficSolution.ReleaseNode.Id && NavigationGoal?.Id != trafficSolution.ReleaseNode.Id) { var splitPath = PathPlanner.PathSplit([..trafficSolution.GivewayNodes.Select(n => new NodeDto { Id = n.Id, X = n.X, Y = n.Y, Name = n.Name, Direction = MapCompute.GetNodeDirection(n.Direction), })], [.. trafficSolution.GivewayEdges.Select(e => new EdgeDto { Id = e.Id, StartNodeId = e.StartNodeId, EndNodeId = e.EndNodeId, TrajectoryDegree = e.TrajectoryDegree, ControlPoint1X = e.ControlPoint1X, ControlPoint1Y = e.ControlPoint1Y, ControlPoint2X = e.ControlPoint2X, ControlPoint2Y = e.ControlPoint2Y, })]); if (splitPath.IsSuccess && splitPath.Data != null && MovePurePursuit != null) { NavigationPath = [..splitPath.Data.Select(n => new NavigationNode() { Id = n.Id, X = n.X, Y = n.Y, Theta = n.Theta, Direction = MapCompute.GetRobotDirection(n.Direction), Actions = n.Actions, })]; CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; MovePurePursuit.UpdatePath([.. NavigationPath]); MovePurePursuit.OnNodeIndex = 0; double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI; double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI; TargetAngle = MathExtension.CheckLimit(CheckingNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward, 180, -180); Action = NavigationAction.Rotate; } } } private async Task RequestInACS(Dictionary newZones) { Guid trafficACSrelaseNodeId = Guid.Empty; foreach (var zone in newZones) { if (zone.Value.Length == 0) trafficACSrelaseNodeId = zone.Key; else { bool requestSuccess = true; foreach (var zoneACS in zone.Value) { if (string.IsNullOrEmpty(zoneACS.Name)) continue; if (CurrentZones.Any(z => z.Id == zoneACS.Id)) continue; var getInTrafficACS = await TrafficACS.RequestIn(RobotModel.RobotId, zoneACS.Name); if (getInTrafficACS.IsSuccess && getInTrafficACS.Data) CurrentZones.Add(zoneACS); else { requestSuccess = false; break; } } if (requestSuccess) trafficACSrelaseNodeId = zone.Key; else break; } } return trafficACSrelaseNodeId; } private void UpdateTraffic() { var trafficManagerGoalIndex = CheckingNodes.FindIndex(n => n.Id == TrafficManagerGoalId); var trafficACSGoalIndex = CheckingNodes.FindIndex(n => n.Id == TrafficACSGoalId); if (trafficManagerGoalIndex != -1 && trafficACSGoalIndex != -1) { var goalIndex = Math.Min(trafficManagerGoalIndex, trafficACSGoalIndex); NavigationNode goal = CheckingNodes[goalIndex]; var inNodeIndex = CheckingNodes.FindIndex(n => n.Id == (InNode is null ? Guid.Empty : InNode.Id)); inNodeIndex = inNodeIndex != -1 ? inNodeIndex : 0; if (BaseNodes.Count == 0 || BaseNodes[^1].Id != goal.Id) BaseNodes = CheckingNodes.GetRange(inNodeIndex, goalIndex + 1 - inNodeIndex); if (IsWaittingStop) { if (NavigationState == NavigationStateType.Waitting) { TargetAngle = MathExtension.CheckLimit(RotateAngle, 180, -180); Action = NavigationAction.Rotate; IsWaittingStop = false; } } else { for (int i = inNodeIndex + 1; i <= goalIndex; i++) { if (Math.Abs(CheckingNodes[i].Theta - CheckingNodes[i - 1].Theta) > 30) { goal = CheckingNodes[i]; IsWaittingStop = true; RotateAngle = CheckingNodes[i].Theta; break; } } UpdateGoal(goal); } } } private async Task GoOutTrafficACS() { if (BaseNodes is null || BaseNodes.Count == 0 || InNode is null) return; var goalIndex = CheckingNodes.FindIndex(n => n.Id == BaseNodes[^1].Id); if (goalIndex == -1) return; var inNodeIndex = CheckingNodes.FindIndex(n => n.Id == InNode.Id); inNodeIndex = inNodeIndex != -1 ? inNodeIndex : 0; if (goalIndex <= inNodeIndex) return; var baseNodes = CheckingNodes.GetRange(inNodeIndex, goalIndex + 1 - inNodeIndex); var baseZones = PathPlanner.GetZones([..baseNodes.Select(n => n.ToNodeDto())], Zones); var outZones = CurrentZones.Where(z => !baseZones.Any(bz => bz.Id == z.Id)).ToList(); foreach (var zoneACS in outZones) { if (string.IsNullOrEmpty(zoneACS.Name)) continue; var outTrafficACS = await TrafficACS.RequestOut(RobotModel.RobotId, zoneACS.Name); if (outTrafficACS.IsSuccess && outTrafficACS.Data) CurrentZones.RemoveAll(z => z.Id == zoneACS.Id); } } protected virtual async Task CheckingHandler() { try { if (NavigationPath is null || InNode is null) throw new Exception("Đường đi không tồn tại"); await GoOutTrafficACS(); if (!TrafficManager.Enable) { if (TrafficManagerGoalId != CheckingNodes[^1].Id) TrafficManagerGoalId = CheckingNodes[^1].Id; } else { TrafficManager.UpdateInNode(RobotModel.RobotId, InNode.Id); var trafficSolution = TrafficManager.GetTrafficNode(RobotModel.RobotId); if (trafficSolution.State != TrafficSolutionState.RefreshPath && TrafficSolutionState == TrafficSolutionState.RefreshPath && trafficSolution.Edges.Length > 0 && trafficSolution.Nodes.Length > 1) { HandleRefreshPath(trafficSolution); } if (trafficSolution.State == TrafficSolutionState.Complete || trafficSolution.State == TrafficSolutionState.Waitting) { HandleCompleteState(trafficSolution); } else if (trafficSolution.State == TrafficSolutionState.GiveWay && trafficSolution.GivewayNodes.Count > 1 && trafficSolution.GivewayEdges.Count > 0) { HandleGivewayState(trafficSolution); } var goal = CheckingNodes.FirstOrDefault(n => n.Id == trafficSolution.ReleaseNode.Id); if (goal is not null && goal.Id != TrafficManagerGoalId) TrafficManagerGoalId = goal.Id; TrafficSolutionState = trafficSolution.State; } if (!TrafficACS.Enable) { if (TrafficACSGoalId != CheckingNodes[^1].Id) TrafficACSGoalId = CheckingNodes[^1].Id; } else { var newZones = TrafficACS.GetZones(InNode?.Id ?? Guid.Empty, [.. CheckingNodes.Select(n => n.ToNodeDto())], Zones); var trafficACSrelaseNodeId = await RequestInACS(newZones); if (trafficACSrelaseNodeId != Guid.Empty && trafficACSrelaseNodeId != TrafficACSGoalId) TrafficACSGoalId = trafficACSrelaseNodeId; } UpdateTraffic(); } catch (Exception ex) { Logger.Warning($"{RobotModel.RobotId} Checking ex: {ex.Message}"); } } private NavigationPathEdge[] GetBasePath() { if (InNode is not null && BaseNodes is not null && BaseNodes.Count > 1) { var inNodeIndex = BaseNodes.FindIndex(n => n.Id == InNode.Id); if (inNodeIndex != -1 && inNodeIndex < BaseNodes.Count - 1) { var nodes = BaseNodes.GetRange(inNodeIndex, BaseNodes.Count - inNodeIndex); var edges = MapManager.GetEdges(MapId, [..nodes.Select(n => new NodeDto { Id = n.Id, })]); if (edges.Length == nodes.Count - 1 && nodes.Count > 1) { List pathEdges = []; var edge = MapManager.GetEdge(nodes[0].Id, nodes[1].Id, MapId); if (edge is null) { pathEdges.Add(new() { StartX = Visualization.X, StartY = Visualization.Y, EndX = nodes[1].X, EndY = nodes[1].Y, Degree = 1, }); } else { var splitStartPath = PathPlanner.PathSplit([new NodeDto { Id = nodes[0].Id, X = nodes[0].X, Y = nodes[0].Y, }, new NodeDto { Id = nodes[1].Id, X = nodes[1].X, Y = nodes[1].Y, Direction = MapCompute.GetNodeDirection(nodes[0].Direction), }], [edge]); if (splitStartPath.IsSuccess && splitStartPath.Data is not null) { int index = 0; double minDistance = double.MaxValue; for (int i = 0; i < splitStartPath.Data.Length; i++) { var distance = Math.Sqrt(Math.Pow(Visualization.X - splitStartPath.Data[i].X, 2) + Math.Pow(Visualization.Y - splitStartPath.Data[i].Y, 2)); if (distance < minDistance) { minDistance = distance; index = i; } } for (int i = index; i < splitStartPath.Data.Length - 1; i++) { pathEdges.Add(new() { StartX = splitStartPath.Data[i].X, StartY = splitStartPath.Data[i].Y, EndX = splitStartPath.Data[i + 1].X, EndY = splitStartPath.Data[i + 1].Y, Degree = 1, }); } } } for (int i = 1; i < nodes.Count - 1; i++) { pathEdges.Add(new() { StartX = nodes[i].X, StartY = nodes[i].Y, EndX = nodes[i + 1].X, EndY = nodes[i + 1].Y, ControlPoint1X = edges[i].ControlPoint1X, ControlPoint1Y = edges[i].ControlPoint1Y, ControlPoint2X = edges[i].ControlPoint2X, ControlPoint2Y = edges[i].ControlPoint2Y, Degree = edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.One ? 1 : edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.Two ? 2 : 3, }); } return [.. pathEdges]; } } } return []; } private NavigationPathEdge[] GetFullPath() { if (InNode is not null && CheckingNodes is not null && CheckingNodes.Count > 1) { var inNodeIndex = CheckingNodes.FindIndex(n => n.Id == InNode.Id); if (inNodeIndex != -1 && inNodeIndex < CheckingNodes.Count - 1) { var nodes = CheckingNodes.GetRange(inNodeIndex, CheckingNodes.Count - inNodeIndex); var edges = MapManager.GetEdges(MapId, [..nodes.Select(n => new NodeDto { Id = n.Id, })]); if (edges.Length == nodes.Count - 1 && nodes.Count > 1) { List pathEdges = []; var edge = MapManager.GetEdge(nodes[0].Id, nodes[1].Id, MapId); if (edge is null) { pathEdges.Add(new() { StartX = Visualization.X, StartY = Visualization.Y, EndX = nodes[1].X, EndY = nodes[1].Y, Degree = 1, }); } else { var splitStartPath = PathPlanner.PathSplit([new NodeDto { Id = nodes[0].Id, X = nodes[0].X, Y = nodes[0].Y, }, new NodeDto { Id = nodes[1].Id, X = nodes[1].X, Y = nodes[1].Y, Direction = MapCompute.GetNodeDirection(nodes[0].Direction), }], [edge]); if (splitStartPath.IsSuccess && splitStartPath.Data is not null) { int index = 0; double minDistance = double.MaxValue; for (int i = 0; i < splitStartPath.Data.Length; i++) { var distance = Math.Sqrt(Math.Pow(Visualization.X - splitStartPath.Data[i].X, 2) + Math.Pow(Visualization.Y - splitStartPath.Data[i].Y, 2)); if (distance < minDistance) { minDistance = distance; index = i; } } for (int i = index; i < splitStartPath.Data.Length - 1; i++) { pathEdges.Add(new() { StartX = splitStartPath.Data[i].X, StartY = splitStartPath.Data[i].Y, EndX = splitStartPath.Data[i + 1].X, EndY = splitStartPath.Data[i + 1].Y, Degree = 1, }); } } } for (int i = 1; i < nodes.Count - 1; i++) { pathEdges.Add(new() { StartX = nodes[i].X, StartY = nodes[i].Y, EndX = nodes[i + 1].X, EndY = nodes[i + 1].Y, ControlPoint1X = edges[i].ControlPoint1X, ControlPoint1Y = edges[i].ControlPoint1Y, ControlPoint2X = edges[i].ControlPoint2X, ControlPoint2Y = edges[i].ControlPoint2Y, Degree = edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.One ? 1 : edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.Two ? 2 : 3, }); } return [.. pathEdges]; } } } return []; } public bool UpdateGoal(NavigationNode goal) { if (NavigationGoal is null || NavigationGoal.Id != goal.Id) { if (NavigationPath is not null && NavigationPath.Any(node => node.Id == goal.Id)) { NavigationGoal = goal; MovePurePursuit?.UpdateGoal(goal); return true; } return false; } return true; } private void ClearPath() { NavigationPath = null; NavigationGoal = null; InNode = null; CheckingNodes.Clear(); TrafficManager.DeleteAgent(RobotModel.RobotId); } public void Dispose() { CheckingStop(); NavStop(); ClearPath(); NavigationState = NavigationStateType.Ready; VelocityController.Stop(); IsCompleted = true; IsProcessing = false; GC.SuppressFinalize(this); } protected void NavStart() { navTimer = new(SampleTimeMilliseconds, NavigationHandler, Logger); navTimer.Start(); IsCompleted = false; IsCanceled = false; IsError = false; IsProcessing = true; } protected void NavStop() { navTimer?.Dispose(); } protected void CheckingStart() { checkingTimer = new(CheckingTimeMilliseconds, CheckingHandler, Logger); checkingTimer.Start(); } public void CreateComledted() { IsCompleted = true; IsCanceled = false; IsError = false; } protected void CheckingStop() { checkingTimer?.Dispose(); } }