• 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

72.0
/lib/pbio/src/servo.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 <inttypes.h>
8
#include <stdlib.h>
9
#include <string.h>
10

11
#include <pbdrv/clock.h>
12
#include <pbdrv/counter.h>
13

14
#include <pbio/angle.h>
15
#include <pbio/int_math.h>
16
#include <pbio/observer.h>
17
#include <pbio/parent.h>
18
#include <pbio/servo.h>
19

20
#if PBIO_CONFIG_SERVO
21

22
// Servo motor objects
23
static pbio_servo_t servos[PBIO_CONFIG_SERVO_NUM_DEV];
24

25

26
/**
27
 * Initializes servo state structure.
28
 *
29
 * @param [in]  index        The index of the servo.
30
 * @param [in]  port         The port instance.
31
 * @param [in]  dcmotor      The DC motor instance.
32
 */
33
pbio_servo_t *pbio_servo_init_instance(uint8_t index, pbio_port_t *port, pbio_dcmotor_t *dcmotor) {
216✔
34
    if (index >= PBIO_CONFIG_SERVO_NUM_DEV) {
216✔
UNCOV
35
        return NULL;
×
36
    }
37
    pbio_servo_t *srv = &servos[index];
216✔
38
    srv->dcmotor = dcmotor;
216✔
39
    srv->tacho.port = port;
216✔
40
    return srv;
216✔
41
}
42

43

44
static void pbio_servo_update_loop_set_state(pbio_servo_t *srv, bool update) {
46✔
45
    srv->run_update_loop = update;
46✔
46
}
46✔
47

48
/**
49
 * Gets the state of the servo update loop.
50
 *
51
 * This becomes true after a successful call to pbio_servo_setup and becomes
52
 * false when there is an error. Such as when the cable is unplugged.
53
 *
54
 * @param [in]  srv         The servo instance
55
 * @return                  True if up and running, false if not.
56
 */
57
bool pbio_servo_update_loop_is_running(pbio_servo_t *srv) {
506,726✔
58

59
    // Servo must be the parent of its dc motor.
60
    if (!pbio_parent_equals(&srv->dcmotor->parent, srv)) {
506,726✔
61
        pbio_servo_update_loop_set_state(srv, false);
×
62
    }
63

64
    return srv->run_update_loop;
506,726✔
65
}
66

67
static pbio_error_t pbio_servo_update(pbio_servo_t *srv) {
53,713✔
68

69
    // Get current time
70
    uint32_t time_now = pbio_control_get_time_ticks();
53,713✔
71

72
    // Read the physical and estimated state
73
    pbio_control_state_t state;
74
    pbio_error_t err = pbio_servo_get_state_control(srv, &state);
53,713✔
75
    if (err != PBIO_SUCCESS) {
53,713✔
UNCOV
76
        return err;
×
77
    }
78

79
    // Trajectory reference point
80
    pbio_trajectory_reference_t ref;
81

82
    // Control action to be calculated
83
    int32_t feedback_torque = 0;
53,713✔
84
    int32_t feedforward_torque = 0;
53,713✔
85

86
    // Check if a control update is needed
87
    if (pbio_control_is_active(&srv->control)) {
53,713✔
88

89
        // Calculate feedback control signal
90
        pbio_dcmotor_actuation_t requested_actuation;
91
        bool external_pause = false;
12,421✔
92
        pbio_control_update(&srv->control, time_now, &state, &ref, &requested_actuation, &feedback_torque, &external_pause);
12,421✔
93

94
        // Get required feedforward torque for current reference
95
        feedforward_torque = pbio_observer_get_feedforward_torque(srv->observer.model, ref.speed, ref.acceleration);
12,421✔
96

97
        // HACK: Constrain total torque to respect temporary duty_cycle limit.
98
        // See https://github.com/pybricks/support/issues/1069.
99
        // The feedback torque is already constrained by the temporary limit,
100
        // and this would be enough in a run_until_overload-like scenario. But
101
        // for now we must limit the feedforward torque also, or it would never
102
        // get into the stall state at high speeds.
103
        int32_t total_torque = pbio_int_math_clamp(feedback_torque + feedforward_torque, srv->control.settings.actuation_max_temporary);
12,421✔
104

105
        // Actuate the servo. For torque control, the torque payload is passed along. Otherwise payload is ignored.
106
        err = pbio_servo_actuate(srv, requested_actuation, total_torque);
12,421✔
107
        if (err != PBIO_SUCCESS) {
12,421✔
108
            return err;
×
109
        }
110
    }
111
    // Whether or not there is control, get the ongoing actuation state so we can log it and update observer.
112
    pbio_dcmotor_actuation_t applied_actuation;
113
    int32_t voltage;
114
    pbio_dcmotor_get_state(srv->dcmotor, &applied_actuation, &voltage);
53,713✔
115

116
    // Optionally log servo state.
117
    if (pbio_logger_is_active(&srv->log)) {
53,713✔
118

119
        // Get stall state
120
        bool stalled;
121
        uint32_t stall_duration;
122
        pbio_servo_is_stalled(srv, &stalled, &stall_duration);
×
123

124
        int32_t log_data[] = {
×
125
            // Column 0: Log time (added by logger).
126
            // Column 1: Current time.
127
            time_now,
128
            // Column 2: Motor angle in degrees.
129
            pbio_control_settings_ctl_to_app_long(&srv->control.settings, &state.position),
×
130
            // Column 3: Motor speed in degrees/second.
131
            pbio_control_settings_ctl_to_app(&srv->control.settings, state.speed),
×
132
            // Column 4: Actuation type (LSB 0--1), stall state (LSB 2).
133
            applied_actuation | ((int32_t)stalled << 2),
×
134
            // Column 5: Actuation voltage.
135
            voltage,
136
            // Column 6: Estimated position in degrees.
137
            pbio_control_settings_ctl_to_app_long(&srv->control.settings, &state.position_estimate),
×
138
            // Column 7: Estimated speed in degrees/second.
139
            pbio_control_settings_ctl_to_app(&srv->control.settings, state.speed_estimate),
×
140
            // Column 8: Feedback torque (uNm).
141
            feedback_torque,
142
            // Column 9: Feedforward torque (uNm).
143
            feedforward_torque,
144
            // Column 10: Observer error feedback voltage torque (mV).
145
            pbio_observer_get_feedback_voltage(&srv->observer, &state.position),
×
146
        };
147
        pbio_logger_add_row(&srv->log, log_data);
×
148
    }
149

150
    // Update the state observer
151
    pbio_observer_update(&srv->observer, time_now, &state.position, applied_actuation, voltage);
53,713✔
152

153
    return PBIO_SUCCESS;
53,713✔
154
}
155

156
/**
157
 * Updates the servo state and controller.
158
 *
159
 * This gets called once on every control loop.
160
 */
161
void pbio_servo_update_all(void) {
26,944✔
162
    pbio_error_t err;
163

164
    // Go through all motors.
165
    for (uint8_t i = 0; i < PBIO_CONFIG_SERVO_NUM_DEV; i++) {
188,608✔
166
        pbio_servo_t *srv = &servos[i];
161,664✔
167

168
        // Run update loop only if registered.
169
        if (srv->run_update_loop) {
161,664✔
170
            err = pbio_servo_update(srv);
53,713✔
171
            if (err != PBIO_SUCCESS) {
53,713✔
172
                // If the update failed, don't update it anymore.
173
                pbio_servo_update_loop_set_state(srv, false);
×
174

175
                // Coast the motor, letting errors pass.
176
                pbio_dcmotor_coast(srv->dcmotor);
×
177

178
                // Stop the control state.
179
                pbio_control_reset(&srv->control);
×
180

181
                // Stop higher level controls, such as drive bases.
182
                pbio_parent_stop(&srv->parent, false);
×
183
            }
184
        }
185
    }
186
}
26,944✔
187

188
// This function is attached to a dcmotor object, so it is able to
189
// stop the servo if the dcmotor needs to execute a new command.
190
static pbio_error_t pbio_servo_stop_from_dcmotor(void *servo, bool clear_parent) {
22✔
191

192
    // Specify pointer type.
193
    pbio_servo_t *srv = servo;
22✔
194

195
    // This external stop is triggered by a lower level peripheral,
196
    // i.e. the dc motor. So it has already has been stopped or changed state
197
    // electrically. All we have to do here is stop the control loop,
198
    // so it won't override the dcmotor to do something else.
199
    if (pbio_control_is_active(&srv->control)) {
22✔
200
        pbio_control_reset(&srv->control);
6✔
201

202
        // If we're not clearing the parent, we are done here. We don't want
203
        // to keep calling the drive base stop over and over.
204
        if (!clear_parent) {
6✔
205
            return PBIO_SUCCESS;
2✔
206
        }
207
    }
208

209
    // If servo control wasn't active, it's still possible that a higher
210
    // level abstraction is using this device. So call its stop as well.
211
    return pbio_parent_stop(&srv->parent, clear_parent);
20✔
212
}
213

214
#define DEG_TO_MDEG(deg) ((deg) * 1000)
215

216
/**
217
 * Loads all parameters of a servo to make it ready for use.
218
 *
219
 * @param [in]    srv                The servo instance.
220
 * @param [in]    type               The type of motor.
221
 * @param [in]    gear_ratio         Ratio that converts control units (mdeg) to user-defined output units (e.g. deg).
222
 * @param [in]    precision_profile  Position tolerance around target in degrees. Set to 0 to load default profile for this motor.
223
 * @return                           Error code.
224
 */
225
static pbio_error_t pbio_servo_initialize_settings(pbio_servo_t *srv, lego_device_type_id_t type, int32_t gear_ratio, int32_t precision_profile) {
23✔
226

227
    // Gear ratio must be strictly positive.
228
    if (gear_ratio < 1) {
23✔
UNCOV
229
        return PBIO_ERROR_INVALID_ARG;
×
230
    }
231

232
    // Get the minimal set of defaults for this servo type.
233
    const pbio_servo_settings_reduced_t *settings_reduced = pbio_servo_get_reduced_settings(type);
23✔
234
    if (!settings_reduced) {
23✔
UNCOV
235
        return PBIO_ERROR_NOT_SUPPORTED;
×
236
    }
237

238
    // If precision not given (if 0), use default value for this motor.
239
    if (precision_profile == 0) {
23✔
240
        precision_profile = settings_reduced->precision_profile;
23✔
241
    }
242

243
    // The tighter the tolerances, the higher the feedback values. This enforces
244
    // an upper limit on the feedback gains.
245
    if (precision_profile < 5) {
23✔
UNCOV
246
        return PBIO_ERROR_INVALID_ARG;
×
247
    }
248

249
    // Save reference to motor model.
250
    srv->observer.model = settings_reduced->model;
23✔
251

252
    // Initialize maximum torque as the stall torque for maximum voltage.
253
    // In practice, the nominal voltage is a bit lower than the 9V values.
254
    // REVISIT: Select nominal voltage based on battery type instead of 7500.
255
    int32_t max_voltage = pbio_dcmotor_get_max_voltage(type);
23✔
256
    int32_t nominal_voltage = pbio_int_math_min(max_voltage, 7500);
23✔
257
    int32_t nominal_torque = pbio_observer_voltage_to_torque(srv->observer.model, nominal_voltage);
23✔
258

259
    // Set all control settings.
260
    srv->control.settings = (pbio_control_settings_t) {
23✔
261
        // For a servo, counts per output unit is counts per degree at the gear train output
262
        .ctl_steps_per_app_step = gear_ratio,
263
        .stall_speed_limit = DEG_TO_MDEG(20),
264
        .stall_time = pbio_control_time_ms_to_ticks(200),
23✔
265
        .speed_max = DEG_TO_MDEG(settings_reduced->rated_max_speed),
23✔
266
        // The default speed is not used for servos currently (an explicit speed
267
        // is given for all run commands), so we initialize it to the maximum.
268
        .speed_default = DEG_TO_MDEG(settings_reduced->rated_max_speed),
23✔
269
        .speed_tolerance = DEG_TO_MDEG(50),
270
        .position_tolerance = DEG_TO_MDEG(precision_profile),
23✔
271
        .acceleration = DEG_TO_MDEG(2000),
272
        .deceleration = DEG_TO_MDEG(2000),
273
        .actuation_max = pbio_observer_voltage_to_torque(srv->observer.model, max_voltage),
23✔
274
        .actuation_max_temporary = pbio_observer_voltage_to_torque(srv->observer.model, max_voltage),
23✔
275
        // The nominal voltage is an indication for the nominal torque limit. To
276
        // ensure proportional control can always get the motor to within the
277
        // configured tolerance, we select pid_kp such that proportional feedback
278
        // just exceeds the nominal torque at the tolerance boundary.
279
        .pid_kp = nominal_torque / precision_profile,
23✔
280
        // Initialize ki such that integral control saturates in about two seconds
281
        // if the motor were stuck at the position tolerance.
282
        .pid_ki = nominal_torque / precision_profile / 2,
23✔
283
        // By default, the kd value is the same ratio of kp on all motors to
284
        // get a comparable step response. This value is not reduced along with
285
        // the user given precision profile like kp, so we use the default
286
        // value from the reduced settings.
287
        .pid_kd = nominal_torque / settings_reduced->precision_profile / 8,
23✔
288
        .pid_kp_low_pct = 50,
289
        .pid_kp_low_error_threshold = DEG_TO_MDEG(5),
290
        .pid_kp_low_speed_threshold = DEG_TO_MDEG(settings_reduced->pid_kp_low_speed_threshold),
23✔
291
        .integral_deadzone = DEG_TO_MDEG(8),
292
        .integral_change_max = DEG_TO_MDEG(15),
293
        .smart_passive_hold_time = pbio_control_time_ms_to_ticks(100),
23✔
294
    };
295

296
    // Initialize all observer settings.
297
    srv->observer.settings = (pbio_observer_settings_t) {
23✔
298
        .stall_speed_limit = srv->control.settings.stall_speed_limit,
23✔
299
        .stall_time = srv->control.settings.stall_time,
23✔
300
        .feedback_voltage_negligible = pbio_observer_torque_to_voltage(srv->observer.model, srv->observer.model->torque_friction) * 5 / 2,
23✔
301
        .feedback_voltage_stall_ratio = 75,
302
        .feedback_gain_low = settings_reduced->feedback_gain_low,
23✔
303
        .feedback_gain_high = settings_reduced->feedback_gain_low * 7,
23✔
304
        .feedback_gain_threshold = DEG_TO_MDEG(20),
305
        .coulomb_friction_speed_cutoff = 500,
306
    };
307

308
    // HACK: The general purpose minimized settings don't capture the stronger
309
    // motors very well. Generalize settings profile.
310
    #if PBIO_CONFIG_SERVO_EV3_NXT
311
    // REVISIT: Unify across all three motors since we can't have auto ID on nxt
312
    srv->control.settings.pid_kp = 50000;
23✔
313
    srv->control.settings.pid_ki = 18000;
23✔
314
    srv->control.settings.pid_kd = 2000;
23✔
315
    #endif
316

317
    return PBIO_SUCCESS;
23✔
318
}
319

320
/**
321
 * Sets up the servo instance to be used in an application.
322
 *
323
 * @param [in]  srv               The servo instance.
324
 * @param [in]  type              The type of motor.
325
 * @param [in]  direction         The direction of positive rotation.
326
 * @param [in]  gear_ratio        The ratio between motor rotation (millidegrees) and the gear train output (degrees).
327
 * @param [in]  reset_angle       If true, reset the current angle to the current absolute position if supported or 0.
328
 * @param [in]  precision_profile Position tolerance around target in degrees. Set to 0 to load default profile for this motor.
329
 * @return                        Error code.
330
 */
331
pbio_error_t pbio_servo_setup(pbio_servo_t *srv, lego_device_type_id_t type, pbio_direction_t direction, int32_t gear_ratio, bool reset_angle, int32_t precision_profile) {
23✔
332
    pbio_error_t err;
333

334
    // Unregister this servo from control loop updates.
335
    pbio_servo_update_loop_set_state(srv, false);
23✔
336

337
    // Configure tacho.
338
    err = pbio_tacho_setup(&srv->tacho, direction, reset_angle);
23✔
339
    if (err != PBIO_SUCCESS) {
23✔
UNCOV
340
        return err;
×
341
    }
342

343
    // Coast and configure dcmotors, and stop its parents, if any.
344
    err = pbio_dcmotor_setup(srv->dcmotor, type, direction);
23✔
345
    if (err != PBIO_SUCCESS) {
23✔
UNCOV
346
        return err;
×
347
    }
348
    // This servo will be the parent of this dcmotor
349
    pbio_parent_set(&srv->dcmotor->parent, srv, pbio_servo_stop_from_dcmotor);
23✔
350

351
    // Reset state
352
    pbio_control_reset(&srv->control);
23✔
353

354
    // Load default settings for this device type.
355
    err = pbio_servo_initialize_settings(srv, type, gear_ratio, precision_profile);
23✔
356
    if (err != PBIO_SUCCESS) {
23✔
UNCOV
357
        return err;
×
358
    }
359

360
    // Get current angle.
361
    pbio_angle_t angle;
362
    err = pbio_tacho_get_angle(&srv->tacho, &angle);
23✔
363
    if (err != PBIO_SUCCESS) {
23✔
UNCOV
364
        return err;
×
365
    }
366

367
    // Reset observer to current angle.
368
    pbio_observer_reset(&srv->observer, &angle);
23✔
369

370
    // Now that all checks have succeeded, we know that this motor is ready.
371
    // So we register this servo from control loop updates.
372
    pbio_servo_update_loop_set_state(srv, true);
23✔
373

374
    return PBIO_SUCCESS;
23✔
375
}
376

377
/**
378
 * Resets the servo angle to a given value.
379
 *
380
 * @param [in]  srv          The servo instance.
381
 * @param [in]  reset_angle  Angle that servo should now report in degrees.
382
 * @param [in]  reset_to_abs If true, ignores reset_angle and resets to absolute angle marked on shaft instead.
383
 * @return                   Error code.
384
 */
385
pbio_error_t pbio_servo_reset_angle(pbio_servo_t *srv, int32_t reset_angle, bool reset_to_abs) {
4✔
386

387
    // If we were busy moving or holding position, that means the reset was
388
    // called while a controller was running in the background. To avoid
389
    // confusion as to where the motor must go after the reset, we'll make it
390
    // stop and apply the configured stop mode right away.
391
    bool apply_stop = pbio_control_is_active(&srv->control);
4✔
392
    pbio_control_on_completion_t on_completion = srv->control.on_completion;
4✔
393

394
    // Get the current state so we can restore it after resetting if needed.
395
    pbio_dcmotor_actuation_t actuation;
396
    int32_t voltage;
397
    pbio_dcmotor_get_state(srv->dcmotor, &actuation, &voltage);
4✔
398

399
    // Stop servo and parents, if any.
400
    pbio_error_t err = pbio_servo_stop(srv, PBIO_CONTROL_ON_COMPLETION_COAST);
4✔
401
    if (err != PBIO_SUCCESS) {
4✔
UNCOV
402
        return err;
×
403
    }
404

405
    // Reset control state, including persistent on completion type.
406
    pbio_control_reset(&srv->control);
4✔
407

408
    // Get new angle in state units.
409
    pbio_angle_t new_angle;
410
    pbio_control_settings_app_to_ctl_long(&srv->control.settings, reset_angle, &new_angle);
4✔
411

412
    // Reset the tacho to the new angle. If reset_to_abs is true, the
413
    // new_angle will be an output, representing the angle it was set to.
414
    // This lets us use it again to reset the observer below.
415
    err = pbio_tacho_reset_angle(&srv->tacho, &new_angle, reset_to_abs);
4✔
416
    if (err != PBIO_SUCCESS) {
4✔
UNCOV
417
        return err;
×
418
    }
419

420
    // Reset observer to new angle.
421
    pbio_observer_reset(&srv->observer, &new_angle);
4✔
422

423
    // Apply user selected completion mode on stop if control was active during reset.
424
    if (apply_stop) {
4✔
425
        return pbio_servo_stop(srv, on_completion);
×
426
    }
427

428
    // Otherwise, restore brake or passive voltage.
429
    if (actuation == PBIO_DCMOTOR_ACTUATION_VOLTAGE) {
4✔
430
        return pbio_dcmotor_set_voltage(srv->dcmotor, voltage);
×
431
    }
432

433
    // Otherwise, we were coasting, so keep doing that.
434
    return PBIO_SUCCESS;
4✔
435
}
436

437
/**
438
 * Gets the servo state in units of control. This means millidegrees at the
439
 * motor output shaft, before any external gearing.
440
 *
441
 * @param [in]  srv         The servo instance.
442
 * @param [out] state       The system state object in units of control.
443
 * @return                  Error code.
444
 */
445
pbio_error_t pbio_servo_get_state_control(pbio_servo_t *srv, pbio_control_state_t *state) {
89,175✔
446

447
    pbio_error_t err;
448

449
    // Read physical angle.
450
    err = pbio_tacho_get_angle(&srv->tacho, &state->position);
89,175✔
451
    if (err != PBIO_SUCCESS) {
89,175✔
UNCOV
452
        return err;
×
453
    }
454

455
    // Get estimated state
456
    pbio_observer_get_estimated_state(&srv->observer, &state->speed, &state->position_estimate, &state->speed_estimate);
89,175✔
457

458
    return PBIO_SUCCESS;
89,175✔
459
}
460

461
/**
462
 * Gets the servo state in units of degrees at the output.
463
 *
464
 * @param [in]  srv         The servo instance.
465
 * @param [out] angle       Angle in degrees.
466
 * @param [out] speed       Angular velocity in degrees per second.
467
 * @return                  Error code.
468
 */
469
pbio_error_t pbio_servo_get_state_user(pbio_servo_t *srv, int32_t *angle, int32_t *speed) {
34✔
470

471
    // Don't allow user command if update loop not registered.
472
    if (!pbio_servo_update_loop_is_running(srv)) {
34✔
UNCOV
473
        return PBIO_ERROR_INVALID_OP;
×
474
    }
475

476
    // Get servo state.
477
    pbio_control_state_t state;
478
    pbio_error_t err = pbio_servo_get_state_control(srv, &state);
34✔
479
    if (err != PBIO_SUCCESS) {
34✔
UNCOV
480
        return err;
×
481
    }
482

483
    // Scale by gear ratio to whole degrees.
484
    *angle = pbio_control_settings_ctl_to_app_long(&srv->control.settings, &state.position);
34✔
485
    *speed = pbio_control_settings_ctl_to_app(&srv->control.settings, state.speed);
34✔
486
    return PBIO_SUCCESS;
34✔
487
}
488

489
/**
490
 * Gets the servo speed in units of degrees per second at the output, with
491
 * a given window size to control how smooth the speed differentiation is.
492
 *
493
 * @param [in]  srv         The servo instance.
494
 * @param [in]  window      Window size in milliseconds.
495
 * @param [out] speed       Calculated speed in degrees per second.
496
 * @return                  Error code.
497
 */
498
pbio_error_t pbio_servo_get_speed_user(pbio_servo_t *srv, uint32_t window, int32_t *speed) {
48✔
499
    pbio_error_t err = pbio_differentiator_get_speed(&srv->observer.differentiator, window, speed);
48✔
500
    if (err != PBIO_SUCCESS) {
48✔
UNCOV
501
        return err;
×
502
    }
503
    *speed = pbio_control_settings_ctl_to_app(&srv->control.settings, *speed);
48✔
504
    return PBIO_SUCCESS;
48✔
505
}
506

507
/**
508
 * Actuates the servo with a given control type and payload.
509
 *
510
 * This is an internal function used after servo or drive base control updates
511
 * and should not be called directly from external code.
512
 *
513
 * @param [in]  srv             The servo instance.
514
 * @param [in]  actuation_type  The type of actuation to apply.
515
 * @param [in]  payload         The control payload, such as the amount of torque.
516
 * @return                      Error code.
517
 */
518
pbio_error_t pbio_servo_actuate(pbio_servo_t *srv, pbio_dcmotor_actuation_t actuation_type, int32_t payload) {
47,716✔
519

520
    // Apply the calculated actuation, by type.
521
    switch (actuation_type) {
47,716✔
522
        case PBIO_DCMOTOR_ACTUATION_COAST:
58✔
523
            return pbio_dcmotor_coast(srv->dcmotor);
58✔
524
        case PBIO_DCMOTOR_ACTUATION_BRAKE:
×
525
            return pbio_dcmotor_set_voltage(srv->dcmotor, 0);
×
526
        case PBIO_DCMOTOR_ACTUATION_VOLTAGE:
×
527
            return pbio_dcmotor_set_voltage(srv->dcmotor, payload);
×
528
        case PBIO_DCMOTOR_ACTUATION_TORQUE:
47,658✔
529
            return pbio_dcmotor_set_voltage(srv->dcmotor, pbio_observer_torque_to_voltage(srv->observer.model, payload));
47,658✔
UNCOV
530
        default:
×
UNCOV
531
            return PBIO_ERROR_INVALID_ARG;
×
532
    }
533
}
534

535
/**
536
 * Stops ongoing controlled motion to coast, brake, or hold the servo.
537
 *
538
 * @param [in]  srv           The servo instance.
539
 * @param [in]  on_completion Coast, brake, or hold after stopping the controller.
540
 * @return                    Error code.
541
 */
542
pbio_error_t pbio_servo_stop(pbio_servo_t *srv, pbio_control_on_completion_t on_completion) {
49✔
543

544
    // Don't allow new user command if update loop not registered.
545
    if (!pbio_servo_update_loop_is_running(srv)) {
49✔
UNCOV
546
        return PBIO_ERROR_INVALID_OP;
×
547
    }
548

549
    // Stop parent object that uses this motor, if any.
550
    pbio_error_t err = pbio_parent_stop(&srv->parent, false);
49✔
551
    if (err != PBIO_SUCCESS) {
49✔
UNCOV
552
        return err;
×
553
    }
554

555
    // Handle HOLD case. Also enforce hold if the stop type was CONTINUE since
556
    // this function needs to make it stop in all cases.
557
    if (on_completion == PBIO_CONTROL_ON_COMPLETION_HOLD ||
49✔
558
        on_completion == PBIO_CONTROL_ON_COMPLETION_CONTINUE) {
559

560
        // Get current physical and estimated state.
561
        pbio_control_state_t state;
562
        err = pbio_servo_get_state_control(srv, &state);
×
563
        if (err != PBIO_SUCCESS) {
×
UNCOV
564
            return err;
×
565
        }
566

567
        // To hold, we first have to figure out which angle to hold.
568
        const pbio_angle_t *hold_target;
569
        pbio_trajectory_reference_t ref;
570
        if (pbio_control_is_active(&srv->control)) {
×
571
            // If control is active, hold at current target, so get it.
572
            uint32_t time = pbio_control_get_time_ticks();
×
573
            pbio_control_get_reference(&srv->control, time, &state, &ref);
×
574
            hold_target = &ref.position;
×
575
        } else {
576
            // If no control is ongoing, just hold measured state.
UNCOV
577
            hold_target = &state.position;
×
578
        }
579
        // Track the hold angle.
580
        return pbio_servo_track_target(srv, pbio_control_settings_ctl_to_app_long(&srv->control.settings, hold_target));
×
581
    }
582

583
    // All other stop modes are passive, so stop control and actuate accordingly.
584
    pbio_control_stop(&srv->control);
49✔
585
    return pbio_servo_actuate(srv, pbio_control_passive_completion_to_actuation_type(on_completion), 0);
49✔
586
}
587

588
/**
589
 * Runs the servo at a given speed and stops after a given duration or runs forever.
590
 *
591
 * @param [in]  srv            The servo instance.
592
 * @param [in]  speed          Angular velocity in degrees per second.
593
 * @param [in]  duration       Duration (ms) from start to becoming stationary again.
594
 * @param [in]  on_completion  What to do when the duration completes.
595
 * @return                     Error code.
596
 */
597
static pbio_error_t pbio_servo_run_time_common(pbio_servo_t *srv, int32_t speed, uint32_t duration, pbio_control_on_completion_t on_completion) {
9✔
598

599
    // Don't allow new user command if update loop not registered.
600
    if (!pbio_servo_update_loop_is_running(srv)) {
9✔
UNCOV
601
        return PBIO_ERROR_INVALID_OP;
×
602
    }
603

604
    // Stop parent object that uses this motor, if any.
605
    pbio_error_t err = pbio_parent_stop(&srv->parent, false);
9✔
606
    if (err != PBIO_SUCCESS) {
9✔
UNCOV
607
        return err;
×
608
    }
609

610
    // Get current time
611
    uint32_t time_now = pbio_control_get_time_ticks();
9✔
612

613
    // Read the physical and estimated state
614
    pbio_control_state_t state;
615
    err = pbio_servo_get_state_control(srv, &state);
9✔
616
    if (err != PBIO_SUCCESS) {
9✔
UNCOV
617
        return err;
×
618
    }
619

620
    // Start a timed maneuver.
621
    return pbio_control_start_timed_control(&srv->control, time_now, &state, duration, speed, on_completion);
9✔
622
}
623

624
/**
625
 * Starts running the servo at a given speed.
626
 *
627
 * @param [in]  srv             The servo instance.
628
 * @param [in]  speed           Angular velocity in degrees per second.
629
 * @return                      Error code.
630
 */
631
pbio_error_t pbio_servo_run_forever(pbio_servo_t *srv, int32_t speed) {
8✔
632
    // Start a timed maneuver and restart it when it is done, thus running forever.
633
    return pbio_servo_run_time_common(srv, speed, PBIO_TRAJECTORY_DURATION_FOREVER_MS, PBIO_CONTROL_ON_COMPLETION_CONTINUE);
8✔
634
}
635

636
/**
637
 * Runs the servo at a given speed and stops after a given duration.
638
 *
639
 * @param [in]  srv            The servo instance.
640
 * @param [in]  speed          Angular velocity in degrees per second.
641
 * @param [in]  duration       Duration (ms) from start to becoming stationary again.
642
 * @param [in]  on_completion  What to do when the duration completes.
643
 * @return                     Error code.
644
 */
645
pbio_error_t pbio_servo_run_time(pbio_servo_t *srv, int32_t speed, uint32_t duration, pbio_control_on_completion_t on_completion) {
1✔
646
    // Start a timed maneuver, duration specified by user.
647
    return pbio_servo_run_time_common(srv, speed, duration, on_completion);
1✔
648
}
649

650
/**
651
 * Runs the servo at a given speed until it stalls, then stops there.
652
 *
653
 * @param [in]  srv                 The servo instance.
654
 * @param [in]  speed               Angular velocity in degrees per second.
655
 * @param [in]  torque_limit        Maximum torque to use.
656
 * @param [in]  on_completion       What to do once stalled.
657
 * @return                          Error code.
658
 */
659
pbio_error_t pbio_servo_run_until_stalled(pbio_servo_t *srv, int32_t speed, int32_t torque_limit, pbio_control_on_completion_t on_completion) {
6✔
660

661
    if (on_completion == PBIO_CONTROL_ON_COMPLETION_CONTINUE) {
6✔
662
        // Can't continue after stall.
UNCOV
663
        return PBIO_ERROR_INVALID_ARG;
×
664
    }
665

666
    // Start an infinite maneuver.
667
    pbio_error_t err = pbio_servo_run_forever(srv, speed);
6✔
668
    if (err != PBIO_SUCCESS) {
6✔
UNCOV
669
        return err;
×
670
    }
671

672
    // Infinite maneuvers continue on completion, but here we want to set the
673
    // user specified on_completion behavior.
674
    srv->control.on_completion = on_completion;
6✔
675

676
    // We add the objective of stopping on stall.
677
    srv->control.type |= PBIO_CONTROL_TYPE_FLAG_OBJECTIVE_IS_STALL;
6✔
678

679
    // Set the temporary torque limit used during this maneuver.
680
    srv->control.settings.actuation_max_temporary = torque_limit;
6✔
681

682
    return PBIO_SUCCESS;
6✔
683
}
684

685
/**
686
 * Runs the servo at a given speed to a given target angle and stops there.
687
 *
688
 * The speed sign is ignored. It always goes in the direction needed to
689
 * read the @p target angle.
690
 *
691
 * @param [in]  srv            The control instance.
692
 * @param [in]  speed          Top angular velocity in degrees per second. If zero, servo is stopped.
693
 * @param [in]  target         Angle to run to.
694
 * @param [in]  on_completion  What to do after becoming stationary at the target angle.
695
 * @return                     Error code.
696
 */
697
pbio_error_t pbio_servo_run_target(pbio_servo_t *srv, int32_t speed, int32_t target, pbio_control_on_completion_t on_completion) {
24✔
698

699
    // Don't allow new user command if update loop not registered.
700
    if (!pbio_servo_update_loop_is_running(srv)) {
24✔
UNCOV
701
        return PBIO_ERROR_INVALID_OP;
×
702
    }
703

704
    // Stop parent object that uses this motor, if any.
705
    pbio_error_t err = pbio_parent_stop(&srv->parent, false);
24✔
706
    if (err != PBIO_SUCCESS) {
24✔
UNCOV
707
        return err;
×
708
    }
709

710
    // Get current time
711
    uint32_t time_now = pbio_control_get_time_ticks();
24✔
712

713
    // Read the physical and estimated state
714
    pbio_control_state_t state;
715
    err = pbio_servo_get_state_control(srv, &state);
24✔
716
    if (err != PBIO_SUCCESS) {
24✔
UNCOV
717
        return err;
×
718
    }
719

720
    // If the speed is zero, we can't do anything meaningful, but most users
721
    // find it confusing if we return an error. To make sure it won't block
722
    // forever, we run by a relative angle to zero, which is immediately done.
723
    if (speed == 0) {
24✔
724
        return pbio_servo_run_angle(srv, speed, 0, on_completion);
×
725
    }
726

727

728
    return pbio_control_start_position_control(&srv->control, time_now, &state, target, speed, on_completion);
24✔
729
}
730

731
/**
732
 * Runs the servo at a given speed by a given angle and stops there.
733
 *
734
 * The following convention is used for speed and angle signs:
735
 *
736
 *    Speed (+) with angle (+) gives forward (+)
737
 *    Speed (+) with angle (-) gives backward (-)
738
 *    Speed (-) with angle (+) gives backward (-)
739
 *    Speed (-) with angle (-) gives forward (+)
740
 *
741
 * @param [in]  srv            The control instance.
742
 * @param [in]  speed          Top angular velocity in degrees per second. If zero, servo is stopped.
743
 * @param [in]  angle          Angle to run by.
744
 * @param [in]  on_completion  What to do after becoming stationary at the final angle.
745
 * @return                     Error code.
746
 */
747
pbio_error_t pbio_servo_run_angle(pbio_servo_t *srv, int32_t speed, int32_t angle, pbio_control_on_completion_t on_completion) {
11✔
748

749
    // Don't allow new user command if update loop not registered.
750
    if (!pbio_servo_update_loop_is_running(srv)) {
11✔
UNCOV
751
        return PBIO_ERROR_INVALID_OP;
×
752
    }
753

754
    // Stop parent object that uses this motor, if any.
755
    pbio_error_t err = pbio_parent_stop(&srv->parent, false);
11✔
756
    if (err != PBIO_SUCCESS) {
11✔
UNCOV
757
        return err;
×
758
    }
759

760
    // Get current time.
761
    uint32_t time_now = pbio_control_get_time_ticks();
11✔
762

763
    // Read the physical and estimated state.
764
    pbio_control_state_t state;
765
    err = pbio_servo_get_state_control(srv, &state);
11✔
766
    if (err != PBIO_SUCCESS) {
11✔
UNCOV
767
        return err;
×
768
    }
769

770
    // If the speed is zero, we can't do anything meaningful, but most users
771
    // find it confusing if we return an error. To make sure it won't block
772
    // forever, we set the angle to zero instead, so we're "done" right away.
773
    if (speed == 0) {
11✔
774
        angle = 0;
×
775
    }
776

777
    // Start the relative angle control
778
    return pbio_control_start_position_control_relative(&srv->control, time_now, &state, angle, speed, on_completion, true);
11✔
779
}
780

781
/**
782
 * Steers the servo to the given target and holds it there.
783
 *
784
 * This is similar to pbio_servo_run_target when using hold on completion,
785
 * but it skips the smooth speed curve and immediately sets the reference
786
 * angle to the new target.
787
 *
788
 * @param [in]  srv            The control instance.
789
 * @param [in]  target         Angle to run to and keep tracking.
790
 * @return                     Error code.
791
 */
792
pbio_error_t pbio_servo_track_target(pbio_servo_t *srv, int32_t target) {
4✔
793

794
    // Don't allow new user command if update loop not registered.
795
    if (!pbio_servo_update_loop_is_running(srv)) {
4✔
UNCOV
796
        return PBIO_ERROR_INVALID_OP;
×
797
    }
798

799
    // Stop parent object that uses this motor, if any.
800
    pbio_error_t err = pbio_parent_stop(&srv->parent, false);
4✔
801
    if (err != PBIO_SUCCESS) {
4✔
UNCOV
802
        return err;
×
803
    }
804

805
    // Start hold command.
806
    return pbio_control_start_position_control_hold(&srv->control, pbio_control_get_time_ticks(), target);
4✔
807
}
808

809
/**
810
 * Checks whether servo is stalled. If the servo is actively controlled,
811
 * it is stalled when the controller cannot maintain the target speed or
812
 * position while using maximum allowed torque. If control is not active,
813
 * it uses the observer to estimate whether it is stalled.
814
 *
815
 * @param [in]  srv             The servo instance.
816
 * @param [out] stalled         True if servo is stalled, false if not.
817
 * @param [out] stall_duration  For how long it has been stalled (ms).
818
 * @return                      Error code.
819
 */
820
pbio_error_t pbio_servo_is_stalled(pbio_servo_t *srv, bool *stalled, uint32_t *stall_duration) {
14✔
821

822
    // Don't allow access if update loop not registered.
823
    if (!pbio_servo_update_loop_is_running(srv)) {
14✔
824
        *stalled = false;
×
825
        *stall_duration = 0;
×
826
        return PBIO_ERROR_INVALID_OP;
×
827
    }
828

829
    // If control is active, this provides the most accurate stall detection.
830
    if (pbio_control_is_active(&srv->control)) {
14✔
831
        *stalled = pbio_control_is_stalled(&srv->control, stall_duration);
6✔
832
        *stall_duration = pbio_control_time_ticks_to_ms(*stall_duration);
6✔
833
        return PBIO_SUCCESS;
6✔
834
    }
835

836
    // If we're coasting, we're not stalled.
837
    pbio_dcmotor_actuation_t actuation;
838
    int32_t voltage;
839
    pbio_dcmotor_get_state(srv->dcmotor, &actuation, &voltage);
8✔
840
    if (actuation == PBIO_DCMOTOR_ACTUATION_COAST) {
8✔
841
        *stall_duration = 0;
8✔
842
        *stalled = false;
8✔
843
        return PBIO_SUCCESS;
8✔
844
    }
845

846
    // If we're here, the user has set their own voltage or duty cycle value.
847
    // In this case, the best we can do is ask the observer if we're stuck.
848
    *stalled = pbio_observer_is_stalled(&srv->observer, pbio_control_get_time_ticks(), stall_duration);
×
849
    *stall_duration = pbio_control_time_ticks_to_ms(*stall_duration);
×
850

851
    return PBIO_SUCCESS;
×
852
}
853

854
/**
855
 * Gets estimated external load experienced by the servo.
856
 *
857
 * @param [in]  srv     The servo instance.
858
 * @param [out] load    Estimated load (mNm).
859
 * @return              Error code.
860
 */
861
pbio_error_t pbio_servo_get_load(pbio_servo_t *srv, int32_t *load) {
5✔
862

863
    // Don't allow access if update loop not registered.
864
    if (!pbio_servo_update_loop_is_running(srv)) {
5✔
865
        *load = 0;
×
866
        return PBIO_ERROR_INVALID_OP;
×
867
    }
868

869
    // Get passive actuation type.
870
    pbio_dcmotor_actuation_t applied_actuation;
871
    int32_t voltage;
872
    pbio_dcmotor_get_state(srv->dcmotor, &applied_actuation, &voltage);
5✔
873

874
    // Get best estimate based on control and physyical state.
875
    if (applied_actuation == PBIO_DCMOTOR_ACTUATION_COAST) {
5✔
876
        // Can't estimate load on coast.
877
        *load = 0;
2✔
878
    } else if (pbio_control_is_active(&srv->control)) {
3✔
879
        // The experienced load is the opposite sign of what the PID is
880
        // trying to overcome.
881
        *load = -srv->control.pid_average;
3✔
882
    } else {
883
        // Read the angle.
884
        pbio_angle_t angle;
885
        pbio_error_t err = pbio_tacho_get_angle(&srv->tacho, &angle);
×
886
        if (err != PBIO_SUCCESS) {
×
887
            return err;
×
888
        }
889
        // Use observer error as a measure of torque.
890
        int32_t feedback_voltage = pbio_observer_get_feedback_voltage(&srv->observer, &angle);
×
891
        *load = pbio_observer_voltage_to_torque(srv->observer.model, feedback_voltage);
×
892
    }
893

894
    // Convert to user torque units (mNm).
895
    *load = pbio_control_settings_actuation_ctl_to_app(*load);
5✔
896

897
    return PBIO_SUCCESS;
5✔
898
}
899

900
#endif // PBIO_CONFIG_SERVO
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