Rob / Application /Services /RobotService.cs
danylokhodus's picture
fix 2
a332db3
Raw
History Blame Contribute Delete
23.4 kB
using Application.Abstractions.Interfaces;
using Application.DTOs.RobotDTOs;
using Entities.Interfaces;
using Entities.Models;
using Microsoft.Extensions.Logging;
namespace Application.Services
{
public class RobotService : IRobotService
{
private readonly IRobotRepository _robotRepository;
private readonly IPasswordHasher _passwordHasher;
private readonly IOrderRepository _orderRepository;
private readonly INodeRepository _nodeRepository;
private readonly IRoutingService _routingService;
private readonly ILogger<RobotService> _logger;
public RobotService(
IRobotRepository robotRepository,
IPasswordHasher passwordHasher,
IOrderRepository orderRepository,
INodeRepository nodeRepository,
IRoutingService routingService,
ILogger<RobotService> logger)
{
_robotRepository = robotRepository;
_passwordHasher = passwordHasher;
_orderRepository = orderRepository;
_nodeRepository = nodeRepository;
_routingService = routingService;
_logger = logger;
}
public async Task<RobotResponseDTO> CreateRobotAsync(CreateRobotDTO robotDto)
{
if (string.IsNullOrWhiteSpace(robotDto.Name))
throw new ArgumentException("Robot name is required");
if (string.IsNullOrWhiteSpace(robotDto.Model))
throw new ArgumentException("Robot model is required");
// Validate CurrentNodeId if provided
if (robotDto.CurrentNodeId.HasValue)
{
var nodeExists = await _nodeRepository.ExistsAsync(robotDto.CurrentNodeId.Value);
if (!nodeExists)
throw new ArgumentException($"Node with ID {robotDto.CurrentNodeId.Value} does not exist");
}
var robot = new Robot
{
Name = robotDto.Name,
Model = robotDto.Model,
Type = robotDto.Type,
Status = RobotStatus.Charging,
BatteryLevel = 100,
CurrentNodeId = robotDto.CurrentNodeId
};
await _robotRepository.AddAsync(robot);
var created = await _robotRepository.GetByIdAsync(robot.Id);
return MapToResponseDTO(created!);
}
public async Task<RobotResponseDTO?> GetRobotByIdAsync(int robotId)
{
var robot = await _robotRepository.GetByIdAsync(robotId);
return robot == null ? null : MapToResponseDTO(robot);
}
public async Task<IEnumerable<RobotResponseDTO>> GetAllRobotsAsync()
{
var robots = await _robotRepository.GetAllAsync();
return robots.Select(MapToResponseDTO);
}
public async Task<IEnumerable<RobotResponseDTO>> GetByStatusAsync(RobotStatus status)
{
var robots = await _robotRepository.GetByStatusAsync(status);
return robots.Select(MapToResponseDTO);
}
public async Task<IEnumerable<RobotResponseDTO>> GetByTypeAsync(RobotType type)
{
var robots = await _robotRepository.GetByTypeAsync(type);
return robots.Select(MapToResponseDTO);
}
public async Task<IEnumerable<RobotResponseDTO>> GetAvailableRobotsAsync()
{
var robots = await _robotRepository.GetAvailableRobotsAsync();
return robots.Select(MapToResponseDTO);
}
public async Task<bool> UpdateRobotAsync(UpdateRobotDTO robotDto)
{
var robot = await _robotRepository.GetByIdAsync(robotDto.Id);
if (robot == null) return false;
if (string.IsNullOrWhiteSpace(robotDto.Name))
throw new ArgumentException("Robot name is required");
if (robotDto.BatteryLevel < 0 || robotDto.BatteryLevel > 100)
throw new ArgumentException("Battery level must be between 0 and 100");
// Validate CurrentNodeId if provided
if (robotDto.CurrentNodeId.HasValue)
{
var nodeExists = await _nodeRepository.ExistsAsync(robotDto.CurrentNodeId.Value);
if (!nodeExists)
throw new ArgumentException($"Node with ID {robotDto.CurrentNodeId.Value} does not exist");
}
robot.Name = robotDto.Name;
robot.Model = robotDto.Model;
robot.Type = robotDto.Type;
robot.Status = robotDto.Status;
robot.BatteryLevel = robotDto.BatteryLevel;
robot.CurrentNodeId = robotDto.CurrentNodeId;
await _robotRepository.UpdateAsync(robot);
return true;
}
public async Task<bool> DeleteRobotAsync(int robotId)
{
if (!await _robotRepository.ExistsAsync(robotId)) return false;
await _robotRepository.DeleteAsync(robotId);
return true;
}
public async Task<(bool Success, int? RobotId, string? ErrorMessage)> RegisterRobotAsync(RobotRegisterDTO registerDto)
{
// Validate and trim input
registerDto.Name = registerDto.Name?.Trim();
registerDto.Model = registerDto.Model?.Trim();
registerDto.SerialNumber = registerDto.SerialNumber?.Trim();
registerDto.AccessKey = registerDto.AccessKey?.Trim();
if (string.IsNullOrWhiteSpace(registerDto.Name))
return (false, null, "Robot name is required");
if (string.IsNullOrWhiteSpace(registerDto.Model))
return (false, null, "Robot model is required");
if (string.IsNullOrWhiteSpace(registerDto.SerialNumber))
return (false, null, "Serial number is required");
if (string.IsNullOrWhiteSpace(registerDto.AccessKey))
return (false, null, "Access key is required");
// Validate robot type
if (!Enum.TryParse<RobotType>(registerDto.Type, true, out var robotType))
return (false, null, "Invalid robot type. Must be 'GroundCourier' or 'Drone'");
// Check if serial number already exists
var existingRobot = await _robotRepository.GetBySerialNumberAsync(registerDto.SerialNumber);
if (existingRobot != null)
{
return (false, null, "Robot already registered");
}
// Validate battery characteristics
if (registerDto.BatteryCapacityJoules.HasValue && registerDto.BatteryCapacityJoules.Value <= 0)
return (false, null, "Battery capacity must be greater than 0");
if (registerDto.EnergyConsumptionPerMeterJoules.HasValue && registerDto.EnergyConsumptionPerMeterJoules.Value <= 0)
return (false, null, "Energy consumption must be greater than 0");
// Validate port range
if (registerDto.Port.HasValue && (registerDto.Port.Value < 1 || registerDto.Port.Value > 65535))
return (false, null, "Port must be between 1 and 65535");
// Validate CurrentNodeId if provided
if (registerDto.CurrentNodeId.HasValue)
{
var nodeExists = await _nodeRepository.ExistsAsync(registerDto.CurrentNodeId.Value);
if (!nodeExists)
return (false, null, $"Node with ID {registerDto.CurrentNodeId.Value} does not exist");
}
// Hash the access key
var accessKeyHash = _passwordHasher.Hash(registerDto.AccessKey);
// Create new robot entity
var robot = new Robot
{
Name = registerDto.Name,
Model = registerDto.Model,
Type = robotType,
Status = RobotStatus.Charging,
BatteryLevel = 100.0,
SerialNumber = registerDto.SerialNumber,
AccessKeyHash = accessKeyHash,
// Battery characteristics (use provided values or defaults)
BatteryCapacityJoules = registerDto.BatteryCapacityJoules ?? 360000, // Default 100Wh
EnergyConsumptionPerMeterJoules = registerDto.EnergyConsumptionPerMeterJoules ?? 36, // Default 36J/m (10km range)
HoverConsumptionPerSecondJoules = registerDto.HoverConsumptionPerSecondJoules ?? 100, // Default 100J/s
// IoT Connection
IpAddress = registerDto.IpAddress,
Port = registerDto.Port ?? 80, // Default HTTP port
// Initial location
CurrentNodeId = registerDto.CurrentNodeId
};
// Save to database
await _robotRepository.AddAsync(robot);
await _robotRepository.SaveChangesAsync();
return (true, robot.Id, null);
}
public async Task<(bool Success, int? RobotId, string? ErrorMessage)> AuthenticateRobotAsync(RobotLoginDTO loginDto)
{
// Validate and trim input
var serialNumber = loginDto.SerialNumber?.Trim();
var accessKey = loginDto.AccessKey?.Trim();
if (string.IsNullOrWhiteSpace(serialNumber))
return (false, null, "Serial number is required");
if (string.IsNullOrWhiteSpace(accessKey))
return (false, null, "Access key is required");
// Get robot by SerialNumber
var robot = await _robotRepository.GetBySerialNumberAsync(serialNumber);
if (robot == null)
{
_logger.LogWarning("Authentication failed: Robot with SerialNumber '{SerialNumber}' not found", serialNumber);
return (false, null, "Invalid serial number or access key");
}
_logger.LogInformation("Found robot {RobotId} for SerialNumber {SerialNumber}", robot.Id, robot.SerialNumber);
if (string.IsNullOrEmpty(robot.AccessKeyHash))
{
_logger.LogWarning("Authentication failed: AccessKeyHash is empty for Robot {RobotId}", robot.Id);
return (false, null, "Robot authentication not configured");
}
// Verify access key
var hashedAccessKey = _passwordHasher.Hash(accessKey);
if (robot.AccessKeyHash != hashedAccessKey)
{
_logger.LogWarning("Authentication failed: Invalid AccessKey for Robot {RobotId}. Provided key (trimmed): '{Key}'", robot.Id, accessKey);
return (false, null, "Invalid serial number or access key");
}
_logger.LogInformation("Authentication successful for Robot {RobotId}", robot.Id);
return (true, robot.Id, null);
}
public async Task<RobotResponseDTO?> UpdateRobotStatusAsync(int robotId, RobotStatusUpdateDTO statusUpdate)
{
var robot = await _robotRepository.GetByIdAsync(robotId);
if (robot == null)
{
return null;
}
// Validate and parse status
if (!Enum.TryParse<RobotStatus>(statusUpdate.Status, true, out var newStatus))
{
throw new ArgumentException("Invalid status. Must be: Idle, Delivering, Charging, or Maintenance");
}
// Validate CurrentNodeId if provided
if (statusUpdate.CurrentNodeId.HasValue)
{
var nodeExists = await _nodeRepository.ExistsAsync(statusUpdate.CurrentNodeId.Value);
if (!nodeExists)
throw new ArgumentException($"Node with ID {statusUpdate.CurrentNodeId.Value} does not exist");
}
// Validate TargetNodeId if provided
if (statusUpdate.TargetNodeId.HasValue)
{
var nodeExists = await _nodeRepository.ExistsAsync(statusUpdate.TargetNodeId.Value);
if (!nodeExists)
throw new ArgumentException($"Target node with ID {statusUpdate.TargetNodeId.Value} does not exist");
}
// Update robot status and location
robot.Status = newStatus;
robot.BatteryLevel = statusUpdate.BatteryLevel;
robot.CurrentNodeId = statusUpdate.CurrentNodeId;
robot.CurrentLatitude = statusUpdate.CurrentLatitude;
robot.CurrentLongitude = statusUpdate.CurrentLongitude;
robot.TargetNodeId = statusUpdate.TargetNodeId;
await _robotRepository.UpdateAsync(robot);
return MapToResponseDTO(robot);
}
// IoT Order Management
public async Task<List<OrderAssignmentDTO>> GetMyOrdersAsync(int robotId)
{
// Verify robot exists
var robot = await _robotRepository.GetByIdAsync(robotId);
if (robot == null)
{
throw new ArgumentException($"Robot with ID {robotId} not found");
}
// Get all orders assigned to this robot that are not completed or cancelled
var orders = robot.ActiveOrders?.Where(o =>
o.Status != OrderStatus.Delivered &&
o.Status != OrderStatus.Cancelled
).ToList() ?? new List<Order>();
var assignments = new List<OrderAssignmentDTO>();
foreach (var order in orders)
{
// Get nodes
var pickupNode = await _nodeRepository.GetByIdAsync(order.PickupNodeId);
var dropoffNode = await _nodeRepository.GetByIdAsync(order.DropoffNodeId);
if (pickupNode == null || dropoffNode == null)
continue;
// Determine robot's starting position
Node startNode = robot.CurrentNode;
if (startNode == null && robot.CurrentLatitude.HasValue && robot.CurrentLongitude.HasValue)
{
startNode = new Node
{
Name = $"Current position",
Latitude = robot.CurrentLatitude.Value,
Longitude = robot.CurrentLongitude.Value
};
}
if (startNode == null) continue;
// Calculate route using the new unified routing service
var (canComplete, routeSegments, totalDistance, batteryUsage, logs) = await _routingService.CalculateDroneRouteAsync(
robot,
startNode,
pickupNode,
dropoffNode,
order.Weight
);
if (!canComplete) continue;
// Map RouteSegmentDTO to RouteWaypointDTO for backward compatibility with existing IoT clients
var waypoints = routeSegments.Select(s => new RouteWaypointDTO
{
SequenceNumber = s.SegmentNumber,
Latitude = s.ToLatitude,
Longitude = s.ToLongitude,
Action = s.Action.ToLower(),
DistanceMeters = s.DistanceMeters
}).ToList();
assignments.Add(new OrderAssignmentDTO
{
OrderId = order.Id,
OrderName = order.Name,
Description = order.Description ?? string.Empty,
Weight = order.Weight,
PickupNodeId = pickupNode.Id,
PickupNodeName = pickupNode.Name,
PickupLatitude = pickupNode.Latitude,
PickupLongitude = pickupNode.Longitude,
DropoffNodeId = dropoffNode.Id,
DropoffNodeName = dropoffNode.Name,
DropoffLatitude = dropoffNode.Latitude,
DropoffLongitude = dropoffNode.Longitude,
Route = waypoints,
TotalDistanceMeters = totalDistance,
EstimatedBatteryUsagePercent = batteryUsage,
OrderStatus = order.Status.ToString(),
AssignedAt = order.CreatedAt
});
}
return assignments;
}
public async Task<AcceptOrderResponseDTO> AcceptOrderAsync(int robotId, int orderId)
{
// Verify robot exists
var robot = await _robotRepository.GetByIdAsync(robotId);
if (robot == null)
{
throw new ArgumentException($"Robot with ID {robotId} not found");
}
// Get order
var order = await _orderRepository.GetByIdAsync(orderId);
if (order == null)
{
throw new ArgumentException($"Order with ID {orderId} not found");
}
// Verify order is assigned to this robot
if (order.RobotId != robotId)
{
throw new InvalidOperationException($"This order is not assigned to this robot. Order.RobotId={order.RobotId}, Expected={robotId}");
}
// Verify order status
if (order.Status != OrderStatus.Pending && order.Status != OrderStatus.Processing)
{
throw new InvalidOperationException($"Cannot accept order with status {order.Status}");
}
// If order is already in Processing and robot is already Delivering, just return success
// This handles the case where the order was already assigned via AssignRobotToOrderAsync
bool alreadyAccepted = order.Status == OrderStatus.Processing && robot.Status == RobotStatus.Delivering;
// Update order status to Processing only if it was Pending
if (order.Status == OrderStatus.Pending)
{
order.Status = OrderStatus.Processing;
await _orderRepository.UpdateAsync(order);
}
// Update robot status to Delivering only if it's not already
if (robot.Status != RobotStatus.Delivering)
{
robot.Status = RobotStatus.Delivering;
await _robotRepository.UpdateAsync(robot);
}
return new AcceptOrderResponseDTO
{
Message = alreadyAccepted ? "Order already accepted and in progress" : "Order accepted successfully",
OrderId = orderId,
OrderStatus = order.Status.ToString(),
AcceptedAt = DateTime.UtcNow
};
}
public async Task<bool> UpdateOrderPhaseAsync(int robotId, int orderId, OrderPhaseUpdateDTO phaseUpdate)
{
// Verify robot exists
var robot = await _robotRepository.GetByIdAsync(robotId);
if (robot == null)
{
throw new ArgumentException($"Robot with ID {robotId} not found");
}
// Get order
var order = await _orderRepository.GetByIdAsync(orderId);
if (order == null)
{
throw new ArgumentException($"Order with ID {orderId} not found");
}
// Verify order is assigned to this robot
if (order.RobotId != robotId)
{
throw new InvalidOperationException("This order is not assigned to this robot");
}
// Update order status based on phase
switch (phaseUpdate.Phase.ToUpper())
{
case "FLIGHT_TO_PICKUP":
case "AT_PICKUP":
case "LOADING":
order.Status = OrderStatus.Processing;
break;
case "FLIGHT_TO_DROPOFF":
case "AT_DROPOFF":
case "UNLOADING":
order.Status = OrderStatus.EnRoute;
break;
case "PACKAGE_DELIVERED":
order.Status = OrderStatus.Delivered;
robot.Status = RobotStatus.Idle;
await _robotRepository.UpdateAsync(robot);
break;
case "FLIGHT_TO_CHARGING":
robot.Status = RobotStatus.Charging;
await _robotRepository.UpdateAsync(robot);
break;
default:
throw new ArgumentException($"Unknown phase: {phaseUpdate.Phase}");
}
await _orderRepository.UpdateAsync(order);
return true;
}
public async Task<RoutingTestResponseDTO> RoutingTestAsync(RoutingTestDTO testDto)
{
var robot = await _robotRepository.GetByIdAsync(testDto.RobotId);
if (robot == null) throw new ArgumentException("Robot not found");
var pickupNode = await _nodeRepository.GetByIdAsync(testDto.PickupNodeId);
var dropoffNode = await _nodeRepository.GetByIdAsync(testDto.DropoffNodeId);
if (pickupNode == null || dropoffNode == null)
throw new ArgumentException("Pickup or Dropoff node not found");
// Determine robot's starting position
Node startNode = robot.CurrentNode;
if (startNode == null && robot.CurrentLatitude.HasValue && robot.CurrentLongitude.HasValue)
{
startNode = new Node
{
Name = $"Current position",
Latitude = robot.CurrentLatitude.Value,
Longitude = robot.CurrentLongitude.Value
};
}
if (startNode == null)
throw new ArgumentException("Robot has no current position/node");
var (canComplete, routeSegments, totalDistance, batteryUsage, logs) = await _routingService.CalculateDroneRouteAsync(
robot,
startNode,
pickupNode,
dropoffNode,
testDto.PackageWeight
);
return new RoutingTestResponseDTO
{
Success = canComplete,
TotalDistanceMeters = totalDistance,
EstimatedBatteryUsagePercent = batteryUsage,
Logs = logs,
Route = routeSegments
};
}
private static RobotResponseDTO MapToResponseDTO(Robot robot)
{
return new RobotResponseDTO
{
Id = robot.Id,
Name = robot.Name,
Model = robot.Model,
SerialNumber = robot.SerialNumber,
Type = robot.Type,
TypeName = robot.Type.ToString(),
Status = robot.Status,
StatusName = robot.Status.ToString(),
BatteryLevel = robot.BatteryLevel,
BatteryCapacityJoules = robot.BatteryCapacityJoules,
EnergyConsumptionPerMeterJoules = robot.EnergyConsumptionPerMeterJoules,
HoverConsumptionPerSecondJoules = robot.HoverConsumptionPerSecondJoules,
CurrentNodeId = robot.CurrentNodeId,
CurrentNodeName = robot.CurrentNode?.Name,
ActiveOrdersCount = robot.ActiveOrders?.Count ?? 0
};
}
}
}