• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pybricks / pybricks-micropython / 19016791193

02 Nov 2025 06:40PM UTC coverage: 57.167% (-2.6%) from 59.744%
19016791193

Pull #406

github

laurensvalk
bricks/virtualhub: Replace with embedded simulation.

Instead of using the newly introduced simhub alongside the virtualhub, we'll just replace the old one entirely now that it has reached feature parity. We can keep calling it the virtualhub.
Pull Request #406: New virtual hub for more effective debugging

41 of 48 new or added lines in 7 files covered. (85.42%)

414 existing lines in 53 files now uncovered.

4479 of 7835 relevant lines covered (57.17%)

17178392.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.15
/lib/pbio/src/drivebase.c
1
// SPDX-License-Identifier: MIT
2
// Copyright (c) 2018-2023 The Pybricks Authors
3

4
// SPDX-License-Identifier: BSD-3-Clause
5
// Copyright (c) 2020-2023 LEGO System A/S
6

7
#include <stdlib.h>
8

9
#include <pbdrv/clock.h>
10
#include <pbio/error.h>
11
#include <pbio/drivebase.h>
12
#include <pbio/int_math.h>
13
#include <pbio/imu.h>
14
#include <pbio/servo.h>
15

16
#if PBIO_CONFIG_NUM_DRIVEBASES > 0
17

18
// Drivebase objects
19
static pbio_drivebase_t drivebases[PBIO_CONFIG_NUM_DRIVEBASES];
20

21
/**
22
 * Gets the state of the drivebase update loop.
23
 *
24
 * This becomes true after a successful call to pbio_drivebase_get_drivebase and
25
 * becomes false when there is an error. Such as when the cable is unplugged.
26
 *
27
 * @param [in]  db          The drivebase instance
28
 * @return                  True if up and running, false if not.
29
 */
30
bool pbio_drivebase_update_loop_is_running(pbio_drivebase_t *db) {
234,199✔
31

32
    // Drivebase must have servos.
33
    if (!db->left || !db->right) {
234,199✔
34
        return false;
62,246✔
35
    }
36

37
    // Drivebase must be the parent of its two servos.
38
    if (!pbio_parent_equals(&db->left->parent, db) || !pbio_parent_equals(&db->right->parent, db)) {
171,953✔
39
        return false;
25✔
40
    }
41

42
    // Both servo update loops must be running, since we want to read the servo observer state.
43
    return pbio_servo_update_loop_is_running(db->left) && pbio_servo_update_loop_is_running(db->right);
171,928✔
44
}
45

46
/**
47
 * Sets the drivebase settings based on the left and right motor settings.
48
 *
49
 * Sets all settings except ctl_steps_per_app_step. This must be set after
50
 * calling this function.
51
 *
52
 * @param [out] s_distance  Settings of the distance controller.
53
 * @param [out] s_heading   Settings of the heading controller.
54
 * @param [in]  s_left      Settings of the left motor controller.
55
 * @param [in]  s_right     Settings of the right motor controller.
56
 */
57
static void drivebase_adopt_settings(pbio_control_settings_t *s_distance, pbio_control_settings_t *s_heading, const pbio_control_settings_t *s_left, const pbio_control_settings_t *s_right) {
6✔
58

59
    // Use minimum PID of both motors, to avoid overly aggressive control if
60
    // one of the two motors has much higher PID values. Then scale it such
61
    // that we use the reduced kp value not just for low errors, but always.
62
    int32_t pid_kp = pbio_int_math_min(s_left->pid_kp, s_right->pid_kp) *
6✔
63
        pbio_int_math_min(s_left->pid_kp_low_pct, s_right->pid_kp_low_pct) / 100;
6✔
64

65
    // Cap maximum speed at the most constrained of the two motors.
66
    int32_t actuation_max = pbio_int_math_min(s_left->actuation_max, s_right->actuation_max);
6✔
67
    int32_t speed_max = pbio_int_math_min(s_left->speed_max, s_right->speed_max);
6✔
68

69
    // For all settings, take the value of the least powerful motor to ensure
70
    // that the drivebase can meet the given specs.
71
    *s_distance = (pbio_control_settings_t) {
6✔
72
        // Must be set immediately after calling the current function.
73
        .ctl_steps_per_app_step = 0,
74
        .stall_speed_limit = pbio_int_math_min(s_left->stall_speed_limit, s_right->stall_speed_limit),
6✔
75
        .stall_time = pbio_int_math_min(s_left->stall_time, s_right->stall_time),
6✔
76
        .speed_max = speed_max,
77
        // The default speed is 40% of the maximum speed.
78
        .speed_default = speed_max * 10 / 25,
6✔
79
        .speed_tolerance = pbio_int_math_min(s_left->speed_tolerance, s_right->speed_tolerance),
6✔
80
        // To account for reduced kp, adjust the position tolerance so we
81
        // always apply enough proportional torque to keep moving near the end.
82
        .position_tolerance = pbio_control_settings_div_by_gain(actuation_max, pid_kp),
6✔
83
        // Make acceleration, deceleration a bit slower for smoother driving.
84
        .acceleration = pbio_int_math_min(s_left->acceleration, s_right->acceleration) * 3 / 4,
6✔
85
        .deceleration = pbio_int_math_min(s_left->deceleration, s_right->deceleration) * 3 / 4,
6✔
86
        .actuation_max = actuation_max,
87
        .pid_kp = pid_kp,
88
        // Dynamic kp reduction is disabled for drivebases. Instead, it uses
89
        // reduced kp across the board.
90
        .pid_kp_low_pct = 0,
91
        .pid_kp_low_error_threshold = 0,
92
        .pid_kp_low_speed_threshold = 0,
93
        // Integral control is not necessary since there is no constant external
94
        // force to overcome that wouldn't be done by proportional control.
95
        .pid_ki = 0,
96
        .pid_kd = pbio_int_math_min(s_left->pid_kd, s_right->pid_kd),
6✔
97
        .integral_deadzone = pbio_int_math_max(s_left->integral_deadzone, s_right->integral_deadzone),
6✔
98
        .integral_change_max = pbio_int_math_min(s_left->integral_change_max, s_right->integral_change_max),
6✔
99
        .smart_passive_hold_time = pbio_int_math_max(s_left->smart_passive_hold_time, s_right->smart_passive_hold_time),
6✔
100
    };
101

102
    // By default, heading control is the nearly same as distance control.
103
    *s_heading = *s_distance;
6✔
104

105
    // We make the default turn speed a bit slower. Given the typical wheel
106
    // diameter, the wheels are often quite close together, so this
107
    // compensates by setting it at 33% instead of 40%.
108
    s_heading->speed_default = s_heading->speed_max / 3;
6✔
109

110
    // Most users intuitively expect heading control to take priority. When
111
    // heading controller is completely saturated, this ensures that it "wins"
112
    // against the distance controller.
113
    s_heading->actuation_max = s_distance->actuation_max * 2;
6✔
114
}
6✔
115

116
/**
117
 * Get the physical and estimated state of a drivebase in units of control.
118
 *
119
 * @param [in]  db              The drivebase instance
120
 * @param [out] state_distance  Physical and estimated state of the distance based on motor angles.
121
 * @param [out] state_heading   Physical and estimated state of the heading based on motor angles.
122
 * @return                      Error code.
123
 */
124
static pbio_error_t pbio_drivebase_get_state_via_motors(pbio_drivebase_t *db, pbio_control_state_t *state_distance, pbio_control_state_t *state_heading) {
17,691✔
125

126
    // Get left servo state
127
    pbio_control_state_t state_left;
128
    pbio_error_t err = pbio_servo_get_state_control(db->left, &state_left);
17,691✔
129
    if (err != PBIO_SUCCESS) {
17,691✔
UNCOV
130
        return err;
×
131
    }
132

133
    // Get right servo state
134
    pbio_control_state_t state_right;
135
    err = pbio_servo_get_state_control(db->right, &state_right);
17,691✔
136
    if (err != PBIO_SUCCESS) {
17,691✔
UNCOV
137
        return err;
×
138
    }
139

140
    // Take average to get distance state
141
    pbio_angle_avg(&state_left.position, &state_right.position, &state_distance->position);
17,691✔
142
    pbio_angle_avg(&state_left.position_estimate, &state_right.position_estimate, &state_distance->position_estimate);
17,691✔
143
    state_distance->speed_estimate = (state_left.speed_estimate + state_right.speed_estimate) / 2;
17,691✔
144
    state_distance->speed = (state_left.speed + state_right.speed) / 2;
17,691✔
145

146
    // Take average difference to get heading state, which is implemented as:
147
    // (left - right) / 2 = (left + right) / 2 - right = avg - right.
148
    pbio_angle_diff(&state_distance->position, &state_right.position, &state_heading->position);
17,691✔
149
    pbio_angle_diff(&state_distance->position_estimate, &state_right.position_estimate, &state_heading->position_estimate);
17,691✔
150
    state_heading->speed_estimate = state_distance->speed_estimate - state_right.speed_estimate;
17,691✔
151
    state_heading->speed = state_distance->speed - state_right.speed;
17,691✔
152

153
    return PBIO_SUCCESS;
17,691✔
154
}
155

156
/**
157
 * Get the physical and estimated state of a drivebase in units of control.
158
 *
159
 * @param [in]  db              The drivebase instance
160
 * @param [out] state_distance  Physical and estimated state of the distance.
161
 * @param [out] state_heading   Physical and estimated state of the heading.
162
 * @return                      Error code.
163
 */
164
static pbio_error_t pbio_drivebase_get_state_control(pbio_drivebase_t *db, pbio_control_state_t *state_distance, pbio_control_state_t *state_heading) {
17,683✔
165

166
    // Gets the "measured" state according to the driver motors.
167
    pbio_error_t err = pbio_drivebase_get_state_via_motors(db, state_distance, state_heading);
17,683✔
168
    if (err != PBIO_SUCCESS) {
17,683✔
UNCOV
169
        return err;
×
170
    }
171

172
    // Subtract distance offset.
173
    pbio_angle_diff(&state_distance->position, &db->distance_offset, &state_distance->position);
17,683✔
174
    pbio_angle_diff(&state_distance->position_estimate, &db->distance_offset, &state_distance->position_estimate);
17,683✔
175

176
    // Subtract heading offset
177
    pbio_angle_diff(&state_heading->position, &db->heading_offset, &state_heading->position);
17,683✔
178
    pbio_angle_diff(&state_heading->position_estimate, &db->heading_offset, &state_heading->position_estimate);
17,683✔
179

180
    // Optionally use gyro to override the heading source for more accuracy.
181
    // The gyro manages its own offset, so we don't need to subtract it here.
182
    // Note that the heading speed *estimate* is not set here. This value, used
183
    // for derivative control, still uses the motor estimate rather than the
184
    // gyro speed, to guarantee the same stability properties to stabilize the
185
    // motors.
186
    if (db->gyro_heading_type != PBIO_IMU_HEADING_TYPE_NONE) {
17,683✔
UNCOV
187
        pbio_imu_get_heading_scaled(db->gyro_heading_type, &state_heading->position, &state_heading->speed, db->control_heading.settings.ctl_steps_per_app_step);
×
188
    }
189

190
    return PBIO_SUCCESS;
17,683✔
191
}
192

193
/**
194
 * Stop the drivebase from updating its controllers.
195
 *
196
 * This does not physically stop the motors if they are already moving.
197
 *
198
 * @param [in]  db              The drivebase instance
199
 */
200
static void pbio_drivebase_stop_drivebase_control(pbio_drivebase_t *db) {
26✔
201
    // Stop drivebase control so polling will stop
202
    pbio_control_stop(&db->control_distance);
26✔
203
    pbio_control_stop(&db->control_heading);
26✔
204
    db->control_paused = false;
26✔
205
}
26✔
206

207
/**
208
 * Stop the motors used by the drivebase from updating its controllers.
209
 *
210
 * This does not physically stop the motors if they are already moving.
211
 *
212
 * @param [in]  db              The drivebase instance
213
 */
214
static void pbio_drivebase_stop_servo_control(pbio_drivebase_t *db) {
44✔
215
    // Stop servo control so polling will stop
216
    pbio_control_stop(&db->left->control);
44✔
217
    pbio_control_stop(&db->right->control);
44✔
218
}
44✔
219

220
/**
221
 * Checks if both drive base controllers are active.
222
 *
223
 * @param [in]  drivebase       Pointer to this drivebase instance.
224
 * @return                      True if heading and distance control are active, else false.
225
 */
226
static bool pbio_drivebase_control_is_active(const pbio_drivebase_t *db) {
19,643✔
227
    return pbio_control_is_active(&db->control_distance) && pbio_control_is_active(&db->control_heading);
19,643✔
228
}
229

230
/**
231
 * Drivebase stop function that can be called from a servo.
232
 *
233
 * When a new command is issued to a servo, the servo calls this to stop the
234
 * drivebase controller and to stop the other motor physically.
235
 *
236
 * @param [in]  drivebase       Void pointer to this drivebase instance.
237
 * @param [in]  clear_parent    Unused. There is currently no higher
238
 *                              abstraction than a drivebase.
239
 * @return                      Error code.
240
 */
241
static pbio_error_t pbio_drivebase_stop_from_servo(void *drivebase, bool clear_parent) {
58✔
242

243
    // A drivebase has no parent, so clear_parent argument is not applicable.
244
    (void)clear_parent;
245

246
    // Specify pointer type.
247
    pbio_drivebase_t *db = drivebase;
58✔
248

249
    // If drive base control is not active, there is nothing we need to do.
250
    if (!pbio_drivebase_control_is_active(db)) {
58✔
251
        return PBIO_SUCCESS;
54✔
252
    }
253

254
    // Stop the drive base controller so the motors don't start moving again.
255
    pbio_drivebase_stop_drivebase_control(db);
4✔
256

257
    // Since we don't know which child called the parent to stop, we stop both
258
    // motors. We don't stop their parents to avoid escalating the stop calls
259
    // up the chain (and back here) once again.
260
    pbio_error_t err = pbio_dcmotor_coast(db->left->dcmotor);
4✔
261
    if (err != PBIO_SUCCESS) {
4✔
UNCOV
262
        return err;
×
263
    }
264
    return pbio_dcmotor_coast(db->right->dcmotor);
4✔
265
}
266

267
#define ROT_MDEG_OVER_PI (114592) // 360 000 / pi
268

269
/**
270
 * Gets and sets up drivebase instance from two servo instances.
271
 *
272
 * @param [out] db_address       Drivebase instance if available.
273
 * @param [in]  left             Left servo instance.
274
 * @param [in]  right            Right servo instance.
275
 * @param [in]  wheel_diameter   Wheel diameter in um.
276
 * @param [in]  axle_track       Distance between wheel-ground contact points in um.
277
 * @return                       Error code.
278
 */
279
pbio_error_t pbio_drivebase_get_drivebase(pbio_drivebase_t **db_address, pbio_servo_t *left, pbio_servo_t *right, int32_t wheel_diameter, int32_t axle_track) {
6✔
280

281
    // Can't build a drive base with just one motor.
282
    if (left == right) {
6✔
UNCOV
283
        return PBIO_ERROR_INVALID_ARG;
×
284
    }
285

286
    // Assert that both motors have the same gearing
287
    if (left->control.settings.ctl_steps_per_app_step != right->control.settings.ctl_steps_per_app_step) {
6✔
UNCOV
288
        return PBIO_ERROR_INVALID_ARG;
×
289
    }
290

291
    // Check if the servos already have parents.
292
    if (pbio_parent_exists(&left->parent) || pbio_parent_exists(&right->parent)) {
6✔
293
        // If a servo is already in use by a higher level
294
        // abstraction like a drivebase, we can't re-use it.
295
        return PBIO_ERROR_BUSY;
×
296
    }
297

298
    // Now we know that the servos are free, there must be an available
299
    // drivebase. We can just use the first one that isn't running.
300
    uint8_t index;
301
    for (index = 0; index < PBIO_CONFIG_NUM_DRIVEBASES; index++) {
6✔
302
        if (!pbio_drivebase_update_loop_is_running(&drivebases[index])) {
6✔
303
            break;
6✔
304
        }
305
    }
306
    // Verify result is in range.
307
    if (index == PBIO_CONFIG_NUM_DRIVEBASES) {
6✔
UNCOV
308
        return PBIO_ERROR_FAILED;
×
309
    }
310

311
    // So, this is the drivebase we'll use.
312
    pbio_drivebase_t *db = &drivebases[index];
6✔
313

314
    // Set return value.
315
    *db_address = db;
6✔
316

317
    // Attach servos
318
    db->left = left;
6✔
319
    db->right = right;
6✔
320

321
    // Set parents of both servos, so they can stop this drivebase.
322
    pbio_parent_set(&left->parent, db, pbio_drivebase_stop_from_servo);
6✔
323
    pbio_parent_set(&right->parent, db, pbio_drivebase_stop_from_servo);
6✔
324

325
    // Stop any existing drivebase controls
326
    pbio_control_reset(&db->control_distance);
6✔
327
    pbio_control_reset(&db->control_heading);
6✔
328
    db->control_paused = false;
6✔
329

330
    // Reset both motors to a passive state
331
    pbio_drivebase_stop_servo_control(db);
6✔
332
    pbio_error_t err = pbio_drivebase_stop(db, PBIO_CONTROL_ON_COMPLETION_COAST);
6✔
333
    if (err != PBIO_SUCCESS) {
6✔
UNCOV
334
        return err;
×
335
    }
336

337
    // Adopt settings as the average or sum of both servos, except scaling
338
    drivebase_adopt_settings(&db->control_distance.settings, &db->control_heading.settings, &left->control.settings, &right->control.settings);
6✔
339

340
    // Verify that the given dimensions are not too small or large to compute
341
    // a correct result for heading and distance control scale below.
342
    if (wheel_diameter < 1000 || axle_track < 1000 ||
6✔
343
        left->control.settings.ctl_steps_per_app_step > INT32_MAX / ROT_MDEG_OVER_PI ||
6✔
344
        left->control.settings.ctl_steps_per_app_step > INT32_MAX / axle_track
6✔
345
        ) {
UNCOV
346
        return PBIO_ERROR_INVALID_ARG;
×
347
    }
348

349
    // Average rotation of the motors for every 1 degree drivebase rotation.
350
    db->control_heading.settings.ctl_steps_per_app_step =
6✔
351
        left->control.settings.ctl_steps_per_app_step * axle_track / wheel_diameter;
6✔
352

353
    // Average rotation of the motors for every 1 mm forward.
354
    db->control_distance.settings.ctl_steps_per_app_step =
6✔
355
        left->control.settings.ctl_steps_per_app_step * ROT_MDEG_OVER_PI / wheel_diameter;
6✔
356

357

358
    // Verify that wheel diameter was not so large that scale is now zero.
359
    if (db->control_distance.settings.ctl_steps_per_app_step < 1 ||
6✔
360
        db->control_heading.settings.ctl_steps_per_app_step < 1) {
6✔
UNCOV
361
        return PBIO_ERROR_INVALID_ARG;
×
362
    }
363

364
    // Reset offsets so current distance and angle are 0. This relies on the
365
    // geometry, so it is done after the scaling is set.
366
    pbio_drivebase_reset(db, 0, 0);
6✔
367

368
    // By default, don't use gyro for steering control.
369
    db->gyro_heading_type = PBIO_IMU_HEADING_TYPE_NONE;
6✔
370

371
    return PBIO_SUCCESS;
6✔
372
}
373

374
/**
375
 * Makes the drivebase use gyro or motor rotation sensors for heading control.
376
 *
377
 * This function will stop the drivebase if it is running.
378
 *
379
 * @param [in]  db               Drivebase instance.
380
 * @param [in]  heading_type     Whether to use the gyro for heading control, and if so which type.
381
 * @return                       Error code.
382
 */
383
pbio_error_t pbio_drivebase_set_use_gyro(pbio_drivebase_t *db, pbio_imu_heading_type_t heading_type) {
×
384

385
    // No need to reset controls if already in correct mode.
386
    if (db->gyro_heading_type == heading_type) {
×
UNCOV
387
        return PBIO_SUCCESS;
×
388
    }
389

390
    // We stop so that new commands will reinitialize the state using the
391
    // newly selected input for heading control.
392
    pbio_error_t err = pbio_drivebase_stop(db, PBIO_CONTROL_ON_COMPLETION_COAST);
×
393
    if (err != PBIO_SUCCESS) {
×
UNCOV
394
        return err;
×
395
    }
396

397
    db->gyro_heading_type = heading_type;
×
398
    return PBIO_SUCCESS;
×
399
}
400

401
/**
402
 * Stops a drivebase.
403
 *
404
 * @param [in]  db               Drivebase instance.
405
 * @param [in]  on_completion    Which stop type to use.
406
 * @return                       Error code.
407
 */
408
pbio_error_t pbio_drivebase_stop(pbio_drivebase_t *db, pbio_control_on_completion_t on_completion) {
23✔
409

410
    // Don't allow new user command if update loop not registered.
411
    if (!pbio_drivebase_update_loop_is_running(db)) {
23✔
UNCOV
412
        return PBIO_ERROR_INVALID_OP;
×
413
    }
414

415
    // We're asked to stop, so continuing makes no sense.
416
    if (on_completion == PBIO_CONTROL_ON_COMPLETION_CONTINUE) {
23✔
UNCOV
417
        return PBIO_ERROR_INVALID_ARG;
×
418
    }
419

420
    // Holding is the same as traveling by 0 degrees.
421
    if (on_completion == PBIO_CONTROL_ON_COMPLETION_HOLD) {
23✔
422
        return pbio_drivebase_drive_straight(db, 0, on_completion);
1✔
423
    }
424

425
    // Stop control.
426
    pbio_drivebase_stop_drivebase_control(db);
22✔
427

428
    // Stop the servos and pass on requested stop type.
429
    pbio_error_t err = pbio_servo_stop(db->left, on_completion);
22✔
430
    if (err != PBIO_SUCCESS) {
22✔
UNCOV
431
        return err;
×
432
    }
433
    return pbio_servo_stop(db->right, on_completion);
22✔
434
}
435

436
/**
437
 * Checks if a drivebase has completed its maneuver.
438
 *
439
 * @param [in]  db          The drivebase instance
440
 * @return                  True if still moving to target, false if not.
441
 */
442
bool pbio_drivebase_is_done(const pbio_drivebase_t *db) {
167,025✔
443
    return pbio_control_is_done(&db->control_distance) && pbio_control_is_done(&db->control_heading);
167,025✔
444
}
445

446
/**
447
 * Updates one drivebase in the control loop.
448
 *
449
 * This reads the physical and estimated state, and updates the controller if
450
 * it is active.
451
 *
452
 * @param [in]  db          The drivebase instance
453
 * @return                  Error code.
454
 */
455
static pbio_error_t pbio_drivebase_update(pbio_drivebase_t *db) {
18,568✔
456

457
    // If passive, no need to update.
458
    if (!pbio_drivebase_control_is_active(db)) {
18,568✔
459
        return PBIO_SUCCESS;
944✔
460
    }
461

462
    // Get current time
463
    uint32_t time_now = pbio_control_get_time_ticks();
17,624✔
464

465
    // Get drive base state
466
    pbio_control_state_t state_distance;
467
    pbio_control_state_t state_heading;
468
    pbio_error_t err = pbio_drivebase_get_state_control(db, &state_distance, &state_heading);
17,624✔
469
    if (err != PBIO_SUCCESS) {
17,624✔
UNCOV
470
        return err;
×
471
    }
472

473
    // Get reference and torque signals for distance control.
474
    pbio_trajectory_reference_t ref_distance;
475
    int32_t distance_torque;
476
    pbio_dcmotor_actuation_t distance_actuation;
477
    bool distance_external_pause = db->control_paused;
17,624✔
478
    pbio_control_update(&db->control_distance, time_now, &state_distance, &ref_distance, &distance_actuation, &distance_torque, &distance_external_pause);
17,624✔
479

480
    // Get reference and torque signals for heading control.
481
    pbio_trajectory_reference_t ref_heading;
482
    int32_t heading_torque;
483
    pbio_dcmotor_actuation_t heading_actuation;
484
    bool heading_external_pause = db->control_paused;
17,624✔
485
    pbio_control_update(&db->control_heading, time_now, &state_heading, &ref_heading, &heading_actuation, &heading_torque, &heading_external_pause);
17,624✔
486

487
    // If either controller is paused, pause both.
488
    db->control_paused = distance_external_pause || heading_external_pause;
17,624✔
489

490
    // If either controller coasts, coast both, thereby also stopping control.
491
    if (distance_actuation == PBIO_DCMOTOR_ACTUATION_COAST ||
17,624✔
492
        heading_actuation == PBIO_DCMOTOR_ACTUATION_COAST) {
17,623✔
493
        return pbio_drivebase_stop(db, PBIO_CONTROL_ON_COMPLETION_COAST);
1✔
494
    }
495
    // If either controller brakes, brake both, thereby also stopping control.
496
    if (distance_actuation == PBIO_DCMOTOR_ACTUATION_BRAKE ||
17,623✔
497
        heading_actuation == PBIO_DCMOTOR_ACTUATION_BRAKE) {
17,623✔
498
        return pbio_drivebase_stop(db, PBIO_CONTROL_ON_COMPLETION_BRAKE);
×
499
    }
500

501
    // The only other expected actuation type is torque, so make sure it is.
502
    if (distance_actuation != PBIO_DCMOTOR_ACTUATION_TORQUE ||
17,623✔
503
        heading_actuation != PBIO_DCMOTOR_ACTUATION_TORQUE) {
17,623✔
UNCOV
504
        return PBIO_ERROR_FAILED;
×
505
    }
506

507
    // The left servo drives at a torque and speed of (average) + (difference).
508
    int32_t feed_forward_left = pbio_observer_get_feedforward_torque(
17,623✔
509
        db->left->observer.model,
17,623✔
510
        ref_distance.speed + ref_heading.speed, // left speed
17,623✔
511
        ref_distance.acceleration + ref_heading.acceleration); // left acceleration
17,623✔
512
    err = pbio_servo_actuate(db->left, PBIO_DCMOTOR_ACTUATION_TORQUE,
17,623✔
513
        distance_torque + heading_torque + feed_forward_left);
17,623✔
514
    if (err != PBIO_SUCCESS) {
17,623✔
UNCOV
515
        return err;
×
516
    }
517

518
    // The right servo drives at a torque and speed of (average) - (difference).
519
    int32_t feed_forward_right = pbio_observer_get_feedforward_torque(
17,623✔
520
        db->right->observer.model,
17,623✔
521
        ref_distance.speed - ref_heading.speed, // right speed
17,623✔
522
        ref_distance.acceleration - ref_heading.acceleration); // right acceleration
17,623✔
523
    return pbio_servo_actuate(db->right, PBIO_DCMOTOR_ACTUATION_TORQUE,
17,623✔
524
        distance_torque - heading_torque + feed_forward_right);
17,623✔
525
}
526

527
/**
528
 * Updates all currently active (previously set up) drivebases.
529
 */
530
void pbio_drivebase_update_all(void) {
26,944✔
531
    // Go through all drive base candidates
532
    for (uint8_t i = 0; i < PBIO_CONFIG_NUM_DRIVEBASES; i++) {
107,776✔
533

534
        pbio_drivebase_t *db = &drivebases[i];
80,832✔
535

536
        // If it's registered for updates, run its update loop
537
        if (pbio_drivebase_update_loop_is_running(db)) {
80,832✔
538
            pbio_drivebase_update(db);
18,568✔
539
        }
540
    }
541
}
26,944✔
542

543
/**
544
 * Starts the drivebase controllers to run by a given distance and angle.
545
 *
546
 * @param [in]  db              The drivebase instance.
547
 * @param [in]  distance        The distance to run by in mm.
548
 * @param [in]  drive_speed     The drive speed in mm/s.
549
 * @param [in]  angle           The angle to turn in deg.
550
 * @param [in]  turn_speed      The turn speed in deg/s.
551
 * @param [in]  on_completion   What to do when reaching the target.
552
 * @return                      Error code.
553
 */
554
static pbio_error_t pbio_drivebase_drive_relative(pbio_drivebase_t *db, int32_t distance, int32_t drive_speed, int32_t angle, int32_t turn_speed, pbio_control_on_completion_t on_completion) {
30✔
555

556
    // Don't allow new user command if update loop not registered.
557
    if (!pbio_drivebase_update_loop_is_running(db)) {
30✔
UNCOV
558
        return PBIO_ERROR_INVALID_OP;
×
559
    }
560

561
    // Stop servo control in case it was running.
562
    pbio_drivebase_stop_servo_control(db);
30✔
563

564
    // Get current time
565
    uint32_t time_now = pbio_control_get_time_ticks();
30✔
566

567
    // Get drive base state
568
    pbio_control_state_t state_distance;
569
    pbio_control_state_t state_heading;
570
    pbio_error_t err = pbio_drivebase_get_state_control(db, &state_distance, &state_heading);
30✔
571
    if (err != PBIO_SUCCESS) {
30✔
UNCOV
572
        return err;
×
573
    }
574

575
    // Start controller that controls the average angle of both motors.
576
    err = pbio_control_start_position_control_relative(&db->control_distance, time_now, &state_distance, distance, drive_speed, on_completion, false);
30✔
577
    if (err != PBIO_SUCCESS) {
30✔
UNCOV
578
        return err;
×
579
    }
580

581
    // Start controller that controls half the difference between both angles.
582
    err = pbio_control_start_position_control_relative(&db->control_heading, time_now, &state_heading, angle, turn_speed, on_completion, false);
30✔
583
    if (err != PBIO_SUCCESS) {
30✔
UNCOV
584
        return err;
×
585
    }
586

587
    // At this point, the two trajectories may have different durations, so they won't complete at the same time
588
    // To account for this, we re-compute the shortest trajectory to have the same duration as the longest.
589

590
    // First, find out which controller takes the lead
591
    const pbio_control_t *control_leader;
592
    pbio_control_t *control_follower;
593

594
    if (pbio_trajectory_get_duration(&db->control_distance.trajectory) >
60✔
595
        pbio_trajectory_get_duration(&db->control_heading.trajectory)) {
30✔
596
        // Distance control takes the longest, so it will take the lead
597
        control_leader = &db->control_distance;
19✔
598
        control_follower = &db->control_heading;
19✔
599
    } else {
600
        // Heading control takes the longest, so it will take the lead
601
        control_leader = &db->control_heading;
11✔
602
        control_follower = &db->control_distance;
11✔
603
    }
604

605
    // Revise follower trajectory so it takes as long as the leader, achieved
606
    // by picking a lower speed and accelerations that makes the times match.
607
    pbio_trajectory_stretch(&control_follower->trajectory, &control_leader->trajectory);
30✔
608

609
    return PBIO_SUCCESS;
30✔
610
}
611

612
/**
613
 * Starts the drivebase controllers to run by a given distance.
614
 *
615
 * This will use the default speed.
616
 *
617
 * @param [in]  db              The drivebase instance.
618
 * @param [in]  distance        The distance to run by in mm.
619
 * @param [in]  on_completion   What to do when reaching the target.
620
 * @return                      Error code.
621
 */
622
pbio_error_t pbio_drivebase_drive_straight(pbio_drivebase_t *db, int32_t distance, pbio_control_on_completion_t on_completion) {
14✔
623
    // Execute the common drive command at default speed (by passing 0 speed).
624
    return pbio_drivebase_drive_relative(db, distance, 0, 0, 0, on_completion);
14✔
625
}
626

627
/**
628
 * Starts the drivebase controllers to run by an arc of given radius and angle.
629
 *
630
 * curve() was originally used as a generalization of turn(), but is now
631
 * deprecated in favor of the arc methods, which have more practical arguments.
632
 *
633
 * This will use the default speed.
634
 *
635
 * @param [in]  db              The drivebase instance.
636
 * @param [in]  radius          Radius of the arc in mm.
637
 * @param [in]  angle           Angle in degrees.
638
 * @param [in]  on_completion   What to do when reaching the target.
639
 * @return                      Error code.
640
 */
641
pbio_error_t pbio_drivebase_drive_curve(pbio_drivebase_t *db, int32_t radius, int32_t angle, pbio_control_on_completion_t on_completion) {
16✔
642

643
    // The angle is signed by the radius so we can go both ways.
644
    int32_t arc_angle = radius < 0 ? -angle : angle;
16✔
645

646
    // Arc length is computed accordingly.
647
    int32_t arc_length = (10 * pbio_int_math_abs(angle) * radius) / 573;
16✔
648

649
    // Execute the common drive command at default speed (by passing 0 speed).
650
    return pbio_drivebase_drive_relative(db, arc_length, 0, arc_angle, 0, on_completion);
16✔
651
}
652

653
/**
654
 * Starts the drivebase controllers to run by an arc of given radius and angle.
655
 *
656
 * With a positive radius, the robot drives along a circle to its right.
657
 * With a negative radius, the robot drives along a circle to its left.
658
 *
659
 * A positive angle means driving forward along the circle, negative is reverse.
660
 *
661
 * This will use the default speed.
662
 *
663
 * @param [in]  db              The drivebase instance.
664
 * @param [in]  radius          Radius of the arc in mm.
665
 * @param [in]  angle           The angle to drive along the circle in degrees.
666
 * @param [in]  on_completion   What to do when reaching the target.
667
 * @return                      Error code.
668
 */
669
pbio_error_t pbio_drivebase_drive_arc_angle(pbio_drivebase_t *db, int32_t radius, int32_t angle, pbio_control_on_completion_t on_completion) {
×
670

671
    if (pbio_int_math_abs(radius) < 10) {
×
UNCOV
672
        return PBIO_ERROR_INVALID_ARG;
×
673
    }
674

675
    // Arc length is radius * angle, with the user angle parameter governing
676
    // the drive direction as positive forward.
677
    int32_t drive_distance = (10 * angle * pbio_int_math_abs(radius)) / 573;
×
678

679
    // The user angle is positive for going forward, no matter the radius sign.
680
    // The internal functions expect positive to mean clockwise for the robot.
681
    int32_t direction = (radius > 0) == (angle > 0) ? 1 : -1;
×
682
    int32_t drive_angle = pbio_int_math_abs(angle) * direction;
×
683

684
    // Execute the common drive command at default speed (by passing 0 speed).
685
    return pbio_drivebase_drive_relative(db, drive_distance, 0, drive_angle, 0, on_completion);
×
686
}
687

688
/**
689
 * Starts the drivebase controllers to run by an arc of given radius and arc length.
690
 *
691
 * With a positive radius, the robot drives along a circle to its right.
692
 * With a negative radius, the robot drives along a circle to its left.
693
 *
694
 * A positive distance means driving forward along the circle, negative is reverse.
695
 *
696
 * This will use the default speed.
697
 *
698
 * @param [in]  db              The drivebase instance.
699
 * @param [in]  radius          Radius of the arc in mm.
700
 * @param [in]  distance        The distance to drive (arc length) in mm.
701
 * @param [in]  on_completion   What to do when reaching the target.
702
 * @return                      Error code.
703
 */
704
pbio_error_t pbio_drivebase_drive_arc_distance(pbio_drivebase_t *db, int32_t radius, int32_t distance, pbio_control_on_completion_t on_completion) {
×
705

706
    if (pbio_int_math_abs(radius) < 10) {
×
UNCOV
707
        return PBIO_ERROR_INVALID_ARG;
×
708
    }
709

710
    // The internal functions expect positive to mean clockwise for the robot
711
    // with respect to the ground, not in relation to any particular circle.
712
    int32_t angle = pbio_int_math_abs(distance) * 573 / pbio_int_math_abs(radius) / 10;
×
713
    if ((radius < 0) != (distance < 0)) {
×
714
        angle *= -1;
×
715
    }
716

717
    // Execute the common drive command at default speed (by passing 0 speed).
718
    return pbio_drivebase_drive_relative(db, distance, 0, angle, 0, on_completion);
×
719
}
720

721
/**
722
 * Starts the drivebase controllers to run for a given duration.
723
 *
724
 * @param [in]  db              The drivebase instance.
725
 * @param [in]  drive_speed     The drive speed in mm/s.
726
 * @param [in]  turn_speed      The turn speed in deg/s.
727
 * @param [in]  duration        The duration in ms.
728
 * @param [in]  on_completion   What to do when reaching the target.
729
 * @return                      Error code.
730
 */
731
static pbio_error_t pbio_drivebase_drive_time_common(pbio_drivebase_t *db, int32_t drive_speed, int32_t turn_speed, uint32_t duration, pbio_control_on_completion_t on_completion) {
8✔
732

733
    // Don't allow new user command if update loop not registered.
734
    if (!pbio_drivebase_update_loop_is_running(db)) {
8✔
UNCOV
735
        return PBIO_ERROR_INVALID_OP;
×
736
    }
737

738
    // Stop servo control in case it was running.
739
    pbio_drivebase_stop_servo_control(db);
8✔
740

741
    // Get current time
742
    uint32_t time_now = pbio_control_get_time_ticks();
8✔
743

744
    // Get drive base state
745
    pbio_control_state_t state_distance;
746
    pbio_control_state_t state_heading;
747
    pbio_error_t err = pbio_drivebase_get_state_control(db, &state_distance, &state_heading);
8✔
748
    if (err != PBIO_SUCCESS) {
8✔
UNCOV
749
        return err;
×
750
    }
751

752
    // Initialize both controllers
753
    err = pbio_control_start_timed_control(&db->control_distance, time_now, &state_distance, duration, drive_speed, on_completion);
8✔
754
    if (err != PBIO_SUCCESS) {
8✔
UNCOV
755
        return err;
×
756
    }
757

758
    err = pbio_control_start_timed_control(&db->control_heading, time_now, &state_heading, duration, turn_speed, on_completion);
8✔
759
    if (err != PBIO_SUCCESS) {
8✔
UNCOV
760
        return err;
×
761
    }
762

763
    return PBIO_SUCCESS;
8✔
764
}
765

766
/**
767
 * Starts the drivebase controllers to run forever.
768
 *
769
 * @param [in]  db              The drivebase instance.
770
 * @param [in]  speed           The drive speed in mm/s.
771
 * @param [in]  turn_rate       The turn rate in deg/s.
772
 * @return                      Error code.
773
 */
774
pbio_error_t pbio_drivebase_drive_forever(pbio_drivebase_t *db, int32_t speed, int32_t turn_rate) {
8✔
775
    return pbio_drivebase_drive_time_common(db, speed, turn_rate, PBIO_TRAJECTORY_DURATION_FOREVER_MS, PBIO_CONTROL_ON_COMPLETION_CONTINUE);
8✔
776
}
777

778
/**
779
 * Gets the drivebase state in user units.
780
 *
781
 * @param [in]  db          The drivebase instance.
782
 * @param [out] distance    Distance traveled in mm.
783
 * @param [out] drive_speed Current speed in mm/s.
784
 * @param [out] angle       Angle turned in degrees.
785
 * @param [out] turn_rate   Current turn rate in deg/s.
786
 * @return                  Error code.
787
 */
788
pbio_error_t pbio_drivebase_get_state_user(pbio_drivebase_t *db, int32_t *distance, int32_t *drive_speed, int32_t *angle, int32_t *turn_rate) {
19✔
789

790
    // Get drive base state
791
    pbio_control_state_t state_distance;
792
    pbio_control_state_t state_heading;
793
    pbio_error_t err = pbio_drivebase_get_state_control(db, &state_distance, &state_heading);
19✔
794
    if (err != PBIO_SUCCESS) {
19✔
UNCOV
795
        return err;
×
796
    }
797
    *distance = pbio_control_settings_ctl_to_app_long(&db->control_distance.settings, &state_distance.position);
19✔
798
    *drive_speed = pbio_control_settings_ctl_to_app(&db->control_distance.settings, state_distance.speed);
19✔
799
    *angle = pbio_control_settings_ctl_to_app_long(&db->control_heading.settings, &state_heading.position);
19✔
800
    *turn_rate = pbio_control_settings_ctl_to_app(&db->control_heading.settings, state_heading.speed);
19✔
801
    return PBIO_SUCCESS;
19✔
802
}
803

804
/**
805
 * Gets the drivebase angle in user units. This is the same as the angle
806
 * returned by ::pbio_drivebase_get_state_user, but as a floating point value.
807
 *
808
 * @param [in]  db          The drivebase instance.
809
 * @param [out] angle       Angle turned in degrees, floating point.
810
 * @return                  Error code.
811
 */
812
pbio_error_t pbio_drivebase_get_state_user_angle(pbio_drivebase_t *db, float *angle) {
2✔
813

814
    // Get drive base state
815
    pbio_control_state_t state_distance;
816
    pbio_control_state_t state_heading;
817
    pbio_error_t err = pbio_drivebase_get_state_control(db, &state_distance, &state_heading);
2✔
818
    if (err != PBIO_SUCCESS) {
2✔
UNCOV
819
        return err;
×
820
    }
821

822
    *angle = pbio_control_settings_ctl_to_app_long_float(&db->control_heading.settings, &state_heading.position);
2✔
823
    return PBIO_SUCCESS;
2✔
824
}
825

826
/**
827
 * Stops the drivebase and resets the accumulated drivebase state in user units.
828
 *
829
 * If the gyro is being used for control, it will be reset to the same angle.
830
 *
831
 * @param [in]  db          The drivebase instance.
832
 * @param [in] distance     Distance traveled in mm.
833
 * @param [in] angle        Angle turned in degrees.
834
 * @return                  Error code.
835
 */
836
pbio_error_t pbio_drivebase_reset(pbio_drivebase_t *db, int32_t distance, int32_t angle) {
8✔
837

838
    // Physically stops motors and stops the ongoing controllers, simplifying
839
    // the state reset since we won't need to restart ongoing motion.
840
    pbio_error_t err = pbio_drivebase_stop(db, PBIO_CONTROL_ON_COMPLETION_COAST);
8✔
841
    if (err != PBIO_SUCCESS) {
8✔
UNCOV
842
        return err;
×
843
    }
844

845
    // Get measured state according to motor encoders.
846
    pbio_control_state_t measured_distance;
847
    pbio_control_state_t measured_heading;
848
    err = pbio_drivebase_get_state_via_motors(db, &measured_distance, &measured_heading);
8✔
849
    if (err != PBIO_SUCCESS) {
8✔
UNCOV
850
        return err;
×
851
    }
852

853
    // We want to get: reported_new = measured - offset_new
854
    // So we can do:     offset_new = measured - reported_new
855
    pbio_angle_t reported_new;
856

857
    pbio_angle_from_low_res(&reported_new, distance, db->control_distance.settings.ctl_steps_per_app_step);
8✔
858
    pbio_angle_diff(&measured_distance.position, &reported_new, &db->distance_offset);
8✔
859

860
    pbio_angle_from_low_res(&reported_new, angle, db->control_heading.settings.ctl_steps_per_app_step);
8✔
861
    pbio_angle_diff(&measured_heading.position, &reported_new, &db->heading_offset);
8✔
862

863
    // Synchronize heading and drivebase angle state if gyro in use.
864
    if (db->gyro_heading_type != PBIO_IMU_HEADING_TYPE_NONE) {
8✔
UNCOV
865
        pbio_imu_set_heading(angle);
×
866
    }
867

868
    return PBIO_SUCCESS;
8✔
869
}
870

871
/**
872
 * Tests if any drive base is currently actively using the gyro.
873
 *
874
 * @return @c true if the gyro is being used, else @c false
875
 */
876
bool pbio_drivebase_any_uses_gyro(void) {
×
877
    for (uint8_t i = 0; i < PBIO_CONFIG_NUM_DRIVEBASES; i++) {
×
878
        pbio_drivebase_t *db = &drivebases[i];
×
879

880
        // Only consider activated drive bases that use the gyro.
881
        if (!pbio_drivebase_update_loop_is_running(db) || db->gyro_heading_type == PBIO_IMU_HEADING_TYPE_NONE) {
×
882
            continue;
×
883
        }
884

885
        // Active controller means driving or holding, so gyro is in use.
886
        if (pbio_control_is_active(&db->control_distance) || pbio_control_is_active(&db->control_heading)) {
×
887
            return true;
×
888
        }
889
    }
UNCOV
890
    return false;
×
891
}
892

893
/**
894
 * Gets the drivebase settings in user units.
895
 *
896
 * @param [in]  db                  Drivebase instance.
897
 * @param [out] drive_speed         Default linear speed in mm/s.
898
 * @param [out] drive_acceleration  Linear acceleration in mm/s^2.
899
 * @param [out] drive_deceleration  Linear deceleration in mm/s^2.
900
 * @param [out] turn_rate           Default turn rate in deg/s.
901
 * @param [out] turn_acceleration   Angular acceleration in deg/s^2.
902
 * @param [out] turn_deceleration   Angular deceleration in deg/s^2.
903
 */
904
pbio_error_t pbio_drivebase_get_drive_settings(const pbio_drivebase_t *db, int32_t *drive_speed, int32_t *drive_acceleration, int32_t *drive_deceleration, int32_t *turn_rate, int32_t *turn_acceleration, int32_t *turn_deceleration) {
2✔
905

906
    const pbio_control_settings_t *sd = &db->control_distance.settings;
2✔
907
    const pbio_control_settings_t *sh = &db->control_heading.settings;
2✔
908

909
    *drive_speed = pbio_control_settings_ctl_to_app(sd, sd->speed_default);
2✔
910
    *drive_acceleration = pbio_control_settings_ctl_to_app(sd, sd->acceleration);
2✔
911
    *drive_deceleration = pbio_control_settings_ctl_to_app(sd, sd->deceleration);
2✔
912
    *turn_rate = pbio_control_settings_ctl_to_app(sh, sh->speed_default);
2✔
913
    *turn_acceleration = pbio_control_settings_ctl_to_app(sh, sh->acceleration);
2✔
914
    *turn_deceleration = pbio_control_settings_ctl_to_app(sh, sh->deceleration);
2✔
915

916
    return PBIO_SUCCESS;
2✔
917
}
918

919
/**
920
 * Sets the drivebase settings in user units.
921
 *
922
 * @param [in]  db                  Drivebase instance.
923
 * @param [in] drive_speed          Default linear speed in mm/s.
924
 * @param [in] drive_acceleration   Linear acceleration in mm/s^2.
925
 * @param [in] drive_deceleration   Linear deceleration in mm/s^2.
926
 * @param [in] turn_rate            Default turn rate in deg/s.
927
 * @param [in] turn_acceleration    Angular acceleration in deg/s^2.
928
 * @param [in] turn_deceleration    Angular deceleration in deg/s^2.
929
 */
930
pbio_error_t pbio_drivebase_set_drive_settings(pbio_drivebase_t *db, int32_t drive_speed, int32_t drive_acceleration, int32_t drive_deceleration, int32_t turn_rate, int32_t turn_acceleration, int32_t turn_deceleration) {
3✔
931

932
    pbio_control_settings_t *sd = &db->control_distance.settings;
3✔
933
    pbio_control_settings_t *sh = &db->control_heading.settings;
3✔
934

935
    pbio_error_t err = pbio_trajectory_validate_speed_limit(sd->ctl_steps_per_app_step, drive_speed);
3✔
936
    if (err != PBIO_SUCCESS) {
3✔
UNCOV
937
        return err;
×
938
    }
939
    err = pbio_trajectory_validate_acceleration_limit(sd->ctl_steps_per_app_step, drive_acceleration);
3✔
940
    if (err != PBIO_SUCCESS) {
3✔
941
        return err;
1✔
942
    }
943
    err = pbio_trajectory_validate_acceleration_limit(sd->ctl_steps_per_app_step, drive_deceleration);
2✔
944
    if (err != PBIO_SUCCESS) {
2✔
UNCOV
945
        return err;
×
946
    }
947
    err = pbio_trajectory_validate_speed_limit(sh->ctl_steps_per_app_step, turn_rate);
2✔
948
    if (err != PBIO_SUCCESS) {
2✔
UNCOV
949
        return err;
×
950
    }
951
    err = pbio_trajectory_validate_acceleration_limit(sh->ctl_steps_per_app_step, turn_acceleration);
2✔
952
    if (err != PBIO_SUCCESS) {
2✔
UNCOV
953
        return err;
×
954
    }
955
    err = pbio_trajectory_validate_acceleration_limit(sh->ctl_steps_per_app_step, turn_deceleration);
2✔
956
    if (err != PBIO_SUCCESS) {
2✔
UNCOV
957
        return err;
×
958
    }
959

960
    sd->speed_default = pbio_int_math_clamp(pbio_control_settings_app_to_ctl(sd, drive_speed), sd->speed_max);
2✔
961
    sd->acceleration = pbio_control_settings_app_to_ctl(sd, drive_acceleration);
2✔
962
    sd->deceleration = pbio_control_settings_app_to_ctl(sd, drive_deceleration);
2✔
963
    sh->speed_default = pbio_int_math_clamp(pbio_control_settings_app_to_ctl(sh, turn_rate), sh->speed_max);
2✔
964
    sh->acceleration = pbio_control_settings_app_to_ctl(sh, turn_acceleration);
2✔
965
    sh->deceleration = pbio_control_settings_app_to_ctl(sh, turn_deceleration);
2✔
966

967
    return PBIO_SUCCESS;
2✔
968
}
969

970
/**
971
 * Checks whether drivebase is stalled. If the drivebase is actively
972
 * controlled, it is stalled when the controller(s) cannot maintain the
973
 * target speed or position while using maximum allowed torque. If control
974
 * is not active, it uses the individual servos to check for stall.
975
 *
976
 * @param [in]  db              The servo instance.
977
 * @param [out] stalled         True if stalled, false if not.
978
 * @param [out] stall_duration  For how long it has been stalled (ms).
979
 * @return                      Error code. ::PBIO_ERROR_INVALID_OP if update
980
 *                              loop not running, else ::PBIO_SUCCESS
981
 */
982
pbio_error_t pbio_drivebase_is_stalled(pbio_drivebase_t *db, bool *stalled, uint32_t *stall_duration) {
1,018✔
983

984
    // Don't allow access if update loop not registered.
985
    if (!pbio_drivebase_update_loop_is_running(db)) {
1,018✔
986
        *stalled = false;
1✔
987
        *stall_duration = 0;
1✔
988
        return PBIO_ERROR_INVALID_OP;
1✔
989
    }
990

991
    pbio_error_t err;
992

993
    // If drive base control is active, look at controller state.
994
    if (pbio_drivebase_control_is_active(db)) {
1,017✔
995
        uint32_t stall_duration_distance; // ticks, 0 on false
996
        uint32_t stall_duration_heading; // ticks, 0 on false
997
        bool stalled_heading = pbio_control_is_stalled(&db->control_heading, &stall_duration_heading);
1,013✔
998
        bool stalled_distance = pbio_control_is_stalled(&db->control_distance, &stall_duration_distance);
1,013✔
999

1000
        // We are stalled if any controller is stalled.
1001
        *stalled = stalled_heading || stalled_distance;
1,013✔
1002
        *stall_duration = pbio_control_time_ticks_to_ms(pbio_int_math_max(stall_duration_distance, stall_duration_heading));
1,013✔
1003
        return PBIO_SUCCESS;
1,013✔
1004
    }
1005

1006
    // Otherwise look at individual servos.
1007
    bool stalled_left;
1008
    bool stalled_right;
1009
    uint32_t stall_duration_left; // ms, 0 on false.
1010
    uint32_t stall_duration_right; // ms, 0 on false.
1011
    err = pbio_servo_is_stalled(db->left, &stalled_left, &stall_duration_left);
4✔
1012
    if (err != PBIO_SUCCESS) {
4✔
UNCOV
1013
        return err;
×
1014
    }
1015
    err = pbio_servo_is_stalled(db->right, &stalled_right, &stall_duration_right);
4✔
1016
    if (err != PBIO_SUCCESS) {
4✔
UNCOV
1017
        return err;
×
1018
    }
1019
    // We are stalled if at least one motor is stalled.
1020
    *stalled = stalled_left || stalled_right;
4✔
1021
    *stall_duration = pbio_int_math_max(stall_duration_left, stall_duration_right);
4✔
1022
    return PBIO_SUCCESS;
4✔
1023
}
1024

1025
#if PBIO_CONFIG_DRIVEBASE_SPIKE
1026

1027
/**
1028
 * Gets spike drivebase instance from two servo instances.
1029
 *
1030
 * This and the following functions provide spike-like "tank-drive" controls.
1031
 * These will not use the pbio functionality for gearing or reversed
1032
 * orientation. Any scaling and flipping happens within the functions below.
1033
 *
1034
 * It is a drivebase, but without wheel size and axle track parameters.
1035
 * Instead, the internal conversions are such that the "distance" is expressed
1036
 * in motor degrees. Likewise, turning in place by N degrees means that both
1037
 * motors turn by that amount.
1038
 *
1039
 * @param [out] db_address       Drivebase instance if available.
1040
 * @param [in]  left             Left servo instance.
1041
 * @param [in]  right            Right servo instance.
1042
 * @return                       Error code.
1043
 */
1044
pbio_error_t pbio_drivebase_get_drivebase_spike(pbio_drivebase_t **db_address, pbio_servo_t *left, pbio_servo_t *right) {
1045
    pbio_error_t err = pbio_drivebase_get_drivebase(db_address, left, right, 1000, 1000);
1046

1047
    // The application input for spike bases is degrees per second average
1048
    // between both wheels, so in millidegrees this is x1000.
1049
    (*db_address)->control_heading.settings.ctl_steps_per_app_step = 1000;
1050
    (*db_address)->control_distance.settings.ctl_steps_per_app_step = 1000;
1051
    return err;
1052
}
1053

1054
/**
1055
 * Starts driving for a given duration, at the provided motor speeds.
1056
 *
1057
 * @param [in]  db              The drivebase instance.
1058
 * @param [in]  speed_left      Left motor speed in deg/s.
1059
 * @param [in]  speed_right     Right motor speed in deg/s.
1060
 * @param [in]  duration        The duration in ms.
1061
 * @param [in]  on_completion   What to do when reaching the target.
1062
 * @return                      Error code.
1063
 */
1064
pbio_error_t pbio_drivebase_spike_drive_time(pbio_drivebase_t *db, int32_t speed_left, int32_t speed_right, uint32_t duration, pbio_control_on_completion_t on_completion) {
1065
    // Flip left tank motor orientation.
1066
    speed_left = -speed_left;
1067

1068
    // Start driving forever with the given sum and dif rates.
1069
    int32_t drive_speed = (speed_left + speed_right) / 2;
1070
    int32_t turn_speed = (speed_left - speed_right) / 2;
1071
    return pbio_drivebase_drive_time_common(db, drive_speed, turn_speed, duration, on_completion);
1072
}
1073

1074
/**
1075
 * Starts driving indefinitely, at the provided motor speeds.
1076
 *
1077
 * @param [in]  db              The drivebase instance.
1078
 * @param [in]  speed_left      Left motor speed in deg/s.
1079
 * @param [in]  speed_right     Right motor speed in deg/s.
1080
 * @return                      Error code.
1081
 */
1082
pbio_error_t pbio_drivebase_spike_drive_forever(pbio_drivebase_t *db, int32_t speed_left, int32_t speed_right) {
1083
    // Same as driving for time, just without an endpoint.
1084
    return pbio_drivebase_spike_drive_time(db, speed_left, speed_right, PBIO_TRAJECTORY_DURATION_FOREVER_MS, PBIO_CONTROL_ON_COMPLETION_CONTINUE);
1085
}
1086

1087
/**
1088
 * Drive the motors by a given angle, at the provided motor speeds.
1089
 *
1090
 * Only the faster motor will travel by the given angle. The slower motor
1091
 * travels less, such that they still stop at the same time.
1092
 *
1093
 * @param [in]  db              The drivebase instance.
1094
 * @param [in]  speed_left      Left motor speed in deg/s.
1095
 * @param [in]  speed_right     Right motor speed in deg/s.
1096
 * @param [in]  angle           Angle (deg) that the fast motor should travel.
1097
 * @param [in]  on_completion   What to do when reaching the target.
1098
 * @return                      Error code.
1099
 */
1100
pbio_error_t pbio_drivebase_spike_drive_angle(pbio_drivebase_t *db, int32_t speed_left, int32_t speed_right, int32_t angle, pbio_control_on_completion_t on_completion) {
1101

1102
    // In the classic tank drive, we flip the left motor here instead of at the low level.
1103
    speed_left *= -1;
1104

1105
    // If both speeds are zero, we can't do anything meaningful, but most users
1106
    // find it confusing if we return an error. To make sure it won't block
1107
    // forever, we set the angle to zero instead, so we're "done" right away.
1108
    if (speed_left == 0 && speed_right == 0) {
1109
        return pbio_drivebase_drive_relative(db, 0, 0, 0, 0, on_completion);
1110
    }
1111

1112
    // Work out angles for each motor.
1113
    int32_t max_speed = pbio_int_math_max(pbio_int_math_abs(speed_left), pbio_int_math_abs(speed_right));
1114
    int32_t angle_left = max_speed == 0 ? 0 : angle * speed_left / max_speed;
1115
    int32_t angle_right = max_speed == 0 ? 0 : angle * speed_right / max_speed;
1116

1117
    // Work out the required total and difference angles to achieve this.
1118
    int32_t distance = (angle_left + angle_right) / 2;
1119
    int32_t turn_angle = (angle_left - angle_right) / 2;
1120
    int32_t speed = (pbio_int_math_abs(speed_left) + pbio_int_math_abs(speed_right)) / 2;
1121

1122
    // Execute the maneuver.
1123
    return pbio_drivebase_drive_relative(db, distance, speed, turn_angle, speed, on_completion);
1124
}
1125

1126
/**
1127
 * Converts a speed and a steering ratio into a separate left and right speed.
1128
 *
1129
 * The steering value must be in the range [-100, 100].
1130
 *
1131
 * @param [in]  speed         Overall speed (deg/s).
1132
 * @param [in]  steering      Steering ratio.
1133
 * @param [out] speed_left    Speed of the left motor (deg/s).
1134
 * @param [out] speed_right   Speed of the right motor (deg/s).
1135
 * @return                    Error code.
1136
 */
1137
pbio_error_t pbio_drivebase_spike_steering_to_tank(int32_t speed, int32_t steering, int32_t *speed_left, int32_t *speed_right) {
1138

1139
    // Steering must be bounded.
1140
    if (steering < -100 || steering > 100) {
1141
        return PBIO_ERROR_INVALID_ARG;
1142
    }
1143

1144
    // Initialize both at the given speed.
1145
    *speed_left = speed;
1146
    *speed_right = speed;
1147

1148
    // Depending on steering direction, one wheel moves slower.
1149
    *(steering > 0 ? speed_right : speed_left) = speed * (100 - 2 * pbio_int_math_abs(steering)) / 100;
1150
    return PBIO_SUCCESS;
1151
}
1152

1153
#endif // PBIO_CONFIG_DRIVEBASE_SPIKE
1154

1155
#endif // PBIO_CONFIG_NUM_DRIVEBASES > 0
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc