|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -from typing import TYPE_CHECKING |
| 5 | +from typing import TYPE_CHECKING, NamedTuple |
6 | 6 |
|
| 7 | +import numpy as np |
7 | 8 | from array_api_compat import array_namespace |
8 | 9 | from scipy.spatial.transform import Rotation as R |
9 | 10 |
|
|
15 | 16 | from drone_models.utils.constants import Constants |
16 | 17 |
|
17 | 18 |
|
18 | | -def pos2attitude( |
| 19 | +def state2attitude( |
19 | 20 | pos: Array, |
20 | 21 | quat: Array, |
21 | 22 | vel: Array, |
22 | 23 | ang_vel: Array, |
23 | 24 | cmd: Array, |
24 | | - constants: Constants, |
25 | | - parameters: dict[str, Array | float], |
26 | | - dt: float = 1 / 500, |
27 | | - i_error: Array | None = None, |
| 25 | + ctrl_freq: float = 100, |
| 26 | + ctrl_errors: tuple[Array, ...] = (), |
| 27 | + ctrl_info: tuple[Array, ...] = (), |
| 28 | + *, |
| 29 | + mass: float, |
| 30 | + kp: Array, |
| 31 | + kd: Array, |
| 32 | + ki: Array, |
| 33 | + gravity_vec: Array, |
| 34 | + mass_thrust: float, |
| 35 | + i_range: Array, |
| 36 | + thrust_min: float, |
| 37 | + thrust_max: float, |
| 38 | + pwm_max: float, |
28 | 39 | ) -> tuple[Array, Array]: |
29 | 40 | """Compute the positional part of the mellinger controller. |
30 | 41 |
|
@@ -56,23 +67,19 @@ def pos2attitude( |
56 | 67 | setpoint_vel = cmd[..., 3:6] |
57 | 68 | setpoint_acc = cmd[..., 6:9] |
58 | 69 | setpoint_yaw = cmd[..., 9] |
| 70 | + dt = 1 / ctrl_freq |
59 | 71 | # setpointRPY_rates = cmd[..., 10:13] |
60 | 72 | # From firmware controller_mellinger |
61 | 73 | r_error = setpoint_pos - pos # l. 145 Position Error (ep) |
62 | 74 | v_error = setpoint_vel - vel # l. 148 Velocity Error (ev) |
63 | 75 | # l.151 ff Integral Error |
64 | | - if i_error is None: |
65 | | - i_error = xp.zeros_like(pos) |
66 | | - i_error = xp.clip(i_error + r_error * dt, -parameters["i_range"], parameters["i_range"]) |
| 76 | + i_error = xp.zeros_like(pos) if ctrl_errors == () else ctrl_errors[0] |
| 77 | + i_error = xp.clip(i_error + r_error * dt, -i_range, i_range) |
67 | 78 | # l. 161 Desired thrust [F_des] |
68 | 79 | # => only one case here, since setpoint is always in absolute mode |
69 | 80 | # Note: since we've defined the gravity in z direction, a "-" needs to be added |
70 | | - target_thrust = ( |
71 | | - parameters["mass"] * (setpoint_acc - constants.GRAVITY_VEC) |
72 | | - + parameters["kp"] * r_error |
73 | | - + parameters["kd"] * v_error |
74 | | - + parameters["ki"] * i_error |
75 | | - ) |
| 81 | + target_thrust = mass * (setpoint_acc - gravity_vec) + kp * r_error + kd * v_error + ki * i_error |
| 82 | + target_thrust = xp.clip(target_thrust, thrust_min * 4, thrust_max * 4) |
76 | 83 | # l. 178 Rate-controlled YAW is moving YAW angle setpoint |
77 | 84 | # => only one case here, since the setpoint is always in absolute mode |
78 | 85 | desiredYaw = setpoint_yaw |
@@ -102,9 +109,8 @@ def pos2attitude( |
102 | 109 | matrix = xp.stack((x_axis_desired, y_axis_desired, z_axis_desired), axis=-1) |
103 | 110 | command_RPY = R.from_matrix(matrix).as_euler("xyz", degrees=False) |
104 | 111 | # l. 283 |
105 | | - thrust = parameters["massThrust"] * current_thrust |
106 | 112 | # Transform thrust into N to keep uniform interface |
107 | | - thrust = pwm2force(thrust, constants.THRUST_MAX, constants.PWM_MAX) * 4 |
| 113 | + thrust = pwm2force(mass_thrust * current_thrust, thrust_max, pwm_max) * 4 |
108 | 114 | command_rpyt = xp.concat((command_RPY, thrust[..., None]), axis=-1) |
109 | 115 | return command_rpyt, i_error |
110 | 116 |
|
@@ -264,3 +270,41 @@ def pwm_clip(motor_pwm: Array, constants: Constants) -> Constants: |
264 | 270 | return xp.where( |
265 | 271 | xp.all(motor_pwm == 0), 0.0, xp.clip(motor_pwm, constants.PWM_MIN, constants.PWM_MAX) |
266 | 272 | ) |
| 273 | + |
| 274 | + |
| 275 | +class MellingerStateParams(NamedTuple): |
| 276 | + """Parameters for the Mellinger state controller.""" |
| 277 | + |
| 278 | + mass: float |
| 279 | + kp: Array |
| 280 | + kd: Array |
| 281 | + ki: Array |
| 282 | + gravity_vec: Array |
| 283 | + mass_thrust: float |
| 284 | + i_range: Array |
| 285 | + thrust_min: float |
| 286 | + thrust_max: float |
| 287 | + pwm_max: float |
| 288 | + |
| 289 | + @staticmethod |
| 290 | + def load(drone_model: str) -> MellingerStateParams: |
| 291 | + """Load the parameters from the config file.""" |
| 292 | + params = MellingerStateParams._load_fake_model() |
| 293 | + return MellingerStateParams(**params) |
| 294 | + |
| 295 | + @staticmethod |
| 296 | + def _load_fake_model() -> dict[str, Array | float]: |
| 297 | + """Load the parameters from the config file.""" |
| 298 | + # TODO: Remove this once we can load proper params |
| 299 | + return { |
| 300 | + "mass": 0.034, |
| 301 | + "mass_thrust": 132000 * 0.034 / 0.027, |
| 302 | + "kp": np.array([0.4, 0.4, 1.25]), |
| 303 | + "kd": np.array([0.2, 0.2, 0.4]), |
| 304 | + "ki": np.array([0.05, 0.05, 0.05]), |
| 305 | + "i_range": np.array([2.0, 2.0, 0.4]), |
| 306 | + "gravity_vec": np.array([0.0, 0.0, -9.81]), # TODO: Double-check if we want this |
| 307 | + "thrust_min": 0.02, |
| 308 | + "thrust_max": 0.1125, |
| 309 | + "pwm_max": 65535.0, |
| 310 | + } |
0 commit comments