• 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

88.04
/lib/pbio/src/control.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
#include <assert.h>
9

10
#include <pbdrv/clock.h>
11

12
#include <pbio/config.h>
13
#include <pbio/control.h>
14
#include <pbio/int_math.h>
15
#include <pbio/trajectory.h>
16
#include <pbio/integrator.h>
17
#include <pbio/util.h>
18

19
/**
20
 * Gets the wall time in control unit time ticks (1e-4 seconds).
21
 *
22
 * @return                    Wall time in control ticks.
23
 */
24
uint32_t pbio_control_get_time_ticks(void) {
71,428✔
25
    _Static_assert(PBIO_TRAJECTORY_TICKS_PER_MS == 10, "time config must match clock resolution");
26
    return pbdrv_clock_get_100us();
71,428✔
27
}
28

29
/**
30
 * Checks if on completion type is active (control keeps going on completion).
31
 *
32
 * @param [in] on_completion  What to do on completion.
33
 * @return                    True if the completion type is active , else false.
34
 */
35
static bool pbio_control_on_completion_is_active(pbio_control_on_completion_t on_completion) {
16,277✔
36
    return on_completion == PBIO_CONTROL_ON_COMPLETION_HOLD ||
16,277✔
37
           on_completion == PBIO_CONTROL_ON_COMPLETION_CONTINUE;
38
}
39

40
/**
41
 * Checks if on completion type is passive with smart mode (target position
42
 * will be preserved as starting point for next relative angle maneuver).
43
 *
44
 * @param [in] on_completion  What to do on completion.
45
 * @return                    True if the completion type is passive with smart mode.
46
 */
47
static bool pbio_control_on_completion_is_passive_smart(pbio_control_on_completion_t on_completion) {
63✔
48
    return on_completion == PBIO_CONTROL_ON_COMPLETION_COAST_SMART ||
63✔
49
           on_completion == PBIO_CONTROL_ON_COMPLETION_BRAKE_SMART;
50
}
51

52
/**
53
 * Converts passive on-completion type to passive actuation type.
54
 *
55
 * @param [in] on_completion  What to do on completion.
56
 * @return                    Matching passive actuation type.
57
 */
58
pbio_dcmotor_actuation_t pbio_control_passive_completion_to_actuation_type(pbio_control_on_completion_t on_completion) {
60✔
59

60
    assert(!pbio_control_on_completion_is_active(on_completion));
60✔
61

62
    if (on_completion == PBIO_CONTROL_ON_COMPLETION_COAST_SMART || on_completion == PBIO_CONTROL_ON_COMPLETION_COAST) {
60✔
63
        return PBIO_DCMOTOR_ACTUATION_COAST;
60✔
64
    }
65
    // Brake and smart brake are the only remaining allowed completion options,
66
    // so always return the matching actuation mode as brake.
UNCOV
67
    return PBIO_DCMOTOR_ACTUATION_BRAKE;
×
68
}
69

70
/**
71
 * Discards smart flag from on completion type.
72
 *
73
 * @param [in] on_completion  What to do on completion.
74
 * @return                    What to do on completion, discarding smart option.
75
 */
76
static pbio_control_on_completion_t pbio_control_on_completion_discard_smart(pbio_control_on_completion_t on_completion) {
25✔
77
    if (on_completion == PBIO_CONTROL_ON_COMPLETION_COAST_SMART) {
25✔
UNCOV
78
        return PBIO_CONTROL_ON_COMPLETION_COAST;
×
79
    }
80
    if (on_completion == PBIO_CONTROL_ON_COMPLETION_BRAKE_SMART) {
25✔
81
        return PBIO_CONTROL_ON_COMPLETION_BRAKE;
×
82
    }
83
    return on_completion;
25✔
84
}
85

86
static void pbio_control_status_set(pbio_control_t *ctl, pbio_control_status_flag_t flag, bool set) {
96,011✔
87
    ctl->status = set ? ctl->status | flag : ctl->status & ~flag;
96,011✔
88
}
96,011✔
89

90
static bool pbio_control_status_test(const pbio_control_t *ctl, pbio_control_status_flag_t flag) {
433,900✔
91
    return ctl->status & flag;
433,900✔
92
}
93

94
static bool pbio_control_check_completion(const pbio_control_t *ctl, uint32_t time, const pbio_control_state_t *state, const pbio_trajectory_reference_t *end) {
47,669✔
95

96
    // If no control is active, then all targets are complete.
97
    if (!pbio_control_is_active(ctl)) {
47,669✔
UNCOV
98
        return true;
×
99
    }
100

101
    // If stalling is the *objective*, stall state is completion state.
102
    if (ctl->type & PBIO_CONTROL_TYPE_FLAG_OBJECTIVE_IS_STALL) {
47,669✔
103
        return pbio_control_status_test(ctl, PBIO_CONTROL_STATUS_STALLED);
1,258✔
104
    }
105

106
    // If request to stop on stall, return if stalled but proceed with other checks.
107
    if (ctl->type & PBIO_CONTROL_TYPE_FLAG_STOP_ON_STALL && pbio_control_status_test(ctl, PBIO_CONTROL_STATUS_STALLED)) {
46,411✔
UNCOV
108
        return true;
×
109
    }
110

111
    // Check if we are passed the nominal maneuver time.
112
    bool time_completed = pbio_util_time_has_passed(time, end->time);
46,411✔
113

114
    if (pbio_control_type_is_time(ctl)) {
46,411✔
115
        // Infinite maneuvers are always done (should never block).
116
        if (pbio_trajectory_get_duration(&ctl->trajectory) >= pbio_control_time_ms_to_ticks(PBIO_TRAJECTORY_DURATION_FOREVER_MS)) {
8,401✔
117
            return true;
8,201✔
118
        }
119

120
        // Finite-time maneuvers are done when the full duration has passed.
121
        return time_completed;
200✔
122
    }
123

124
    // What remains now is to deal with angle-based maneuvers. As with time
125
    // based trajectories, we want at least the duration to pass, so return
126
    // false if time has not yet completed.
127
    if (!time_completed) {
38,010✔
128
        return false;
29,377✔
129
    }
130

131
    // For a nonzero final speed, we're done once we're at or past
132
    // the target, no matter the tolerances. Equivalently, we're done
133
    // once the sign of the angle error differs from the speed sign.
134
    int32_t position_remaining = pbio_angle_diff_mdeg(&end->position, &state->position);
8,633✔
135
    if (end->speed != 0) {
8,633✔
136
        return pbio_int_math_sign(position_remaining) != pbio_int_math_sign(end->speed);
1✔
137
    }
138

139
    // For zero final speed, we need to at least stand still, so return false
140
    // when we're still moving faster than the tolerance.
141
    if (pbio_int_math_abs(state->speed) > ctl->settings.speed_tolerance) {
8,632✔
142
        return false;
599✔
143
    }
144

145
    // Once we stand still, we're complete if the distance to the
146
    // target is equal to or less than the allowed tolerance.
147
    return pbio_int_math_abs(position_remaining) <= ctl->settings.position_tolerance;
8,033✔
148
}
149

150
/**
151
 * Gets the position, speed, and acceleration, on the reference trajectory.
152
 *
153
 * This expands the lower-level pbio_trajectory_get_reference(), by
154
 * additionally compensating for the stall state of the integrators.
155
 *
156
 * @param [in]  ctl              Control status structure.
157
 * @param [in]  time_now         Wall time (ticks).
158
 * @param [in]  state            Current control state.
159
 * @param [out] ref              Current reference, compensated for integrator state.
160
 *
161
 */
162
void pbio_control_get_reference(pbio_control_t *ctl, uint32_t time_now, const pbio_control_state_t *state, pbio_trajectory_reference_t *ref) {
119✔
163

164
    // Get reference state, compensating for any time shift in position control.
165
    pbio_trajectory_get_reference(&ctl->trajectory, pbio_control_get_ref_time(ctl, time_now), ref);
119✔
166

167
    // For timed control, further compensate reference position for the shift
168
    // in the speed integrator that may have been incurred due to load.
169
    if (pbio_control_type_is_time(ctl)) {
119✔
170
        int32_t position_error = pbio_angle_diff_mdeg(&ref->position, &state->position);
4✔
171
        int32_t position_error_used = pbio_speed_integrator_get_error(&ctl->speed_integrator, position_error);
4✔
172
        pbio_angle_add_mdeg(&ref->position, position_error_used - position_error);
4✔
173
    }
174
}
119✔
175

176
static int32_t pbio_control_get_pid_kp(const pbio_control_settings_t *settings, int32_t position_error, int32_t target_error, int32_t abs_command_speed) {
47,669✔
177

178
    // Reduced kp values are only needed for some motors under slow speed
179
    // conditions. For everything else, use the default kp value.
180
    if (abs_command_speed >= settings->pid_kp_low_speed_threshold || position_error == 0) {
47,669✔
181
        return settings->pid_kp;
44,380✔
182
    }
183

184
    // We only need a positive kp value, so work with absolute values
185
    // throughout this function to make things easy to follow.
186
    position_error = pbio_int_math_abs(position_error);
3,289✔
187
    target_error = pbio_int_math_abs(target_error);
3,289✔
188

189
    // Lowest kp value, used when steadily turning at slow speed.
190
    const int32_t kp_low = settings->pid_kp * settings->pid_kp_low_pct / 100;
3,289✔
191

192
    // Get equivalent kp value to produce a piece-wise affine (pwa) feedback in the
193
    // position error. It grows slower at first, and then at the configured rate.
194
    const int32_t kp_pwa = position_error <= settings->pid_kp_low_error_threshold ?
6,578✔
195
        // For small errors, feedback is linear in low kp value.
196
        kp_low :
3,289✔
197
        // Above the threshold, feedback grows with the default gain.
198
        settings->pid_kp - settings->pid_kp_low_error_threshold * (settings->pid_kp - kp_low) / position_error;
230✔
199

200
    // Proportional control saturates where the error leads to maximum actuation.
201
    // For errors any smaller than that,
202
    const int32_t saturation_lower = pbio_control_settings_div_by_gain(settings->actuation_max, settings->pid_kp);
3,289✔
203

204
    // Further away from the target, we can use the reduced value and still
205
    // guarantee maximum actuation, to avoid getting stuck.
206
    const int32_t saturation_upper = saturation_lower * 100 / settings->pid_kp_low_pct;
3,289✔
207

208
    // Get the kp value as constrained by the condition to use maxium actuation
209
    // close to the target to guarantee that we can always get there.
210
    int32_t kp_target;
211
    if (target_error < saturation_lower) {
3,289✔
212
        kp_target = settings->pid_kp;
3,085✔
213
    } else if (target_error > saturation_upper) {
204✔
214
        kp_target = kp_low;
169✔
215
    } else {
216
        // In between, we gradually shift towards the higher value as we get
217
        // closer to the final target to avoid a sudden transition.
218
        kp_target = kp_low + settings->pid_kp *
35✔
219
            (100 - settings->pid_kp_low_pct) * (saturation_upper - target_error) /
35✔
220
            (saturation_upper - saturation_lower) / 100;
35✔
221
    }
222

223
    // The most constrained objective is obtained by taking the highest value.
224
    return pbio_int_math_max(kp_pwa, kp_target);
3,289✔
225
}
226

227
/**
228
 * Updates the PID controller state to calculate the next actuation step.
229
 *
230
 * @param [in]   ctl              The control instance.
231
 * @param [in]   time_now         The wall time (ticks).
232
 * @param [in]   state            The current state of the system being controlled (control units).
233
 * @param [out]  ref              Computed reference point on the trajectory (control units).
234
 * @param [out]  actuation        Required actuation type.
235
 * @param [out]  control          The control output, which is the actuation payload (control units).
236
 * @param [inout] external_pause  Whether to force the controller to pause using external information (in), and
237
 *                                whether the controller still needs pausing according to its own state (out).
238
 */
239
void pbio_control_update(
47,669✔
240
    pbio_control_t *ctl,
241
    uint32_t time_now,
242
    const pbio_control_state_t *state,
243
    pbio_trajectory_reference_t *ref,
244
    pbio_dcmotor_actuation_t *actuation,
245
    int32_t *control,
246
    bool *external_pause) {
247

248
    // Get reference signals at the reference time point in the trajectory.
249
    // This compensates for any time we may have spent pausing when the motor was stalled.
250
    pbio_trajectory_get_reference(&ctl->trajectory, pbio_control_get_ref_time(ctl, time_now), ref);
47,669✔
251

252
    // Get reference point we want to be at in the end, to check for completion.
253
    pbio_trajectory_reference_t ref_end;
254
    pbio_trajectory_get_endpoint(&ctl->trajectory, &ref_end);
47,669✔
255

256
    // Get position and speed error
257
    int32_t position_error = pbio_angle_diff_mdeg(&ref->position, &state->position);
47,669✔
258
    int32_t speed_error = ref->speed - state->speed_estimate;
47,669✔
259

260
    // Calculate integral control errors, depending on control type.
261
    int32_t integral_error;
262
    int32_t position_error_used;
263
    int32_t target_error;
264
    if (pbio_control_type_is_position(ctl)) {
47,669✔
265
        // Get error to final position, used below to adjust controls near end.
266
        target_error = pbio_angle_diff_mdeg(&ref_end.position, &state->position);
38,010✔
267
        // Update count integral error and get current error state
268
        integral_error = pbio_position_integrator_update(&ctl->position_integrator, position_error, target_error);
38,010✔
269
        // For position control, the proportional term is the real position error.
270
        position_error_used = position_error;
38,010✔
271
    } else {
272
        // For time/speed based commands, the main error is speed. It integrates into a quantity with unit of position.
273
        // There is no count integral control, because we do not need a second order integrator for speed control.
274
        position_error_used = pbio_speed_integrator_get_error(&ctl->speed_integrator, position_error);
9,659✔
275
        integral_error = 0;
9,659✔
276
        // Speed maneuvers don't have a specific angle end target, so the error is infinite.
277
        target_error = INT32_MAX;
9,659✔
278
    }
279

280
    // Corresponding PID control signal
281
    int32_t pid_kp = pbio_control_get_pid_kp(&ctl->settings, position_error, target_error, pbio_trajectory_get_abs_command_speed(&ctl->trajectory));
47,669✔
282
    int32_t torque_proportional = pbio_control_settings_mul_by_gain(position_error_used, pid_kp);
47,669✔
283
    int32_t torque_derivative = pbio_control_settings_mul_by_gain(speed_error, ctl->settings.pid_kd);
47,669✔
284
    int32_t torque_integral = pbio_control_settings_mul_by_gain(integral_error, ctl->settings.pid_ki);
47,669✔
285

286
    // Total torque signal, capped by the actuation limit
287
    int32_t torque = pbio_int_math_clamp(torque_proportional + torque_integral + torque_derivative, ctl->settings.actuation_max_temporary);
47,669✔
288

289
    // This completes the computation of the control signal.
290
    // The next steps take care of handling windup, or triggering a stop if we are on target.
291

292
    // We want to stop building up further errors if we are at the proportional torque limit. So, we pause the trajectory
293
    // if we get at this limit. We wait a little longer though, to make sure it does not fall back to below the limit
294
    // within one sample, which we can predict using the current rate times the loop time, with a factor two tolerance.
295
    int32_t windup_margin = pbio_control_settings_mul_by_loop_time(pbio_int_math_abs(state->speed)) * 2;
47,669✔
296
    int32_t max_windup_torque = ctl->settings.actuation_max_temporary + pbio_control_settings_mul_by_gain(windup_margin, ctl->settings.pid_kp);
47,669✔
297

298
    // Speed value that is rounded to zero if small. This is used for a
299
    // direction error check below to avoid false reverses near zero.
300
    int32_t speed_for_direction_check = pbio_int_math_abs(state->speed) < ctl->settings.stall_speed_limit ? 0 : state->speed;
47,669✔
301

302
    // Position anti-windup: pause trajectory or integration if falling behind despite using maximum torque
303
    bool pause_integration =
47,669✔
304
        // Pause if proportional torque is beyond maximum windup torque:
305
        pbio_int_math_abs(torque_proportional) >= max_windup_torque &&
47,669✔
306
        // But not if we're trying to run in the other direction (else we can get unstuck by just reversing).
307
        pbio_int_math_sign(torque_proportional) != -pbio_int_math_sign(ref->speed - speed_for_direction_check) &&
48,875✔
308
        // But not if we should be accelerating in the other direction (else we can get unstuck by just reversing).
309
        pbio_int_math_sign(torque_proportional) != -pbio_int_math_sign(ref->acceleration);
1,206✔
310

311
    // Position anti-windup in case of angle control (accumulated position error may not get too high)
312
    if (pbio_control_type_is_position(ctl)) {
47,669✔
313
        if (pause_integration || *external_pause) {
38,010✔
314
            // We are at the torque limit and we should prevent further position error integration.
315
            pbio_position_integrator_pause(&ctl->position_integrator, time_now);
494✔
316
        } else {
317
            // Not at the limit so continue integrating errors
318
            pbio_position_integrator_resume(&ctl->position_integrator, time_now);
37,516✔
319
        }
320
    }
321
    // Position anti-windup in case of timed speed control (speed integral may not get too high)
322
    else {
323
        if (pause_integration || *external_pause) {
9,659✔
324
            // We are at the torque limit and we should prevent further speed error integration.
325
            pbio_speed_integrator_pause(&ctl->speed_integrator, time_now, position_error);
745✔
326
        } else {
327
            // Not at the limit so continue integrating errors
328
            pbio_speed_integrator_resume(&ctl->speed_integrator, position_error);
8,914✔
329
        }
330
    }
331

332
    // Informs caller if pausing is still needed according to this controller's state.
333
    *external_pause = pause_integration;
47,669✔
334

335
    // Check if controller is stalled, and set the status.
336
    pbio_control_status_set(ctl, PBIO_CONTROL_STATUS_STALLED,
95,338✔
337
        pbio_control_type_is_position(ctl) ?
47,669✔
338
        pbio_position_integrator_stalled(&ctl->position_integrator, time_now, state->speed, ref->speed) :
38,010✔
339
        pbio_speed_integrator_stalled(&ctl->speed_integrator, time_now, state->speed, ref->speed));
9,659✔
340

341
    // Check if we are on target, and set the status.
342
    pbio_control_status_set(ctl, PBIO_CONTROL_STATUS_COMPLETE,
47,669✔
343
        pbio_control_check_completion(ctl, ref->time, state, &ref_end));
47,669✔
344

345
    // Save (low-pass filtered) load for diagnostics
346
    ctl->pid_average = (ctl->pid_average * (100 - PBIO_CONFIG_CONTROL_LOOP_TIME_MS) + torque * PBIO_CONFIG_CONTROL_LOOP_TIME_MS) / 100;
47,669✔
347

348
    // Decide actuation based on control status.
349
    if (// Not on target yet, so keep actuating.
47,669✔
350
        !pbio_control_status_test(ctl, PBIO_CONTROL_STATUS_COMPLETE) ||
63,886✔
351
        // Active completion type, so keep actuating.
352
        pbio_control_on_completion_is_active(ctl->on_completion) ||
16,263✔
353
        // Smart passive mode, and we're only just complete, so keep actuating.
354
        // This ensures that any subsequent user command can pick up from here
355
        // without resetting any controllers. This avoids accumulating errors
356
        // in sequential relative maneuvers.
357
        (pbio_control_on_completion_is_passive_smart(ctl->on_completion) &&
46✔
358
         !pbio_util_time_has_passed(ref->time, ref_end.time + ctl->settings.smart_passive_hold_time))) {
37✔
359
        // Keep actuating, so apply calculated PID torque value.
360
        *actuation = PBIO_DCMOTOR_ACTUATION_TORQUE;
47,658✔
361
        *control = torque;
47,658✔
362
    } else {
363
        // No more control is needed, so switch to passive mode.
364
        *actuation = pbio_control_passive_completion_to_actuation_type(ctl->on_completion);
11✔
365
        *control = 0;
11✔
366
        pbio_control_stop(ctl);
11✔
367
    }
368

369
    // Handling hold after running for time requires an extra step because
370
    // it can only be done by starting a new position based command.
371
    if (pbio_control_status_test(ctl, PBIO_CONTROL_STATUS_COMPLETE) &&
63,886✔
372
        pbio_control_type_is_time(ctl) &&
16,217✔
373
        ctl->on_completion == PBIO_CONTROL_ON_COMPLETION_HOLD) {
8,202✔
374
        // Use current state as target for holding.
375
        int32_t target = pbio_control_settings_ctl_to_app_long(&ctl->settings, &state->position);
1✔
376
        pbio_control_start_position_control_hold(ctl, time_now, target);
1✔
377
    }
378

379
    // Optionally log control data.
380
    if (pbio_logger_is_active(&ctl->log)) {
47,669✔
381

382
        // For speed control, we use a reference adjusted for pausing. This is
383
        // accounted for above, but here we need it in angle units for logging.
384
        pbio_angle_t ref_position_log = ref->position;
×
385
        pbio_angle_add_mdeg(&ref_position_log, position_error_used - position_error);
×
386

387
        int32_t log_data[] = {
×
388
            // Column 0: Log time (added by logger).
389
            // Column 1: Time since start of trajectory.
390
            ref->time - ctl->trajectory.start.time,
×
391
            // Column 2: Position in application units.
392
            pbio_control_settings_ctl_to_app_long(&ctl->settings, &state->position),
×
393
            // Column 3: Speed in application units.
394
            pbio_control_settings_ctl_to_app(&ctl->settings, state->speed),
×
395
            // Column 4: Actuation type (LSB 0--1), stall state (LSB 2), on target (LSB 3), pause integration (LSB 4).
396
            *actuation | (ctl->status << 2) | ((int32_t)pause_integration << 4),
×
397
            // Column 5: Actuation payload, e.g. torque.
398
            *control,
×
399
            // Column 6: Reference position in application units.
400
            pbio_control_settings_ctl_to_app_long(&ctl->settings, &ref_position_log),
×
401
            // Column 7: Reference speed in application units.
402
            pbio_control_settings_ctl_to_app(&ctl->settings, ref->speed),
×
403
            // Column 8: Estimated position in application units.
404
            pbio_control_settings_ctl_to_app_long(&ctl->settings, &state->position_estimate),
×
405
            // Column 9: Estimated speed in application units.
406
            pbio_control_settings_ctl_to_app(&ctl->settings, state->speed_estimate),
×
407
            // Column 10: P term of PID control in (uNm).
408
            torque_proportional,
409
            // Column 12: I term of PID control in (uNm).
410
            torque_integral,
411
            // Column 13: D term of PID control in (uNm).
412
            torque_derivative,
413
        };
414
        pbio_logger_add_row(&ctl->log, log_data);
×
415
    }
416
}
47,669✔
417

418
/**
419
 * Stops (but not resets) the update loop from updating this controller. This
420
 * is normally called when a motor coasts or brakes.
421
 *
422
 * @param [in]  ctl         Control status structure.
423
 */
424
void pbio_control_stop(pbio_control_t *ctl) {
245✔
425
    ctl->type = PBIO_CONTROL_TYPE_NONE;
245✔
426
    pbio_control_status_set(ctl, PBIO_CONTROL_STATUS_COMPLETE, true);
245✔
427
    pbio_control_status_set(ctl, PBIO_CONTROL_STATUS_STALLED, false);
245✔
428
    ctl->pid_average = 0;
245✔
429
}
245✔
430

431
/**
432
 * Sets the control type for the new maneuver and initializes the corresponding
433
 * control status.
434
 *
435
 * @param [in]  ctl            The control instance.
436
 * @param [in]  time_now       The wall time (ticks).
437
 * @param [in]  type           Control type for the next maneuver.
438
 * @param [in]  on_completion  What to do when reaching the target position.
439
 */
440
static void pbio_control_set_control_type(pbio_control_t *ctl, uint32_t time_now, pbio_control_type_t type, pbio_control_on_completion_t on_completion) {
125✔
441

442
    // Setting none control type is the same as stopping.
443
    if ((type & PBIO_CONTROL_TYPE_MASK) == PBIO_CONTROL_TYPE_NONE) {
125✔
444
        pbio_control_stop(ctl);
×
445
        return;
×
446
    }
447

448
    // Set on completion action for this maneuver.
449
    ctl->on_completion = on_completion;
125✔
450

451
    // Reset maximum actuation value used for this run.
452
    ctl->settings.actuation_max_temporary = ctl->settings.actuation_max;
125✔
453

454
    // Reset done state. It will get the correct value during the next control
455
    // update. REVISIT: Evaluate it here.
456
    pbio_control_status_set(ctl, PBIO_CONTROL_STATUS_COMPLETE, false);
125✔
457

458
    // Exit if control type already set.
459
    if (ctl->type == type) {
125✔
460
        return;
67✔
461
    }
462

463
    // Reset stall state. It will get the correct value during the next control
464
    // update. REVISIT: Evaluate it here.
465
    pbio_control_status_set(ctl, PBIO_CONTROL_STATUS_STALLED, false);
58✔
466

467
    // Reset integrator for new control type.
468
    if ((type & PBIO_CONTROL_TYPE_MASK) == PBIO_CONTROL_TYPE_POSITION) {
58✔
469
        // If the new type is position, reset position integrator.
470
        pbio_position_integrator_reset(&ctl->position_integrator, &ctl->settings, time_now);
34✔
471
    } else {
472
        // If the new type is timed, reset speed integrator.
473
        pbio_speed_integrator_reset(&ctl->speed_integrator, &ctl->settings);
24✔
474
    }
475

476
    // Set the given type.
477
    ctl->type = type;
58✔
478
}
479

480
/**
481
 * Resets and initializes the control state. This is called when a device that
482
 * uses this controller is first initialized or when it is disconnected.
483
 *
484
 * @param [in]  ctl         Control status structure.
485
 */
486
void pbio_control_reset(pbio_control_t *ctl) {
45✔
487

488
    // Stop the control loop update.
489
    pbio_control_stop(ctl);
45✔
490

491
    // Reset the previous on-completion state.
492
    ctl->on_completion = PBIO_CONTROL_ON_COMPLETION_COAST;
45✔
493

494
    // The on_completion state is the only persistent setting between
495
    // subsequent maneuvers, so nothing else needs to be reset explicitly.
496
}
45✔
497

498
static pbio_error_t _pbio_control_start_position_control(pbio_control_t *ctl, uint32_t time_now, const pbio_control_state_t *state, const pbio_angle_t *target, int32_t speed, pbio_control_on_completion_t on_completion, bool allow_trajectory_shift) {
95✔
499

500
    pbio_error_t err;
501

502
    // Common trajectory parameters for all cases covered here.
503
    pbio_trajectory_command_t command = {
190✔
504
        .position_end = *target,
505
        .speed_target = speed == 0 ? ctl->settings.speed_default : speed,
95✔
506
        .speed_max = ctl->settings.speed_max,
95✔
507
        .acceleration = ctl->settings.acceleration,
95✔
508
        .deceleration = ctl->settings.deceleration,
95✔
509
        .continue_running = on_completion == PBIO_CONTROL_ON_COMPLETION_CONTINUE,
95✔
510
    };
511

512

513
    // Given the control status, fill in remaining commands and get trajectory.
514
    if (!pbio_control_is_active(ctl)) {
95✔
515
        // If no control is ongoing, we just start from the measured state.
516
        command.time_start = time_now;
30✔
517
        command.position_start = state->position;
30✔
518
        command.speed_start = state->speed;
30✔
519

520
        // With the command fully populated, we can calculate the trajectory.
521
        err = pbio_trajectory_new_angle_command(&ctl->trajectory, &command);
30✔
522
        if (err != PBIO_SUCCESS) {
30✔
UNCOV
523
            return err;
×
524
        }
525
    } else if (pbio_control_type_is_time(ctl)) {
65✔
526
        // If timed control is ongoing, start from its current reference,
527
        // accounting for its integrator windup.
528
        pbio_trajectory_reference_t ref;
529
        pbio_control_get_reference(ctl, time_now, state, &ref);
2✔
530
        command.time_start = ref.time;
2✔
531
        command.position_start = ref.position;
2✔
532
        command.speed_start = ref.speed;
2✔
533

534
        // With the command fully populated, we can calculate the trajectory.
535
        err = pbio_trajectory_new_angle_command(&ctl->trajectory, &command);
2✔
536
        if (err != PBIO_SUCCESS) {
2✔
537
            return err;
×
538
        }
539
    } else {
540
        // Otherwise, if position control is active, (re)start from the current
541
        // reference. This way the reference just branches off on a new
542
        // trajectory instead of falling back slightly, avoiding a speed drop.
543
        pbio_trajectory_reference_t ref;
544
        pbio_control_get_reference(ctl, time_now, state, &ref);
63✔
545
        command.time_start = ref.time;
63✔
546
        command.position_start = ref.position;
63✔
547
        command.speed_start = ref.speed;
63✔
548

549
        // Before we override the trajectory to renew it, get the starting
550
        // point of the current speed/angle segment of the reference. We may
551
        // need it below.
552
        pbio_trajectory_reference_t ref_vertex;
553
        pbio_trajectory_get_last_vertex(&ctl->trajectory, command.time_start, &ref_vertex);
63✔
554

555
        // With the command fully populated, we can calculate the trajectory.
556
        err = pbio_trajectory_new_angle_command(&ctl->trajectory, &command);
63✔
557
        if (err != PBIO_SUCCESS) {
63✔
558
            return err;
×
559
        }
560

561
        // If the new trajectory is tangent to the current one, we can do
562
        // better than just branching off. Instead, we can adjust the command
563
        // so it starts from the same point as the previous trajectory. This
564
        // avoids rounding errors when restarting commands in a tight loop.
565
        if (ctl->trajectory.a0 == ref.acceleration && allow_trajectory_shift) {
63✔
566

567
            // Update command with shifted starting point, equal to ongoing
568
            // maneuver.
569
            command.time_start = ref_vertex.time;
5✔
570
            command.position_start = ref_vertex.position;
5✔
571
            command.speed_start = ref_vertex.speed;
5✔
572

573
            // Recalculate the trajectory from the shifted starting point.
574
            err = pbio_trajectory_new_angle_command(&ctl->trajectory, &command);
5✔
575
            if (err != PBIO_SUCCESS) {
5✔
UNCOV
576
                return err;
×
577
            }
578
        }
579
    }
580

581
    // Activate control type and reset integrators if needed.
582
    pbio_control_set_control_type(ctl, time_now, PBIO_CONTROL_TYPE_POSITION, on_completion);
95✔
583

584
    return PBIO_SUCCESS;
95✔
585
}
586

587
/**
588
 * Starts the controller to run to a given target position.
589
 *
590
 * In a servo application, this means running to a target angle.
591
 *
592
 * @param [in]  ctl            The control instance.
593
 * @param [in]  time_now       The wall time (ticks).
594
 * @param [in]  state          The current state of the system being controlled (control units).
595
 * @param [in]  position       The target position to run to (application units).
596
 * @param [in]  speed          The top speed on the way to the target (application units). The sign is ignored. If zero, default speed is used.
597
 * @param [in]  on_completion  What to do when reaching the target position.
598
 * @return                     Error code.
599
 */
600
pbio_error_t pbio_control_start_position_control(pbio_control_t *ctl, uint32_t time_now, const pbio_control_state_t *state, int32_t position, int32_t speed, pbio_control_on_completion_t on_completion) {
24✔
601

602
    // Convert target position to control units.
603
    pbio_angle_t target;
604
    pbio_control_settings_app_to_ctl_long(&ctl->settings, position, &target);
24✔
605

606
    // Start position control in control units.
607
    return _pbio_control_start_position_control(ctl, time_now, state, &target, pbio_control_settings_app_to_ctl(&ctl->settings, speed), on_completion, true);
24✔
608
}
609

610
/**
611
 * Starts the controller to run by a given distance.
612
 *
613
 * In a servo application, this means running by the given angle.
614
 *
615
 * This function computes what the new target position will be, and then
616
 * calls pbio_control_start_position_control to get there.
617
 *
618
 * @param [in]  ctl                    The control instance.
619
 * @param [in]  time_now               The wall time (ticks).
620
 * @param [in]  state                  The current state of the system being controlled (control units).
621
 * @param [in]  distance               The distance to run by (application units).
622
 * @param [in]  speed                  The top speed on the way to the target (application units). Negative speed flips the distance sign.
623
 * @param [in]  on_completion          What to do when reaching the target position.
624
 * @param [in]  allow_trajectory_shift Whether trajectory may be time-shifted for better performance in tight loops (true) or not (false).
625
 * @return                             Error code.
626
 */
627
pbio_error_t pbio_control_start_position_control_relative(pbio_control_t *ctl, uint32_t time_now, const pbio_control_state_t *state, int32_t distance, int32_t speed, pbio_control_on_completion_t on_completion, bool allow_trajectory_shift) {
71✔
628

629
    // Convert distance to control units.
630
    pbio_angle_t increment;
631
    pbio_control_settings_app_to_ctl_long(&ctl->settings, (speed < 0 ? -distance : distance), &increment);
71✔
632

633
    // We need to decide where the relative motion starts from, and use that
634
    // to compute the position target by adding the increment.
635
    pbio_angle_t target;
636

637
    if (pbio_control_is_active(ctl)) {
71✔
638
        // If control is already active, restart from current reference.
639
        pbio_trajectory_reference_t ref;
640
        pbio_control_get_reference(ctl, time_now, state, &ref);
54✔
641
        pbio_angle_sum(&ref.position, &increment, &target);
54✔
642
    } else {
643
        // Control is inactive. We still have two options.
644
        // If the previous command used smart coast and we're still close to
645
        // its target, we want to start from there. This avoids accumulating
646
        // errors in programs that use mostly relative motions like run_angle.
647
        pbio_trajectory_reference_t prev_end;
648
        pbio_trajectory_get_endpoint(&ctl->trajectory, &prev_end);
17✔
649
        if (pbio_control_on_completion_is_passive_smart(ctl->on_completion) &&
19✔
650
            pbio_angle_diff_is_small(&prev_end.position, &state->position) &&
2✔
651
            pbio_int_math_abs(pbio_angle_diff_mdeg(&prev_end.position, &state->position)) < ctl->settings.position_tolerance * 2) {
2✔
652
            // We're close enough, so make the new target relative to the
653
            // endpoint of the last one.
654
            pbio_angle_sum(&prev_end.position, &increment, &target);
2✔
655
        } else {
656
            // No special cases apply, so the best we can do is just start from
657
            // the current state.
658
            pbio_angle_sum(&state->position, &increment, &target);
15✔
659
        }
660
    }
661

662
    return _pbio_control_start_position_control(ctl, time_now, state, &target, pbio_control_settings_app_to_ctl(&ctl->settings, speed), on_completion, allow_trajectory_shift);
71✔
663
}
664

665
/**
666
 * Starts the controller and holds at the given position.
667
 *
668
 * This is similar to starting position control, but it skips the trajectory
669
 * computation and just sets the reference to the target position right away.
670
 *
671
 * @param [in]  ctl             The control instance.
672
 * @param [in]  time_now        The wall time (ticks).
673
 * @param [in]  position        The target position to hold (application units).
674
 * @return                      Error code.
675
 */
676
pbio_error_t pbio_control_start_position_control_hold(pbio_control_t *ctl, uint32_t time_now, int32_t position) {
5✔
677

678
    // Compute new maneuver based on user argument, starting from the initial state
679
    pbio_trajectory_command_t command = {
10✔
680
        .time_start = pbio_control_get_ref_time(ctl, time_now),
5✔
681
        .speed_target = 0,
682
        .continue_running = false,
683
    };
684
    pbio_control_settings_app_to_ctl_long(&ctl->settings, position, &command.position_start);
5✔
685
    pbio_control_settings_app_to_ctl_long(&ctl->settings, position, &command.position_end);
5✔
686

687
    // Holding means staying at a constant trajectory.
688
    pbio_trajectory_make_constant(&ctl->trajectory, &command);
5✔
689

690
    // Activate control type and reset integrators if needed.
691
    pbio_control_set_control_type(ctl, time_now, PBIO_CONTROL_TYPE_POSITION, PBIO_CONTROL_ON_COMPLETION_HOLD);
5✔
692

693
    return PBIO_SUCCESS;
5✔
694
}
695

696
/**
697
 * Starts the controller to run for a given amount of time.
698
 *
699
 * @param [in]  ctl             The control instance.
700
 * @param [in]  time_now        The wall time (ticks).
701
 * @param [in]  state           The current state of the system being controlled (control units).
702
 * @param [in]  duration        For how long to run (ms).
703
 * @param [in]  speed           The top speed (application units). Negative speed means reverse.
704
 * @param [in]  on_completion   What to do when duration is over.
705
 * @return                      Error code.
706
 */
707
pbio_error_t pbio_control_start_timed_control(pbio_control_t *ctl, uint32_t time_now, const pbio_control_state_t *state, uint32_t duration, int32_t speed, pbio_control_on_completion_t on_completion) {
25✔
708

709
    pbio_error_t err;
710

711
    // For timed maneuvers, being "smart" by remembering the position endpoint
712
    // does nothing useful, so discard it to keep only the passive actuation type.
713
    on_completion = pbio_control_on_completion_discard_smart(on_completion);
25✔
714

715
    // Common trajectory parameters for the cases covered here.
716
    pbio_trajectory_command_t command = {
75✔
717
        .time_start = time_now,
718
        .duration = pbio_control_time_ms_to_ticks(duration),
25✔
719
        .speed_target = pbio_control_settings_app_to_ctl(&ctl->settings, speed),
25✔
720
        .speed_max = ctl->settings.speed_max,
25✔
721
        .acceleration = ctl->settings.acceleration,
25✔
722
        .deceleration = ctl->settings.deceleration,
25✔
723
        .continue_running = on_completion == PBIO_CONTROL_ON_COMPLETION_CONTINUE,
25✔
724
    };
725

726
    // Given the control status, fill in remaining commands and get trajectory.
727
    if (!pbio_control_is_active(ctl)) {
25✔
728
        // If no control is ongoing, we just start from the measured state.
729
        command.time_start = time_now;
17✔
730
        command.position_start = state->position;
17✔
731
        command.speed_start = state->speed;
17✔
732

733
        // With the command fully populated, we can calculate the trajectory.
734
        err = pbio_trajectory_new_time_command(&ctl->trajectory, &command);
17✔
735
        if (err != PBIO_SUCCESS) {
17✔
UNCOV
736
            return err;
×
737
        }
738
    } else {
739
        // Otherwise, If control is active, (re)start from the current
740
        // reference. This way the reference just branches off on a new
741
        // trajectory instead of falling back slightly, avoiding a speed drop.
742
        // NB: We do not compensate for the speed integrator offset here using
743
        // pbio_control_get_reference() since the new time based maneuver
744
        // proceeds to use the same one.
745
        uint32_t time_ref = pbio_control_get_ref_time(ctl, time_now);
8✔
746
        pbio_trajectory_reference_t ref;
747
        pbio_trajectory_get_reference(&ctl->trajectory, time_ref, &ref);
8✔
748
        command.position_start = ref.position;
8✔
749
        command.speed_start = ref.speed;
8✔
750

751
        // Before we override the trajectory to renew it, get the starting
752
        // point of the current speed/angle segment of the reference. We may
753
        // need it below.
754
        pbio_trajectory_reference_t ref_vertex;
755
        pbio_trajectory_get_last_vertex(&ctl->trajectory, command.time_start, &ref_vertex);
8✔
756

757
        // With the command fully populated, we can calculate the trajectory.
758
        err = pbio_trajectory_new_time_command(&ctl->trajectory, &command);
8✔
759
        if (err != PBIO_SUCCESS) {
8✔
760
            return err;
×
761
        }
762

763
        // If the new trajectory is tangent to the current one, we can do
764
        // better than just branching off. Instead, we can adjust the command
765
        // so it starts from the same point as the previous trajectory. This
766
        // avoids rounding errors when restarting commands in a tight loop.
767
        if (pbio_control_type_is_time(ctl) && ctl->trajectory.a0 == ref.acceleration) {
8✔
768

769
            // Update command with shifted starting point, equal to ongoing
770
            // maneuver.
771
            command.time_start = ref_vertex.time;
×
772
            command.position_start = ref_vertex.position;
×
773
            command.speed_start = ref_vertex.speed;
×
774

775
            // We shifted the start time into the past, so we must adjust
776
            // duration accordingly.
777
            command.duration += (time_now - ref_vertex.time);
×
778

779
            // Recalculate the trajectory from the shifted starting point.
780
            err = pbio_trajectory_new_time_command(&ctl->trajectory, &command);
×
781
            if (err != PBIO_SUCCESS) {
×
UNCOV
782
                return err;
×
783
            }
784
        }
785
    }
786

787
    // Activate control type and reset integrators if needed.
788
    pbio_control_set_control_type(ctl, time_now, PBIO_CONTROL_TYPE_TIMED, on_completion);
25✔
789

790
    return PBIO_SUCCESS;
25✔
791
}
792

793

794

795
/**
796
 * Gets the time at which to evaluate the reference trajectory by compensating
797
 * the wall time by the amount of time spent stalling during which a position
798
 * based trajectory does not progress.
799
 *
800
 * @param [in]  ctl         Control status structure.
801
 * @param [in]  time_now    Wall time (ticks).
802
 * @return int32_t          Time (ticks) on the trajectory curve.
803
 */
804
uint32_t pbio_control_get_ref_time(const pbio_control_t *ctl, uint32_t time_now) {
47,801✔
805
    // Angle controllers may pause the time so the reference position does not
806
    // keep accumulating while the controller is stuck.
807
    if (pbio_control_type_is_position(ctl)) {
47,801✔
808
        return pbio_position_integrator_get_ref_time(&ctl->position_integrator, time_now);
38,135✔
809
    }
810
    // In all other cases, it is just the current time.
811
    return time_now;
9,666✔
812
}
813

814
/**
815
 * Checks if the controller is currently active.
816
 *
817
 * @param [in]  ctl             The control instance.
818
 * @return                      True if active (position or time), false if not.
819
 */
820
bool pbio_control_is_active(const pbio_control_t *ctl) {
477,212✔
821
    return (ctl->type & PBIO_CONTROL_TYPE_MASK) != PBIO_CONTROL_TYPE_NONE;
477,212✔
822
}
823

824
/**
825
 * Checks if the controller is currently doing position control.
826
 *
827
 * @param [in]  ctl             The control instance.
828
 * @return                      True if position control is active, false if not.
829
 */
830
bool pbio_control_type_is_position(const pbio_control_t *ctl) {
190,813✔
831
    return (ctl->type & PBIO_CONTROL_TYPE_MASK) == PBIO_CONTROL_TYPE_POSITION;
190,813✔
832
}
833

834
/**
835
 * Checks if the controller is currently doing timed control.
836
 *
837
 * @param [in]  ctl             The control instance.
838
 * @return                      True if timed control is active, false if not.
839
 */
840
bool pbio_control_type_is_time(const pbio_control_t *ctl) {
62,820✔
841
    return (ctl->type & PBIO_CONTROL_TYPE_MASK) == PBIO_CONTROL_TYPE_TIMED;
62,820✔
842
}
843

844
/**
845
 * Checks if the controller is stalled and for how long.
846
 *
847
 * @param [in]  ctl             The control instance.
848
 * @param [out] stall_duration  For how long the controller has stalled (ticks).
849
 * @return                      True if controller is stalled, false if not.
850
 */
851
bool pbio_control_is_stalled(const pbio_control_t *ctl, uint32_t *stall_duration) {
2,032✔
852

853
    // Return false if no control is active or if we're not stalled.
854
    if (!pbio_control_is_active(ctl) || !pbio_control_status_test(ctl, PBIO_CONTROL_STATUS_STALLED)) {
2,032✔
855
        *stall_duration = 0;
2,027✔
856
        return false;
2,027✔
857
    }
858

859
    // Get time since stalling began.
860
    uint32_t time_pause_begin = pbio_control_type_is_position(ctl) ? ctl->position_integrator.time_pause_begin : ctl->speed_integrator.time_pause_begin;
5✔
861
    *stall_duration = pbio_control_get_time_ticks() - time_pause_begin;
5✔
862

863
    return true;
5✔
864
}
865

866
/**
867
 * Checks if the controller is done.
868
 *
869
 * For trajectories with a stationary endpoint, done means on target.
870
 *
871
 * @param [in]  ctl             The control instance.
872
 * @return                      True if the controller is done, false if not.
873
 */
874
bool pbio_control_is_done(const pbio_control_t *ctl) {
335,280✔
875
    return !pbio_control_is_active(ctl) || pbio_control_status_test(ctl, PBIO_CONTROL_STATUS_COMPLETE);
335,280✔
876
}
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