using Appccelerate.StateMachine;
using Appccelerate.StateMachine.Machine;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RobotNet10.CANOpen.CiA402.Enums;
using RobotNet10.RobotApp.Devices;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.Shared.Geometry;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace RobotNet10.RobotApp.Motion;
///
/// States cho DifferentialDrive state machine - tương tự CiA402 DriveState
///
public enum DifferentialDriveState
{
NotReadyToSwitchOn,
SwitchOnDisabled,
ReadyToSwitchOn,
SwitchedOn,
OperationEnabled,
QuickStopActive,
FaultReactionActive,
Fault
}
///
/// Triggers cho DifferentialDrive state machine - tương tự CiA402 Controlword commands
///
public enum DifferentialDriveTrigger
{
Shutdown, // Disable voltage
SwitchOn, // Switch on command
DisableVoltage, // Disable voltage command
QuickStop, // Quick stop command
EnableOperation, // Enable operation command
DisableOperation, // Disable operation command
FaultReset // Fault reset command
}
///
/// DifferentialDrive service - điều khiển robot di chuyển bằng 2 bánh xe độc lập
/// Implement InverseKinematics và OdometryEstimator
/// Sử dụng state machine để quản lý trạng thái
///
public class DifferentialDrive : IInverseKinematics, IOdometryEstimator, IHostedService, IDisposable
{
private readonly PassiveStateMachine _stateMachine;
private readonly DifferentialDriveConfiguration _config;
private readonly IDeviceProvider _deviceProvider;
private readonly ILogger _logger;
private readonly IPlcController? _plcController;
private readonly Lock _lock = new();
// Servo devices
private ICiA402Servo? _leftWheelServo;
private ICiA402Servo? _rightWheelServo;
// Odometry state
private Pose _currentPose = new();
private int _lastLeftWheelPosition;
private int _lastRightWheelPosition;
private bool _isFirstUpdate = true;
// Calculated parameters
private double _leftWheelRadius;
private double _rightWheelRadius;
public double _wheelbase; // Khoảng cách giữa 2 bánh xe
// Conversion factors
private double _leftWheelMetersPerCount; // Mét trên encoder count
private double _rightWheelMetersPerCount; // Mét trên encoder count
private DifferentialDriveState _currentState = DifferentialDriveState.SwitchOnDisabled;
private bool _disposed = false;
///
/// Gets the current state of the DifferentialDrive
///
public DifferentialDriveState State => _currentState;
///
/// Gets whether the drive is in OperationEnabled state (ready to accept velocity commands)
///
public bool IsOperationEnabled => _currentState == DifferentialDriveState.OperationEnabled;
///
/// Gets the current pose (odometry)
///
public Pose CurrentPose
{
get
{
lock (_lock)
{
return _currentPose;
}
}
}
public DifferentialDrive(
IConfiguration configuration,
IDeviceProvider deviceProvider,
ILogger logger,
IPlcController? plcController = null)
{
_deviceProvider = deviceProvider ?? throw new ArgumentNullException(nameof(deviceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_plcController = plcController;
// Load configuration
var configSection = configuration.GetSection("Motion:DifferentialDrive");
if (!configSection.Exists())
{
throw new InvalidOperationException("Configuration section 'Motion:DifferentialDrive' not found in appsettings.json");
}
_config = new DifferentialDriveConfiguration();
configSection.Bind(_config);
// Validate configuration
ValidateConfiguration();
// Calculate parameters
CalculateParameters();
// Build state machine
_stateMachine = BuildStateMachine();
_stateMachine.Start();
}
private void ValidateConfiguration()
{
if (string.IsNullOrWhiteSpace(_config.LeftWheel.DeviceId))
throw new InvalidOperationException("LeftWheel.DeviceId is required");
if (string.IsNullOrWhiteSpace(_config.RightWheel.DeviceId))
throw new InvalidOperationException("RightWheel.DeviceId is required");
if (_config.LeftWheel.WheelDiameter <= 0)
throw new InvalidOperationException("LeftWheel.WheelDiameter must be greater than 0");
if (_config.RightWheel.WheelDiameter <= 0)
throw new InvalidOperationException("RightWheel.WheelDiameter must be greater than 0");
if (_config.LeftWheel.PulsesPerRevolution <= 0)
throw new InvalidOperationException("LeftWheel.PulsesPerRevolution must be greater than 0");
if (_config.RightWheel.PulsesPerRevolution <= 0)
throw new InvalidOperationException("RightWheel.PulsesPerRevolution must be greater than 0");
}
private void CalculateParameters()
{
// Calculate wheel radius
_leftWheelRadius = _config.LeftWheel.WheelDiameter / 2.0;
_rightWheelRadius = _config.RightWheel.WheelDiameter / 2.0;
// Calculate meters per encoder count
var leftWheelCircumference = Math.PI * _config.LeftWheel.WheelDiameter;
var rightWheelCircumference = Math.PI * _config.RightWheel.WheelDiameter;
_leftWheelMetersPerCount = leftWheelCircumference / _config.LeftWheel.PulsesPerRevolution;
_rightWheelMetersPerCount = rightWheelCircumference / _config.RightWheel.PulsesPerRevolution;
// Extract wheel positions
var leftPos = _config.LeftWheel.Position.Position;
var rightPos = _config.RightWheel.Position.Position;
// Calculate wheelbase (distance between wheel contact points)
var dx = rightPos.X - leftPos.X;
var dy = rightPos.Y - leftPos.Y;
_wheelbase = Math.Sqrt(dx * dx + dy * dy);
// Console.WriteLine($"rightPos.Y: {rightPos.Y}, leftPos.Y: {leftPos.Y}");
_logger.LogInformation(
"DifferentialDrive parameters calculated: LeftRadius={LeftRadius}m, RightRadius={RightRadius}m, " +
"Wheelbase={Wheelbase}m, LeftY={LeftY}m, RightY={RightY}m",
_leftWheelRadius, _rightWheelRadius, _wheelbase, leftPos.Y, rightPos.Y);
}
private PassiveStateMachine BuildStateMachine()
{
var builder = new StateMachineDefinitionBuilder();
// SwitchOnDisabled state (initial state)
builder.In(DifferentialDriveState.SwitchOnDisabled)
.ExecuteOnEntry(() =>
{
lock (_lock)
{
_currentState = DifferentialDriveState.SwitchOnDisabled;
}
_logger.LogInformation("DifferentialDrive state: SwitchOnDisabled");
})
.On(DifferentialDriveTrigger.Shutdown)
.Goto(DifferentialDriveState.ReadyToSwitchOn)
.Execute(() =>
{
_ = Task.Run(InitializeAsync);
});
// NotReadyToSwitchOn state
builder.In(DifferentialDriveState.NotReadyToSwitchOn)
.ExecuteOnEntry(() =>
{
lock (_lock)
{
_currentState = DifferentialDriveState.NotReadyToSwitchOn;
}
_logger.LogWarning("DifferentialDrive state: NotReadyToSwitchOn");
})
.On(DifferentialDriveTrigger.DisableVoltage)
.Goto(DifferentialDriveState.SwitchOnDisabled);
// ReadyToSwitchOn state
builder.In(DifferentialDriveState.ReadyToSwitchOn)
.ExecuteOnEntry(() =>
{
lock (_lock)
{
_currentState = DifferentialDriveState.ReadyToSwitchOn;
}
_logger.LogInformation("DifferentialDrive state: ReadyToSwitchOn");
})
.On(DifferentialDriveTrigger.SwitchOn)
.Goto(DifferentialDriveState.SwitchedOn)
.Execute(() =>
{
_ = Task.Run(SwitchOnAsync);
})
.On(DifferentialDriveTrigger.DisableVoltage)
.Goto(DifferentialDriveState.SwitchOnDisabled)
.Execute(() =>
{
_ = Task.Run(DisableAsync);
});
// SwitchedOn state
builder.In(DifferentialDriveState.SwitchedOn)
.ExecuteOnEntry(() =>
{
lock (_lock)
{
_currentState = DifferentialDriveState.SwitchedOn;
}
_logger.LogInformation("DifferentialDrive state: SwitchedOn");
})
.On(DifferentialDriveTrigger.EnableOperation)
.Goto(DifferentialDriveState.OperationEnabled)
.Execute(() =>
{
_ = Task.Run(EnableOperationAsync);
})
.On(DifferentialDriveTrigger.DisableOperation)
.Goto(DifferentialDriveState.ReadyToSwitchOn)
.On(DifferentialDriveTrigger.QuickStop)
.Goto(DifferentialDriveState.QuickStopActive)
.Execute(() =>
{
_ = Task.Run(QuickStopAsync);
})
.On(DifferentialDriveTrigger.DisableVoltage)
.Goto(DifferentialDriveState.SwitchOnDisabled)
.Execute(() =>
{
_ = Task.Run(DisableAsync);
});
// OperationEnabled state
builder.In(DifferentialDriveState.OperationEnabled)
.ExecuteOnEntry(() =>
{
lock (_lock)
{
_currentState = DifferentialDriveState.OperationEnabled;
}
_logger.LogInformation("DifferentialDrive state: OperationEnabled");
})
.On(DifferentialDriveTrigger.DisableOperation)
.Goto(DifferentialDriveState.SwitchedOn)
.Execute(() =>
{
_ = Task.Run(DisableOperationAsync);
})
.On(DifferentialDriveTrigger.QuickStop)
.Goto(DifferentialDriveState.QuickStopActive)
.Execute(() =>
{
_ = Task.Run(QuickStopAsync);
})
.On(DifferentialDriveTrigger.DisableVoltage)
.Goto(DifferentialDriveState.SwitchOnDisabled)
.Execute(() =>
{
_ = Task.Run(DisableAsync);
});
// QuickStopActive state
builder.In(DifferentialDriveState.QuickStopActive)
.ExecuteOnEntry(() =>
{
lock (_lock)
{
_currentState = DifferentialDriveState.QuickStopActive;
}
_logger.LogWarning("DifferentialDrive state: QuickStopActive");
})
.On(DifferentialDriveTrigger.DisableVoltage)
.Goto(DifferentialDriveState.SwitchOnDisabled)
.Execute(() =>
{
_ = Task.Run(DisableAsync);
});
// FaultReactionActive state
builder.In(DifferentialDriveState.FaultReactionActive)
.ExecuteOnEntry(() =>
{
lock (_lock)
{
_currentState = DifferentialDriveState.FaultReactionActive;
}
_logger.LogError("DifferentialDrive state: FaultReactionActive");
})
.On(DifferentialDriveTrigger.FaultReset)
.Goto(DifferentialDriveState.SwitchOnDisabled)
.Execute(() =>
{
ResetBothServoFaults();
_logger.LogInformation("DifferentialDrive fault reset");
});
// Fault state
builder.In(DifferentialDriveState.Fault)
.ExecuteOnEntry(() =>
{
lock (_lock)
{
_currentState = DifferentialDriveState.Fault;
}
_logger.LogError("DifferentialDrive state: Fault");
})
.On(DifferentialDriveTrigger.FaultReset)
.Goto(DifferentialDriveState.SwitchOnDisabled)
.Execute(() =>
{
ResetBothServoFaults();
_logger.LogInformation("DifferentialDrive fault reset");
});
return builder
.WithInitialState(DifferentialDriveState.SwitchOnDisabled)
.Build()
.CreatePassiveStateMachine();
}
private async Task InitializeAsync()
{
try
{
// Get servo devices
var leftDevice = _deviceProvider.GetDevice(_config.LeftWheel.DeviceId);
var rightDevice = _deviceProvider.GetDevice(_config.RightWheel.DeviceId);
if (leftDevice is not ICiA402Servo leftServo)
{
_logger.LogError("Left wheel device '{DeviceId}' is not an ICiA402Servo", _config.LeftWheel.DeviceId);
_stateMachine.Fire(DifferentialDriveTrigger.FaultReset);
return;
}
if (rightDevice is not ICiA402Servo rightServo)
{
_logger.LogError("Right wheel device '{DeviceId}' is not an ICiA402Servo", _config.RightWheel.DeviceId);
_stateMachine.Fire(DifferentialDriveTrigger.FaultReset);
return;
}
_leftWheelServo = leftServo;
_rightWheelServo = rightServo;
// Subscribe to position changed events
_leftWheelServo.PositionChanged += OnLeftWheelPositionChanged;
_rightWheelServo.PositionChanged += OnRightWheelPositionChanged;
// Subscribe to statusword changed events to detect faults
_leftWheelServo.StatuswordChanged += OnLeftWheelStatuswordChanged;
_rightWheelServo.StatuswordChanged += OnRightWheelStatuswordChanged;
// Set operation mode to Profile Velocity
await _leftWheelServo.SetOperationModeAsync(OperationMode.ProfileVelocity);
await _rightWheelServo.SetOperationModeAsync(OperationMode.ProfileVelocity);
// Set profile velocity to 0 (đảm bảo động cơ không quay)
await _leftWheelServo.SetProfileVelocityAsync(0);
await _rightWheelServo.SetProfileVelocityAsync(0);
// Set target velocity to 0 (đảm bảo động cơ không quay)
await _leftWheelServo.SetTargetVelocityAsync(0);
await _rightWheelServo.SetTargetVelocityAsync(0);
// Get initial positions
_lastLeftWheelPosition = _leftWheelServo.CachedPosition;
_lastRightWheelPosition = _rightWheelServo.CachedPosition;
_logger.LogInformation("DifferentialDrive initialized successfully - servos ready, velocities set to 0");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize DifferentialDrive");
_stateMachine.Fire(DifferentialDriveTrigger.FaultReset);
}
}
private async Task DisableAsync()
{
try
{
// Stop both wheels
if (_leftWheelServo != null)
{
await _leftWheelServo.SetTargetVelocityAsync(0);
await _leftWheelServo.DisableAsync();
_leftWheelServo.PositionChanged -= OnLeftWheelPositionChanged;
_leftWheelServo.StatuswordChanged -= OnLeftWheelStatuswordChanged;
}
if (_rightWheelServo != null)
{
await _rightWheelServo.SetTargetVelocityAsync(0);
await _rightWheelServo.DisableAsync();
_rightWheelServo.PositionChanged -= OnRightWheelPositionChanged;
_rightWheelServo.StatuswordChanged -= OnRightWheelStatuswordChanged;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disabling DifferentialDrive");
}
}
private async Task SwitchOnAsync()
{
try
{
// Switch on both servos (enable voltage, switch on)
if (_leftWheelServo != null)
{
await _leftWheelServo.SwitchOnAsync();
}
if (_rightWheelServo != null)
{
await _rightWheelServo.SwitchOnAsync();
}
_logger.LogInformation("DifferentialDrive switched on");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error switching on DifferentialDrive");
}
}
private async Task EnableOperationAsync()
{
try
{
// Enable operation on both servos
if (_leftWheelServo != null)
{
await _leftWheelServo.EnableOperationAsync();
}
if (_rightWheelServo != null)
{
await _rightWheelServo.EnableOperationAsync();
}
_logger.LogInformation("DifferentialDrive operation enabled");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error enabling operation");
}
}
private async Task QuickStopAsync()
{
try
{
// Stop both wheels immediately
if (_leftWheelServo != null)
{
await _leftWheelServo.SetTargetVelocityAsync(0);
await _leftWheelServo.QuickStopAsync();
}
if (_rightWheelServo != null)
{
await _rightWheelServo.SetTargetVelocityAsync(0);
await _rightWheelServo.QuickStopAsync();
}
_logger.LogWarning("DifferentialDrive quick stop executed");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing quick stop");
}
}
private async Task DisableOperationAsync()
{
try
{
// Stop both wheels
if (_leftWheelServo != null)
{
await _leftWheelServo.SetTargetVelocityAsync(0);
await _leftWheelServo.DisableOperationAsync();
}
if (_rightWheelServo != null)
{
await _rightWheelServo.SetTargetVelocityAsync(0);
await _rightWheelServo.DisableOperationAsync();
}
_logger.LogInformation("DifferentialDrive operation disabled");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disabling operation");
}
}
/// Reset fault trên cả hai servo CiA402 (trái/phải) trước khi chuyển drive state — gọi khi FaultReset trigger.
private void ResetBothServoFaults()
{
try
{
var left = _leftWheelServo?.FaultResetAsync(CancellationToken.None);
var right = _rightWheelServo?.FaultResetAsync(CancellationToken.None);
if (left != null) left.GetAwaiter().GetResult();
if (right != null) right.GetAwaiter().GetResult();
_logger.LogInformation("DifferentialDrive: both wheel servos fault reset sent");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "DifferentialDrive: error resetting servo faults");
}
}
private void OnLeftWheelPositionChanged(object? sender, PositionChangedEventArgs e)
{
// Update odometry when either wheel position changes
UpdateOdometry();
}
private void OnRightWheelPositionChanged(object? sender, PositionChangedEventArgs e)
{
// Update odometry when either wheel position changes
UpdateOdometry();
}
private void OnLeftWheelStatuswordChanged(object? sender, StatuswordChangedEventArgs e)
{
if (e.NewState == DriveState.Fault || e.NewState == DriveState.FaultReactionActive)
{
_logger.LogError("Left wheel servo entered fault state: {State}", e.NewState);
_stateMachine.Fire(DifferentialDriveTrigger.FaultReset);
}
}
private void OnRightWheelStatuswordChanged(object? sender, StatuswordChangedEventArgs e)
{
if (e.NewState == DriveState.Fault || e.NewState == DriveState.FaultReactionActive)
{
_logger.LogError("Right wheel servo entered fault state: {State}", e.NewState);
_stateMachine.Fire(DifferentialDriveTrigger.FaultReset);
}
}
#region IInverseKinematics Implementation
///
/// Tính toán vận tốc các bánh xe từ vận tốc robot (Twist)
/// Sử dụng position thực tế của bánh xe (không phụ thuộc vào tên "left/right")
///
/// Công thức kinematics tổng quát cho differential drive:
/// 1. Tính vận tốc tại contact point: v_contact = v_robot + ω × r_wheel
/// 2. Bánh xe roll perpendicular to position vector (tangent of circle)
/// 3. Project v_contact lên roll direction
/// 4. Apply IsReversed nếu motor đấu ngược
/// Roll direction = perpendicular to position vector (for differential drive)
///
public (int leftWheelVelocity, int rightWheelVelocity) CalculateWheelVelocities(Twist twist)
{
// Robot velocity
var vx = twist.Linear.X;
var vy = twist.Linear.Y;
var omega = -twist.Angular.Z;
// var wheelbase = _wheelbase;
// Console.WriteLine($"vx: {vx}, vy: {vy}, omega: {omega}");
// Console.WriteLine($"wheelbase: {_wheelbase}");
// Wheel positions
var leftPos = _config.LeftWheel.Position.Position;
var rightPos = _config.RightWheel.Position.Position;
// Bước 1: Tính vận tốc tại contact point của mỗi bánh xe
// v_contact = v_robot + ω × r
// Cross product trong 2D: ω × (x, y) = (-ω×y, ω×x)
var leftContactVx = vx + omega * leftPos.Y;
var leftContactVy = vy - omega * leftPos.X;
var rightContactVx = vx + omega * rightPos.Y;
var rightContactVy = vy - omega * rightPos.X;
// Bước 2: Roll direction của bánh xe trong differential drive
// Roll direction = perpendicular to position vector (tangent of circle)
// Với position = (x, y), perpendicular = (-y, x) normalized
// Nhưng cho differential drive đơn giản với wheels at (0, ±Y):
// Position vector = (0, Y) → perpendicular = (-Y, 0) normalized = (±1, 0)
// → Tất cả bánh đều roll theo X direction!
// Roll direction cho differential drive: luôn theo X direction (forward của robot)
var leftRollDirX = -1.0;
var leftRollDirY = 0.0;
var rightRollDirX = -1.0;
var rightRollDirY = 0.0;
// Bước 3: Project v_contact lên roll direction (dot product)
// v_wheel = v_contact · roll_direction
var leftWheelVelocity = leftContactVx * leftRollDirX + leftContactVy * leftRollDirY;
var rightWheelVelocity = rightContactVx * rightRollDirX + rightContactVy * rightRollDirY;
// Console.WriteLine($"leftwheelVelocity (m/s): {leftWheelVelocity}, rightWheelVelocity (m/s): {rightWheelVelocity}");
// Bước 4: Apply reversed flag if motor/encoder wired backwards
if (_config.LeftWheel.IsReversed)
leftWheelVelocity = -leftWheelVelocity;
if (_config.RightWheel.IsReversed)
rightWheelVelocity = -rightWheelVelocity;
// Bước 5: Chuyển đổi từ m/s sang encoder counts/s
var leftWheelVelocityCounts = (int)(leftWheelVelocity / _leftWheelMetersPerCount);
var rightWheelVelocityCounts = (int)(rightWheelVelocity / _rightWheelMetersPerCount);
return (leftWheelVelocityCounts, rightWheelVelocityCounts);
}
///
/// Đặt vận tốc robot (Twist) - sẽ tự động tính toán và điều khiển các bánh xe
///
public async Task SetVelocityAsync(Twist twist, CancellationToken ct = default)
{
if (_currentState != DifferentialDriveState.OperationEnabled)
{
if (_currentState == DifferentialDriveState.QuickStopActive ||
_currentState == DifferentialDriveState.SwitchedOn)
{
_logger.LogWarning("DifferentialDrive not in OperationEnabled state ({State}), attempting recovery...", _currentState);
FaultReset();
await Task.Delay(200, ct);
Enable();
await Task.Delay(300, ct);
if (_currentState != DifferentialDriveState.OperationEnabled)
{
_logger.LogError("Recovery failed: DifferentialDrive not operational. State: {State}", _currentState);
return;
}
}
else
{
_logger.LogWarning("Cannot set velocity: DifferentialDrive state is {State}", _currentState);
return;
}
}
var (leftVel, rightVel) = CalculateWheelVelocities(twist);
try
{
if (_leftWheelServo != null)
{
await _leftWheelServo.SetTargetVelocityAsync(leftVel, ct);
}
if (_rightWheelServo != null)
{
await _rightWheelServo.SetTargetVelocityAsync(rightVel, ct);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting wheel velocities");
_stateMachine.Fire(DifferentialDriveTrigger.FaultReset);
}
// Truyền hướng di chuyển xuống PLC: tiến M931, lùi M932, không đi thì cả hai off
var vx = twist.Linear.X;
const double zeroThreshold = 0.01;
_plcController?.SetDirectionForwardBackward(vx > zeroThreshold, vx < -zeroThreshold);
}
#endregion
#region IOdometryEstimator Implementation
///
/// Reset odometry về vị trí ban đầu (0, 0, 0)
///
public void Reset()
{
Reset(new Pose());
}
///
/// Reset odometry về một pose cụ thể
///
public void Reset(Pose pose)
{
lock (_lock)
{
_currentPose = pose;
_isFirstUpdate = true;
if (_leftWheelServo != null)
{
_lastLeftWheelPosition = _leftWheelServo.CachedPosition;
}
if (_rightWheelServo != null)
{
_lastRightWheelPosition = _rightWheelServo.CachedPosition;
}
_logger.LogInformation("Odometry reset to pose: X={X}, Y={Y}, Z={Z}", pose.Position.X, pose.Position.Y, pose.Position.Z);
}
}
///
/// Cập nhật odometry từ vị trí encoder của các bánh xe
///
public void Update(int leftWheelPosition, int rightWheelPosition)
{
lock (_lock)
{
if (_isFirstUpdate)
{
_lastLeftWheelPosition = leftWheelPosition;
_lastRightWheelPosition = rightWheelPosition;
_isFirstUpdate = false;
return;
}
// Tính delta position (encoder counts)
var deltaLeft = leftWheelPosition - _lastLeftWheelPosition;
var deltaRight = rightWheelPosition - _lastRightWheelPosition;
// Apply reversed flag (nếu motor đấu ngược, encoder đếm ngược)
if (_config.LeftWheel.IsReversed)
deltaLeft = -deltaLeft;
if (_config.RightWheel.IsReversed)
deltaRight = -deltaRight;
// Chuyển đổi sang mét
var deltaLeftMeters = deltaLeft * _leftWheelMetersPerCount;
var deltaRightMeters = deltaRight * _rightWheelMetersPerCount;
// Tính toán vận tốc trung bình và góc quay
var averageVelocity = (deltaLeftMeters + deltaRightMeters) / 2.0;
var deltaTheta = (deltaRightMeters - deltaLeftMeters) / _wheelbase;
// Cập nhật pose
// Lấy orientation hiện tại từ quaternion
var currentTheta = QuaternionToYaw(_currentPose.Orientation);
// Tính toán vị trí mới
var newX = _currentPose.Position.X + averageVelocity * Math.Cos(currentTheta);
var newY = _currentPose.Position.Y + averageVelocity * Math.Sin(currentTheta);
var newTheta = currentTheta + deltaTheta;
// Cập nhật pose
_currentPose = new Pose
{
Position = new Point(newX, newY, _currentPose.Position.Z),
Orientation = YawToQuaternion(newTheta)
};
// Cập nhật last positions
_lastLeftWheelPosition = leftWheelPosition;
_lastRightWheelPosition = rightWheelPosition;
}
}
private void UpdateOdometry()
{
if (_leftWheelServo == null || _rightWheelServo == null)
return;
Update(_leftWheelServo.CachedPosition, _rightWheelServo.CachedPosition);
}
///
/// Chuyển đổi quaternion sang yaw angle (radians)
///
private static double QuaternionToYaw(Quaternion q)
{
// Yaw = atan2(2*(w*z + x*y), 1 - 2*(y^2 + z^2))
var sinYaw = 2.0 * (q.W * q.Z + q.X * q.Y);
var cosYaw = 1.0 - 2.0 * (q.Y * q.Y + q.Z * q.Z);
return Math.Atan2(sinYaw, cosYaw);
}
///
/// Chuyển đổi yaw angle (radians) sang quaternion
///
private static Quaternion YawToQuaternion(double yaw)
{
var halfYaw = yaw / 2.0;
return new Quaternion(0, 0, Math.Sin(halfYaw), Math.Cos(halfYaw));
}
///
/// Set profile acceleration cho drive system (chuyển đổi từ m/s² sang encoder counts/s²)
///
/// Gia tốc tăng tốc (m/s²)
/// Cancellation token
public async Task SetAccelerationAsync(double acceleration, CancellationToken ct = default)
{
try
{
// Chuyển đổi từ m/s² sang encoder counts/s²
// acceleration_counts = acceleration_ms2 / metersPerCount
var leftAccelerationCounts = (uint)Math.Abs(acceleration / _leftWheelMetersPerCount);
var rightAccelerationCounts = (uint)Math.Abs(acceleration / _rightWheelMetersPerCount);
if (_leftWheelServo != null)
{
await _leftWheelServo.SetProfileAccelerationAsync(leftAccelerationCounts, ct);
}
if (_rightWheelServo != null)
{
await _rightWheelServo.SetProfileAccelerationAsync(rightAccelerationCounts, ct);
}
_logger.LogInformation("DifferentialDrive acceleration set to: {Acceleration} m/s² (Left: {LeftCounts}, Right: {RightCounts} counts/s²)",
acceleration, leftAccelerationCounts, rightAccelerationCounts);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting acceleration to {Acceleration} m/s²", acceleration);
throw;
}
}
///
/// Set profile deceleration cho drive system (chuyển đổi từ m/s² sang encoder counts/s²)
///
/// Gia tốc giảm tốc (m/s²)
/// Cancellation token
public async Task SetDecelerationAsync(double deceleration, CancellationToken ct = default)
{
try
{
// Chuyển đổi từ m/s² sang encoder counts/s²
var leftDecelerationCounts = (uint)Math.Abs(deceleration / _leftWheelMetersPerCount);
var rightDecelerationCounts = (uint)Math.Abs(deceleration / _rightWheelMetersPerCount);
if (_leftWheelServo != null)
{
await _leftWheelServo.SetProfileDecelerationAsync(leftDecelerationCounts, ct);
}
if (_rightWheelServo != null)
{
await _rightWheelServo.SetProfileDecelerationAsync(rightDecelerationCounts, ct);
}
_logger.LogInformation("DifferentialDrive deceleration set to: {Deceleration} m/s² (Left: {LeftCounts}, Right: {RightCounts} counts/s²)",
deceleration, leftDecelerationCounts, rightDecelerationCounts);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting deceleration to {Deceleration} m/s²", deceleration);
throw;
}
}
#endregion
#region Public Control Methods
///
/// Shutdown command - transition from SwitchOnDisabled to ReadyToSwitchOn
///
public void Shutdown()
{
_stateMachine.Fire(DifferentialDriveTrigger.Shutdown);
}
///
/// Switch on command - transition from ReadyToSwitchOn to SwitchedOn
///
public void SwitchOn()
{
_stateMachine.Fire(DifferentialDriveTrigger.SwitchOn);
}
///
/// Enable operation command - transition from SwitchedOn to OperationEnabled
///
public void EnableOperation()
{
_stateMachine.Fire(DifferentialDriveTrigger.EnableOperation);
}
///
/// Disable operation command - transition from OperationEnabled to SwitchedOn
///
public void DisableOperation()
{
_stateMachine.Fire(DifferentialDriveTrigger.DisableOperation);
}
///
/// Quick stop command - transition to QuickStopActive
///
public void QuickStop()
{
_stateMachine.Fire(DifferentialDriveTrigger.QuickStop);
}
///
/// Disable voltage command - transition to SwitchOnDisabled
///
public void DisableVoltage()
{
_stateMachine.Fire(DifferentialDriveTrigger.DisableVoltage);
}
///
/// Fault reset command - luôn gửi fault reset xuống 2 servo, rồi chuyển state machine (nếu đang Fault/FaultReactionActive).
///
public void FaultReset()
{
ResetBothServoFaults();
_stateMachine.Fire(DifferentialDriveTrigger.FaultReset);
}
///
/// Enable DifferentialDrive - convenience method to go through full sequence
///
public void Enable()
{
// Auto-transition through states if needed
if (_currentState == DifferentialDriveState.SwitchOnDisabled)
{
Shutdown();
}
else if (_currentState == DifferentialDriveState.ReadyToSwitchOn)
{
SwitchOn();
}
else if (_currentState == DifferentialDriveState.SwitchedOn)
{
EnableOperation();
}
}
///
/// Enable 2 động cơ giống enable bằng tay trên device: gửi lệnh trực tiếp xuống từng servo, await từng bước.
///
public async Task EnableAsync(CancellationToken cancellationToken = default)
{
if (_leftWheelServo == null || _rightWheelServo == null)
{
_logger.LogWarning("DifferentialDrive.EnableAsync: servo chưa có, bỏ qua");
return;
}
try
{
await _leftWheelServo.SetOperationModeAsync(OperationMode.ProfileVelocity, cancellationToken);
await _rightWheelServo.SetOperationModeAsync(OperationMode.ProfileVelocity, cancellationToken);
await _leftWheelServo.SetProfileVelocityAsync(0, cancellationToken);
await _rightWheelServo.SetProfileVelocityAsync(0, cancellationToken);
await _leftWheelServo.SetTargetVelocityAsync(0, cancellationToken);
await _rightWheelServo.SetTargetVelocityAsync(0, cancellationToken);
await _leftWheelServo.SwitchOnAsync(cancellationToken);
await _rightWheelServo.SwitchOnAsync(cancellationToken);
await _leftWheelServo.EnableOperationAsync(cancellationToken);
await _rightWheelServo.EnableOperationAsync(cancellationToken);
lock (_lock)
{
_currentState = DifferentialDriveState.OperationEnabled;
}
// Đồng bộ state machine với trạng thái thực để device/điều khiển hoạt động đúng
_stateMachine.Fire(DifferentialDriveTrigger.Shutdown);
_stateMachine.Fire(DifferentialDriveTrigger.SwitchOn);
_stateMachine.Fire(DifferentialDriveTrigger.EnableOperation);
_logger.LogInformation("DifferentialDrive: 2 động cơ đã enable (EnableAsync), state machine đã sync");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "DifferentialDrive.EnableAsync: lỗi khi enable động cơ");
}
}
///
/// Disable DifferentialDrive - convenience method
///
public void Disable()
{
if (_currentState == DifferentialDriveState.OperationEnabled)
{
DisableOperation();
}
else if (_currentState == DifferentialDriveState.SwitchedOn ||
_currentState == DifferentialDriveState.ReadyToSwitchOn)
{
DisableVoltage();
}
}
///
/// Set operation mode cho cả hai wheel servos
///
/// Operation mode to set
/// Cancellation token
public async Task SetOperationModeAsync(OperationMode mode, CancellationToken ct = default)
{
try
{
if (_leftWheelServo != null)
{
await _leftWheelServo.SetOperationModeAsync(mode, ct);
}
if (_rightWheelServo != null)
{
await _rightWheelServo.SetOperationModeAsync(mode, ct);
}
_logger.LogInformation("DifferentialDrive operation mode set to: {Mode}", mode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting operation mode to {Mode}", mode);
throw;
}
}
///
/// Get current operation mode của left wheel servo (giả sử cả 2 bánh có cùng mode)
///
/// Cancellation token
public async Task GetOperationModeAsync(CancellationToken ct = default)
{
try
{
if (_leftWheelServo != null)
{
return await _leftWheelServo.GetOperationModeAsync(ct);
}
_logger.LogWarning("Cannot get operation mode: Left wheel servo not available");
return OperationMode.NoMode;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting operation mode");
return OperationMode.NoMode;
}
}
#endregion
#region IHostedService
///
/// Start DifferentialDrive service — init chạy nền để không chặn Kestrel/web UI lúc startup.
///
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting DifferentialDrive service (non-blocking)...");
_ = Task.Run(() => InitializeWhenDevicesReadyAsync(cancellationToken), cancellationToken);
return Task.CompletedTask;
}
private async Task InitializeWhenDevicesReadyAsync(CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Waiting for all devices to be connected...");
var connected = await _deviceProvider.WaitForDevicesConnectedAsync(TimeSpan.FromMinutes(5), cancellationToken);
if (!connected)
{
_logger.LogWarning("Timeout waiting for devices to connect. DifferentialDrive will not be initialized.");
return;
}
_logger.LogInformation("All devices connected. Initializing DifferentialDrive...");
Shutdown();
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("DifferentialDrive initialization cancelled");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting DifferentialDrive service");
}
}
///
/// Stop DifferentialDrive service
///
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping DifferentialDrive service...");
try
{
await DisableOperationAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error stopping DifferentialDrive service");
}
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
try
{
Disable();
_stateMachine.Stop();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disposing DifferentialDrive");
}
}
#endregion
}