• 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

92.13
/lib/pbio/src/trajectory.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 <assert.h>
8
#include <stdbool.h>
9
#include <stdint.h>
10
#include <stdlib.h>
11

12
#include <pbio/angle.h>
13
#include <pbio/int_math.h>
14
#include <pbio/trajectory.h>
15

16
/**
17
 * For a single maneuver, the relative angle in millidegrees is capped at
18
 * half the numerical limit.
19
 */
20
#define ANGLE_MAX (INT32_MAX / 2)
21
#define assert_angle(th) (assert(pbio_int_math_abs((th)) < ANGLE_MAX))
22

23
/**
24
 * Speed is capped at 2000 deg/s (20000 ddeg/s), which is about twice the
25
 * rated speed for a typical motor. Some commands deal with speeds relative
26
 * to another, so allow up to double that amount.
27
 */
28
#define SPEED_MAX (20000)
29
#define assert_speed(w) (assert(pbio_int_math_abs((w)) <= SPEED_MAX + 1))
30
#define assert_speed_rel(w) (assert_speed(w / 2))
31

32
/**
33
 * Acceleration is capped at 20000 deg/s^2, which for a typical motor
34
 * means reaching the maximum speed in just one control sample. The
35
 * minimum sets an upper bound on the acceleration time.
36
 */
37
#define ACCELERATION_MAX (20000)
38
#define ACCELERATION_MIN (50)
39
#define ANGLE_ACCEL_MAX (SPEED_MAX * SPEED_MAX / ACCELERATION_MIN * (10 / 2))
40
#define assert_accel_small(a) (assert(pbio_int_math_abs((a)) <= ACCELERATION_MAX))
41
#define assert_accel_numerator(a) (assert(pbio_int_math_abs((a)) <= ACCELERATION_MAX && pbio_int_math_abs((a)) >= ACCELERATION_MIN))
42
#define assert_accel_angle(th) (assert(pbio_int_math_abs((th)) <= ANGLE_ACCEL_MAX))
43

44
/*
45
 * Time segment length is capped at the maximum angle divided by maximum speed,
46
 * and scaled to time ticks. This is about 536 seconds.
47
 * Acceleration time segments take at most as long as reaching the maximum
48
 * speed with the lowest possible acceleration
49
 */
50

51
#define TIME_MAX (ANGLE_MAX / (SPEED_MAX * 100) * 10000)
52
#define TIME_ACCEL_MAX (SPEED_MAX * 2 / ACCELERATION_MIN * 1000)
53
#define assert_time(t) (assert((t) >= 0 && (t) < TIME_MAX))
54
#define assert_accel_time(t) (assert((t) >= 0 && (t) < TIME_ACCEL_MAX))
55

56
/*
57
 * Position (mdeg) and time (1e-4 s) are the same as in control module.
58
 * But speed is in millidegrees/second in control units, but this module uses
59
 * decidegrees per second to keep the math numerically bounded.
60
 */
61

62
/**
63
 * Converts a speed from ddeg/s to mdeg/s.
64
 *
65
 * @param [in]  trajectory_speed    The speed in ddeg/s.
66
 * @returns                         The speed in mdeg/s.
67
 */
68
static int32_t to_control_speed(int32_t trajectory_speed) {
2,147,597,692✔
69
    return trajectory_speed * 100;
2,147,597,692✔
70
}
71

72
/**
73
 * Converts a speed from mdeg/s to ddeg/s and limits it to +/- ::SPEED_MAX.
74
 *
75
 * @param [in]  control_speed       The speed in mdeg/s.
76
 * @returns                         The speed in ddeg/s.
77
 */
78
static int32_t to_trajectory_speed(int32_t control_speed) {
607,593✔
79
    return pbio_int_math_clamp(control_speed / 100, SPEED_MAX);
607,593✔
80
}
81

82
/*
83
 * Acceleration is in millidegrees/second^2 in control units, but this module
84
 * uses deg/s^2 per second to keep the math numerically bounded.
85
 */
86

87
/**
88
 * Converts an acceleration from deg/s^2 to mdeg/s^2.
89
 *
90
 * @param [in]  trajectory_accel    The acceleration in deg/s^2.
91
 * @returns                         The acceleration in mdeg/s^2.
92
 */
93
static int32_t to_control_accel(int32_t trajectory_accel) {
2,147,559,745✔
94
    return trajectory_accel * 1000;
2,147,559,745✔
95
}
96

97
/**
98
 * Converts an acceleration from mdeg/s^2 to deg/s^2 and limits it to the range
99
 * ::ACCELERATION_MIN to ::ACCELERATION_MAX.
100
 *
101
 * @param [in]  control_accel       The acceleration in mdeg/s^2.
102
 * @returns                         The acceleration in deg/s^2.
103
 */
104
static int32_t to_trajectory_accel(int32_t control_accel) {
395,464✔
105
    return pbio_int_math_bind(control_accel / 1000, ACCELERATION_MIN, ACCELERATION_MAX);
395,464✔
106
}
107

108
/**
109
 * Converts time from unsigned (for use outside this file) to signed (used here).
110
 * @param [in]  time    Unsigned time value.
111
 * @returns             Signed time value.
112
 */
113
#define TO_TRAJECTORY_TIME(time) ((int32_t)(time))
114

115
/**
116
 * Converts time from signed (used here) to unsigned(for use outside this file).
117
 * @param [in]  time    Signed time value.
118
 * @returns             Unsigned time value.
119
 */
120
#define TO_CONTROL_TIME(time) ((uint32_t)(time))
121

122
/**
123
 * Validates that the given speed limit is within the numerically allowed range.
124
 *
125
 * @param [in] ctl_steps_per_app_step   Control units per application unit.
126
 * @param [in] speed                    Speed limit (application units, positive).
127
 * @return                              ::PBIO_SUCCESS on valid values.
128
 *                                      ::PBIO_ERROR_INVALID_ARG if any argument is outside the allowed range.
129
 */
130
pbio_error_t pbio_trajectory_validate_speed_limit(int32_t ctl_steps_per_app_step, int32_t speed) {
5✔
131

132
    // Speeds can be negative but speed *limit* must be a strictly positive value.
133
    const int32_t speed_min = to_control_speed(100) / ctl_steps_per_app_step;
5✔
134
    const int32_t speed_max = to_control_speed(SPEED_MAX) / ctl_steps_per_app_step;
5✔
135

136
    if (speed < speed_min || speed > speed_max) {
5✔
UNCOV
137
        return PBIO_ERROR_INVALID_ARG;
×
138
    }
139
    return PBIO_SUCCESS;
5✔
140
}
141

142
/**
143
 * Validates that the given acceleration is within the numerically allowed range.
144
 *
145
 * @param [in] ctl_steps_per_app_step   Control units per application unit.
146
 * @param [in] acceleration             Acceleration (application units, positive).
147
 * @return                              ::PBIO_SUCCESS on valid values.
148
 *                                      ::PBIO_ERROR_INVALID_ARG if any argument is outside the allowed range.
149
 */
150
pbio_error_t pbio_trajectory_validate_acceleration_limit(int32_t ctl_steps_per_app_step, int32_t acceleration) {
9✔
151
    const int32_t accel_min = to_control_accel(ACCELERATION_MIN) / ctl_steps_per_app_step;
9✔
152
    const int32_t accel_max = to_control_accel(ACCELERATION_MAX) / ctl_steps_per_app_step;
9✔
153
    if (acceleration < accel_min || acceleration > accel_max) {
9✔
154
        return PBIO_ERROR_INVALID_ARG;
1✔
155
    }
156
    return PBIO_SUCCESS;
8✔
157
}
158

159
/**
160
 * Reverses a trajectory.
161
 *
162
 * On return, @p trj has been modified to contain the reverse trajectory.
163
 *
164
 * @param trj [in]      An initalized trajectory to be reversed.
165
 */
166
static void reverse_trajectory(pbio_trajectory_t *trj) {
80,493✔
167
    // Negate positions, essentially flipping around starting point.
168
    trj->th1 = -trj->th1;
80,493✔
169
    trj->th2 = -trj->th2;
80,493✔
170
    trj->th3 = -trj->th3;
80,493✔
171

172
    // Negate speeds and accelerations
173
    trj->wu *= -1;
80,493✔
174
    trj->w0 *= -1;
80,493✔
175
    trj->w1 *= -1;
80,493✔
176
    trj->w3 *= -1;
80,493✔
177
    trj->a0 *= -1;
80,493✔
178
    trj->a2 *= -1;
80,493✔
179

180
    // Negate starting point data
181
    trj->start.speed *= -1;
80,493✔
182
    trj->start.acceleration *= -1;
80,493✔
183
}
80,493✔
184

185
/**
186
 * Populates the starting point of a trajectory based on user command.
187
 *
188
 * @param [out] start   An uninitialized trajectory reference point to hold the result.
189
 * @param [in]  c       The command to use.
190
 */
191
static void pbio_trajectory_set_start(pbio_trajectory_reference_t *start, const pbio_trajectory_command_t *c) {
292,733✔
192
    start->acceleration = 0;
292,733✔
193
    start->position = c->position_start;
292,733✔
194
    start->speed = c->speed_start;
292,733✔
195
    start->time = c->time_start;
292,733✔
196
}
292,733✔
197

198
/**
199
 * Initializes a trajectory struct based on a user command.
200
 *
201
 * @param [out] trj     An uninitialized trajectory to hold the result.
202
 * @param [in]  c       The command to use.
203
 */
204
void pbio_trajectory_make_constant(pbio_trajectory_t *trj, const pbio_trajectory_command_t *c) {
95,001✔
205

206
    // Almost everything will be zero, so just zero everything.
207
    *trj = (pbio_trajectory_t) {0};
95,001✔
208

209
    // Fill out starting point based on user command.
210
    pbio_trajectory_set_start(&trj->start, c);
95,001✔
211

212
    // Set speeds, scaled to ddeg/s.
213
    trj->w0 = trj->w1 = trj->wu = to_trajectory_speed(c->speed_target);
95,001✔
214
    trj->w3 = c->continue_running ? to_trajectory_speed(c->speed_target): 0;
95,001✔
215
}
95,001✔
216

217
/**
218
 * Gets the traversed angle when accelerating from one speed value to another.
219
 *
220
 * Divides speed^2 by acceleration*2, giving angle.
221
 *
222
 * @param [in]  w_end   The ending speed in ddeg/s.
223
 * @param [in]  w_start The starting speed in ddeg/s.
224
 * @param [in]  a       The acceleration in deg/s^2.
225
 * @returns             The angle in mdeg.
226
 */
227
static int32_t div_w2_by_a(int32_t w_end, int32_t w_start, int32_t a) {
859,885✔
228

229
    assert_accel_numerator(a);
859,885✔
230
    assert_speed(w_end);
859,885✔
231
    assert_speed(w_start);
859,885✔
232

233
    return pbio_int_math_mult_then_div(w_end * w_end - w_start * w_start, (10 / 2), a);
859,885✔
234
}
235

236
/**
237
 * Divides speed by acceleration, giving time.
238
 *
239
 * @param [in]  w       The speed in ddeg/s.
240
 * @param [in]  a       The acceleration in deg/s^2.
241
 * @returns             The time in s*10^-4.
242
 */
243
static int32_t div_w_by_a(int32_t w, int32_t a) {
431,695✔
244

245
    assert_accel_numerator(a);
431,695✔
246
    assert_speed_rel(w);
431,695✔
247

248
    return w * 1000 / a;
431,695✔
249
}
250

251
/**
252
 * Divides angle by time, giving speed.
253
 *
254
 * @param [in]  th      The angle in mdeg.
255
 * @param [in]  t       The time in s*10^-4.
256
 * @returns             The speed in ddeg/s.
257
 */
258
static int32_t div_th_by_t(int32_t th, int32_t t) {
29✔
259

260
    assert_time(t);
29✔
261
    assert_angle(th);
29✔
262

263
    if (pbio_int_math_abs(th) < INT32_MAX / 100) {
29✔
264
        return th * 100 / t;
29✔
265
    }
266
    return th / t * 100;
×
267
}
268

269
/**
270
 * Divides speed by time, giving acceleration.
271
 *
272
 * @param [in]  w       The speed in ddeg/s.
273
 * @param [in]  t       The time in s*10^-4.
274
 * @returns             The acceleration in deg/s^2.
275
 */
276
static int32_t div_w_by_t(int32_t w, int32_t t) {
57✔
277

278
    assert_time(t);
57✔
279
    assert_speed_rel(w);
57✔
280

281
    return w * 1000 / t;
57✔
282
}
283

284
/**
285
 * Divides angle by speed, giving time (s*10^-4).
286
 *
287
 * @param [in]  th      The angle in mdeg.
288
 * @param [in]  w       The speed in ddeg/s.
289
 * @returns             The time in s*10^-4.
290
 */
291
static int32_t div_th_by_w(int32_t th, int32_t w) {
298,486✔
292

293
    assert_angle(th);
298,486✔
294
    assert_speed_rel(w);
298,486✔
295

296
    return pbio_int_math_mult_then_div(th, 100, w);
298,486✔
297
}
298

299
/**
300
 * Multiplies speed by time, giving angle.
301
 *
302
 * @param [in]  w       The speed in ddeg/s.
303
 * @param [in]  t       The time in s*10^-4.
304
 * @returns             The angle in mdeg.
305
 */
306
static int32_t mul_w_by_t(int32_t w, int32_t t) {
1,461,217,934✔
307

308
    assert_time(t);
1,461,217,934✔
309
    assert_speed_rel(w);
1,461,217,934✔
310

311
    return pbio_int_math_mult_then_div(w, t, 100);
1,461,217,934✔
312
}
313

314
/**
315
 * Multiplies acceleration by time, giving speed.
316
 *
317
 * @param [in]  a       The acceleration in deg/s^2.
318
 * @param [in]  t       The time in s*10^-4.
319
 * @returns             The speed in ddeg/s.
320
 */
321
static int32_t mul_a_by_t(int32_t a, int32_t t) {
238,345,179✔
322

323
    assert_time(t);
238,345,179✔
324
    assert_accel_small(a);
238,345,179✔
325
    assert_accel_time(t);
238,345,179✔
326

327
    return pbio_int_math_mult_then_div(a, t, 1000);
238,345,179✔
328
}
329

330
/**
331
 * Multiplies acceleration by time^2/2, giving angle.
332
 *
333
 * @param [in]  a       The acceleration in deg/s^2.
334
 * @param [in]  t       The time in s*10^-4.
335
 * @returns             The angle in mdeg.
336
 */
337
static int32_t mul_a_by_t2(int32_t a, int32_t t) {
119,172,604✔
338

339
    assert_time(t);
119,172,604✔
340
    assert_accel_small(a);
119,172,604✔
341
    assert_accel_time(t);
119,172,604✔
342

343
    return mul_w_by_t(mul_a_by_t(a, t), t) / 2;
119,172,604✔
344
}
345

346
/**
347
 * Gets starting speed to reach end speed within given angle and acceleration.
348
 *
349
 * Inverse of div_w2_by_a().
350
 *
351
 * @param [in]  w_end   The ending speed in ddeg/s.
352
 * @param [in]  a       The acceleration in deg/s^2.
353
 * @param [in]  th      The angle in mdeg.
354
 * @returns             The starting speed in ddeg/s.
355
 */
356
static int32_t bind_w0(int32_t w_end, int32_t a, int32_t th) {
81,515✔
357

358
    assert_accel_small(a);
81,515✔
359
    assert_speed(w_end);
81,515✔
360
    assert((int64_t)pbio_int_math_abs(a) * (int64_t)pbio_int_math_abs(th) < INT32_MAX);
81,515✔
361

362
    // This is only called if the acceleration is not high enough to reach
363
    // the end speed within a certain angle. So only if the angle is smaller
364
    // than w2 / a / 2, which is safe.
365
    return pbio_int_math_sqrt(w_end * w_end + a * th / (10 / 2));
81,515✔
366
}
367

368
/**
369
 * Gets the intersection of two speed curves.
370
 *
371
 * Given a stationary starting and final angle, computes the intersection of
372
 * the speed curves if we accelerate up and down without a stationary speed phase.
373
 *
374
 * @param [in]  th3     The final angle in mdeg.
375
 * @param [in]  th0     The starting angle in mdeg.
376
 * @param [in]  a0      The "up" acceleration in deg/s^2.
377
 * @param [in]  a2      The "down" acceleration in deg/s^2.
378
 * @returns             The angle at which the two curves intersect in mdeg.
379
 */
380
static int32_t intersect_ramp(int32_t th3, int32_t th0, int32_t a0, int32_t a2) {
25,440✔
381

382
    assert_accel_angle(th3 - th0);
25,440✔
383

384
    // If angles are equal, that's where they intersect.
385
    // This avoids the acceleration ratio division, which
386
    // is needed when a2 == a0, which is allowed in this
387
    // scenario. This happens when the movement consists
388
    // of just deceleration the whole way.
389
    if (th3 == th0) {
25,440✔
390
        return th0;
342✔
391
    }
392

393
    assert_accel_numerator(a0);
25,098✔
394
    assert_accel_numerator(a2);
25,098✔
395
    assert(a0 != a2);
25,098✔
396

397
    return th0 + pbio_int_math_mult_then_div(th3 - th0, a2, a2 - a0);
25,098✔
398
}
399

400
/**
401
 * Computes a trajectory for a timed command assuming *positive* speed.
402
 *
403
 * @param [out] trj     An uninitialized trajectory to hold the result.
404
 * @param [in]  c       The command to use.
405
 * @returns             ::PBIO_ERROR_INVALID_ARG if the command duration is out of range or the resulting angle is too large,
406
 *                      ::PBIO_ERROR_FAILED if any of the calculated intervals are non-positive,
407
 *                      otherwise ::PBIO_SUCCESS.
408
 */
409
static pbio_error_t pbio_trajectory_new_forward_time_command(pbio_trajectory_t *trj, const pbio_trajectory_command_t *c) {
8,665✔
410

411
    // Return error for a long user-specified duration.
412
    if (c->duration > TIME_MAX) {
8,665✔
UNCOV
413
        return PBIO_ERROR_INVALID_ARG;
×
414
    }
415

416
    // Fill out starting point based on user command.
417
    pbio_trajectory_set_start(&trj->start, c);
8,665✔
418

419
    // Save duration.
420
    trj->t3 = TO_TRAJECTORY_TIME(c->duration);
8,665✔
421

422
    // Speed continues at target speed or goes to zero. And scale to ddeg/s.
423
    trj->w3 = c->continue_running ? to_trajectory_speed(c->speed_target) : 0;
8,665✔
424
    trj->w0 = to_trajectory_speed(c->speed_start);
8,665✔
425
    int32_t wt = (trj->wu = to_trajectory_speed(c->speed_target));
8,665✔
426
    int32_t accel = to_trajectory_accel(c->acceleration);
8,665✔
427
    int32_t decel = to_trajectory_accel(c->deceleration);
8,665✔
428

429
    // Return error if approximate angle too long.
430
    if (mul_w_by_t(wt, trj->t3) > ANGLE_MAX) {
8,665✔
UNCOV
431
        return PBIO_ERROR_INVALID_ARG;
×
432
    }
433

434
    // Bind initial speed to make solution feasible.
435
    if (div_w_by_a(trj->w0, accel) < -trj->t3) {
8,665✔
436
        trj->w0 = -mul_a_by_t(accel, trj->t3);
×
437
    }
438
    if (div_w_by_a(trj->w0 - trj->w3, pbio_int_math_max(accel, decel)) > trj->t3) {
8,665✔
439
        trj->w0 = trj->w3 + mul_a_by_t(pbio_int_math_max(accel, decel), trj->t3);
×
440
    }
441

442
    // Bind target speed to make solution feasible.
443
    if (div_w_by_a(wt - trj->w3, decel) > trj->t3) {
8,665✔
444
        wt = trj->w3 + mul_a_by_t(decel, trj->t3);
×
445
    }
446

447
    // Initial acceleration sign depends on initial speed. It accelerates if
448
    // the initial speed is less than the target speed. Otherwise it
449
    // decelerates. The equality case is intrinsically dealt with in the
450
    // nominal acceleration case down below.
451
    trj->a0 = trj->w0 < wt ? accel : -accel;
8,665✔
452

453
    // Since our maneuver is forward, final deceleration is always negative.
454
    trj->a2 = -decel;
8,665✔
455

456
    // Work with time intervals instead of absolute time. Read 'm' as '-'.
457
    int32_t t3mt2;
458
    int32_t t2mt1;
459

460
    // The in-phase completes at the point where it reaches the target speed.
461
    trj->t1 = div_w_by_a(wt - trj->w0, trj->a0);
8,665✔
462

463
    // The out-phase rotation is the time passed while slowing down.
464
    t3mt2 = div_w_by_a(trj->w3 - wt, trj->a2);
8,665✔
465

466
    // Get the time and speed for the constant speed phase.
467
    t2mt1 = trj->t3 - trj->t1 - t3mt2;
8,665✔
468
    trj->w1 = wt;
8,665✔
469

470
    // The aforementioned results are valid only if the computed time intervals
471
    // don't overlap, so t2mt1 must be positive.
472
    if (t2mt1 < 0) {
8,665✔
473
        // The result is invalid. Then we will not reach the target speed, but
474
        // begin decelerating at the point where the in/out phases intersect.
475
        // The valid result depends on whether there is an end speed or not.
476

477
        if (c->continue_running && trj->a0 > 0) {
×
478
            // If we have a nonzero final speed and initial positive
479
            // acceleration, the result can only be invalid if there is not
480
            // enough time to reach the final speed. Then, we just accelerate
481
            // the best we can in the available time.
482
            trj->t1 = trj->t3;
×
483
            t2mt1 = 0;
×
484
            trj->w1 = trj->w0 + mul_a_by_t(trj->a0, trj->t3);
×
485
            trj->w3 = trj->w1;
×
486
        } else {
487
            // Otherwise, we can just take the intersection of the accelerating
488
            // and decelerating ramps to find the angle at t1 = t2.
489
            assert(trj->a0 != trj->a2);
×
490
            trj->t1 = div_w_by_a(trj->w3 - trj->w0 - mul_a_by_t(trj->a2, trj->t3), trj->a0 - trj->a2);
×
491

492
            // There is no constant speed phase in this case.
493
            t2mt1 = 0;
×
494
            trj->w1 = trj->w0 + mul_a_by_t(trj->a0, trj->t1);
×
495
        }
496
    }
497

498
    // For numerically negligible speed differences (achieved in zero time),
499
    // set the initial speed equal to speed at t1. This ensures correct
500
    // behavior when evaluating the reference when time is 0.
501
    if (trj->t1 == 0) {
8,665✔
502
        trj->w0 = trj->w1;
1,059✔
503
    }
504

505
    // With the difference between t1 and t2 known, we know t2.
506
    trj->t2 = trj->t1 + t2mt1;
8,665✔
507

508
    // Corresponding angle values
509
    trj->th1 = div_w2_by_a(trj->w1, trj->w0, trj->a0);
8,665✔
510
    trj->th2 = trj->th1 + mul_w_by_t(trj->w1, t2mt1);
8,665✔
511
    trj->th3 = trj->th2 + div_w2_by_a(trj->w3, trj->w1, trj->a2);
8,665✔
512

513
    // Assert that all resulting time intervals are positive.
514
    if (trj->t1 < 0 || trj->t2 - trj->t1 < 0 || trj->t3 - trj->t2 < 0) {
8,665✔
515
        return PBIO_ERROR_FAILED;
×
516
    }
517
    return PBIO_SUCCESS;
8,665✔
518
}
519

520
/**
521
 * Computes a trajectory for an angle command assuming *positive* speed.
522
 *
523
 * @param [out] trj     An uninitialized trajectory to hold the result.
524
 * @param [in]  c       The command to use.
525
 * @returns             ::PBIO_ERROR_INVALID_ARG if the command duration is out of range,
526
 *                      ::PBIO_ERROR_FAILED if any of the calculated intervals are non-positive,
527
 *                      otherwise ::PBIO_SUCCESS.
528
 */
529
static pbio_error_t pbio_trajectory_new_forward_angle_command(pbio_trajectory_t *trj, const pbio_trajectory_command_t *c) {
189,067✔
530

531
    // Fill out starting point based on user command.
532
    pbio_trajectory_set_start(&trj->start, c);
189,067✔
533

534
    // Get angle to travel.
535
    trj->th3 = pbio_angle_diff_mdeg(&c->position_end, &c->position_start);
189,067✔
536

537
    // Speed continues at target speed or goes to zero. And scale to ddeg/s.
538
    trj->w3 = c->continue_running ? to_trajectory_speed(c->speed_target) : 0;
189,067✔
539
    trj->w0 = to_trajectory_speed(c->speed_start);
189,067✔
540
    int32_t wt = (trj->wu = to_trajectory_speed(c->speed_target));
189,067✔
541
    int32_t accel = to_trajectory_accel(c->acceleration);
189,067✔
542
    int32_t decel = to_trajectory_accel(c->deceleration);
189,067✔
543

544
    // Bind initial speed to make solution feasible. Do the larger-than check
545
    // using quadratic terms to avoid square root evaluations in most cases.
546
    // This is only needed for positive initial speed, because negative initial
547
    // speeds are always feasible in angle-based maneuvers.
548
    int32_t a_max = pbio_int_math_max(accel, decel);
189,067✔
549
    if (trj->w0 > 0 && div_w2_by_a(trj->w0, trj->w3, a_max) > trj->th3) {
189,067✔
550
        trj->w0 = bind_w0(trj->w3, a_max, trj->th3);
17,955✔
551
    }
552

553
    // Do the same check for the target speed, but now use the deceleration
554
    // magnitude as the only constraint. The target speed is always positive,
555
    // so we can omit that additional check here. We have the full angle (th3)
556
    // to slow down, plus any angle run backwards due to initial speed, if any.
557
    int32_t fwd_angle = trj->th3 - (trj->w0 > 0 ? 0 : div_w2_by_a(0, trj->w0, accel));
189,067✔
558
    if (div_w2_by_a(wt, trj->w3, decel) > fwd_angle) {
189,067✔
559
        wt = bind_w0(trj->w3, decel, fwd_angle);
19,912✔
560
    }
561

562
    // Get initial approximation of duration.
563
    int32_t t3_approx = div_th_by_w(fwd_angle, wt);
189,067✔
564
    if (trj->w0 < 0) {
189,067✔
565
        // Add time to dissipate initial speed. The case of forward
566
        // excess speed gets handled below by cutting down speed.
567
        t3_approx += div_w_by_a(-trj->w0, accel);
78,756✔
568
    }
569
    // Return error if maneuver would take too long.
570
    if (t3_approx > TIME_MAX) {
189,067✔
571
        return PBIO_ERROR_INVALID_ARG;
34,260✔
572
    }
573

574
    // Initial acceleration sign depends on initial speed. It accelerates if
575
    // the initial speed is less than the target speed. Otherwise it
576
    // decelerates. The equality case is intrinsically dealt with in the
577
    // nominal acceleration case down below.
578
    trj->a0 = trj->w0 < wt ? accel : -accel;
154,807✔
579

580
    // Since our maneuver is forward, final deceleration is always negative.
581
    trj->a2 = -decel;
154,807✔
582

583
    // Now we can evaluate the nominal cases and check if they hold. First get
584
    // a fictitious zero-speed angle for computational convenience. This is the
585
    // angle on the speed-angle phase plot where the in-phase square root
586
    // function intersects the zero speed axis if we kept (de)accelerating.
587
    int32_t thf = div_w2_by_a(0, trj->w0, trj->a0);
154,807✔
588

589
    // The in-phase completes at the point where it reaches the target speed.
590
    trj->th1 = thf + div_w2_by_a(wt, 0, trj->a0);
154,807✔
591

592
    // The out-phase rotation is the angle traveled while slowing down. But if
593
    // the end speed equals the target speed, there is no out-phase.
594
    trj->th2 = trj->th3 + div_w2_by_a(wt, trj->w3, trj->a2);
154,807✔
595

596
    // In the nominal case, we always reach the target speed, so start with:
597
    trj->w1 = wt;
154,807✔
598

599
    // The aforementioned results are valid only if
600
    // the computed angle th2 is indeed larger than th1.
601
    if (trj->th2 < trj->th1) {
154,807✔
602
        // The result is invalid. Then we will not reach the target speed, but
603
        // begin decelerating at the point where the in/out phases intersect.
604

605
        if (c->continue_running && trj->a0 > 0) {
43,648✔
606
            // If we have a nonzero final speed and initial positive
607
            // acceleration, the result can only be invalid if there is not
608
            // enough time to reach the final speed. Then, we just accelerate
609
            // the best we can in the available rotation.
610
            trj->w1 = bind_w0(0, trj->a0, trj->th3 - thf);
13,891✔
611
            trj->th1 = trj->th3;
13,891✔
612
            trj->th2 = trj->th1;
13,891✔
613

614
            // The final speed is not reached either, so reaching
615
            // w1 is the best we can do in the given time.
616
            trj->w3 = trj->w1;
13,891✔
617
        } else if (c->continue_running && trj->a0 < 0) {
29,757✔
618
            // If we have a nonzero final speed and initial negative
619
            // acceleration, we are going too fast and we would overshoot
620
            // the target before having slowed down. So we need to reduce the
621
            // initial speed to be able to reach it.
622
            trj->w0 = bind_w0(trj->w3, -trj->a0, trj->th3);
4,317✔
623
            trj->w1 = trj->w3;
4,317✔
624
            trj->th1 = trj->th3;
4,317✔
625
            trj->th2 = trj->th3;
4,317✔
626
        } else {
627
            // Otherwise we have a zero final speed. We can just take the
628
            // intersection of the accelerating and decelerating ramps to find
629
            // the speed at t1 = t2.
630
            trj->th1 = intersect_ramp(trj->th3, thf, trj->a0, trj->a2);
25,440✔
631
            trj->th2 = trj->th1;
25,440✔
632
            trj->w1 = bind_w0(0, trj->a0, trj->th1 - thf);
25,440✔
633

634
            // If w0 and w1 are very close, the previously determined
635
            // acceleration sign may be wrong after rounding errors, so update.
636
            trj->a0 = trj->w0 < trj->w1 ? accel : -accel;
25,440✔
637
        }
638
    }
639

640
    // Constant speed phase duration, if any.
641
    int32_t t2mt1 = trj->th2 == trj->th1 ? 0 : div_th_by_w(trj->th2 - trj->th1, trj->w1);
154,807✔
642

643
    // With the intermediate angles and speeds now known, we can calculate the
644
    // corresponding durations to match.
645
    trj->t1 = div_w_by_a(trj->w1 - trj->w0, trj->a0);
154,807✔
646

647
    // For numerically negligible speed differences (achieved in zero time),
648
    // set the initial speed equal to speed at t1. This ensures correct
649
    // behavior when evaluating the reference when time is 0.
650
    if (trj->t1 == 0) {
154,807✔
651
        trj->w0 = trj->w1;
11,926✔
652
    }
653

654
    trj->t2 = trj->t1 + t2mt1;
154,807✔
655
    trj->t3 = trj->t2 + div_w_by_a(trj->w3 - trj->w1, trj->a2);
154,807✔
656

657
    // Ensure computed motion points are ordered.
658
    if (trj->th2 < trj->th1 || trj->th3 < trj->th2) {
154,807✔
UNCOV
659
        return PBIO_ERROR_FAILED;
×
660
    }
661

662
    // Assert times are valid.
663
    assert_time(trj->t1);
154,807✔
664
    assert_time(trj->t2);
154,807✔
665
    assert_time(trj->t3);
154,807✔
666
    assert_time(trj->t2 - trj->t1);
154,807✔
667
    assert_time(trj->t3 - trj->t2);
154,807✔
668
    if (trj->t1 < 0 || trj->t2 - trj->t1 < 0 || trj->t3 - trj->t2 < 0) {
154,807✔
669
        return PBIO_ERROR_FAILED;
×
670
    }
671
    return PBIO_SUCCESS;
154,807✔
672
}
673

674
/**
675
 * Stretches a trajectory to end at the same time as @p leader.
676
 *
677
 * @param [in]  trj     An initalized trajectory to be modified.
678
 * @param [in]  leader  The trajectory to follow.
679
 */
680
void pbio_trajectory_stretch(pbio_trajectory_t *trj, const pbio_trajectory_t *leader) {
30✔
681

682
    // Synchronize timestamps with leading trajectory.
683
    trj->t1 = leader->t1;
30✔
684
    trj->t2 = leader->t2;
30✔
685
    trj->t3 = leader->t3;
30✔
686

687
    if (trj->t3 == 0) {
30✔
688
        // This is a stationary maneuver, so there's nothing to recompute.
689
        return;
1✔
690
    }
691

692
    // This recomputes several components of a trajectory such that it travels
693
    // the same distance as before, but with new time stamps t1, t2, and t3.
694
    // Setting the speed integral equal to (th3 - th0) gives three constraint
695
    // equations with three unknowns (a0, a2, wt), for which we can solve.
696

697
    // Solve constraint to find peak velocity
698
    trj->w1 = div_th_by_t(2 * trj->th3 - mul_w_by_t(trj->w0, trj->t1) - mul_w_by_t(trj->w3, trj->t3 - trj->t2),
29✔
699
        trj->t3 + trj->t2 - trj->t1);
29✔
700

701
    // Get corresponding accelerations
702
    trj->a0 = trj->t1 == 0 ? 0 : div_w_by_t(trj->w1 - trj->w0, trj->t1);
29✔
703
    trj->a2 = (trj->t3 - trj->t2) == 0 ? 0 : div_w_by_t(trj->w3 - trj->w1, trj->t3 - trj->t2);
29✔
704

705
    // Since the target speed may have been lowered, we need to adjust w3 too.
706
    trj->w3 = (trj->t3 - trj->t2) == 0 ? trj->w1 : 0;
29✔
707

708
    // With all constraints already satisfied, we can just compute the
709
    // intermediate positions relative to the endpoints, given the now-known
710
    // accelerations and speeds.
711
    trj->th1 = mul_w_by_t(trj->w0, trj->t1) + mul_a_by_t2(trj->a0, trj->t1);
29✔
712
    trj->th2 = trj->th1 + mul_w_by_t(trj->w1, trj->t2 - trj->t1);
29✔
713
}
714

715
/**
716
 * Computes a trajectory for a timed command.
717
 *
718
 * @param [out] trj     An uninitialized trajectory to hold the result.
719
 * @param [in]  command The command to use.
720
 * @returns             ::PBIO_ERROR_INVALID_ARG if the command duration is out of range or the resulting angle is too large,
721
 *                      ::PBIO_ERROR_FAILED if any of the calculated intervals are non-positive,
722
 *                      otherwise ::PBIO_SUCCESS.
723
 */
724
pbio_error_t pbio_trajectory_new_time_command(pbio_trajectory_t *trj, const pbio_trajectory_command_t *command) {
8,665✔
725

726
    // Copy the command so we can modify it.
727
    pbio_trajectory_command_t c = *command;
8,665✔
728

729
    // Return empty maneuver for zero time
730
    if (c.duration == 0) {
8,665✔
731
        c.speed_target = 0;
×
732
        c.continue_running = false;
×
733
        pbio_trajectory_make_constant(trj, &c);
×
734
        return PBIO_SUCCESS;
×
735
    }
736

737
    // Remember if the original user-specified maneuver was backward.
738
    bool backward = c.speed_target < 0;
8,665✔
739

740
    // Convert user command into a forward maneuver to simplify computations.
741
    if (backward) {
8,665✔
742
        c.speed_target *= -1;
2,888✔
743
        c.speed_start *= -1;
2,888✔
744
    }
745

746
    // Bind target speed by maximum speed.
747
    c.speed_target = pbio_int_math_min(c.speed_target, c.speed_max);
8,665✔
748

749
    // Calculate the trajectory, assumed to be forward.
750
    pbio_error_t err = pbio_trajectory_new_forward_time_command(trj, &c);
8,665✔
751
    if (err != PBIO_SUCCESS) {
8,665✔
UNCOV
752
        return err;
×
753
    }
754

755
    // Reverse the maneuver if the original arguments imposed backward motion.
756
    if (backward) {
8,665✔
757
        reverse_trajectory(trj);
2,888✔
758
    }
759
    return PBIO_SUCCESS;
8,665✔
760
}
761

762
/**
763
 * Computes a trajectory for an angle command.
764
 *
765
 * @param [out] trj     An uninitialized trajectory to hold the result.
766
 * @param [in]  command The command to use.
767
 * @returns             ::PBIO_ERROR_INVALID_ARG if the command duration is out of range or the angle is too large,
768
 *                      ::PBIO_ERROR_FAILED if any of the calculated intervals are non-positive,
769
 *                      otherwise ::PBIO_SUCCESS.
770
 */
771
pbio_error_t pbio_trajectory_new_angle_command(pbio_trajectory_t *trj, const pbio_trajectory_command_t *command) {
345,701✔
772

773
    // Copy the command so we can modify it.
774
    pbio_trajectory_command_t c = *command;
345,701✔
775

776
    // Return error for maneuver that is too long by angle.
777
    if (!pbio_angle_diff_is_small(&c.position_end, &c.position_start)) {
345,701✔
778
        return PBIO_ERROR_INVALID_ARG;
75,600✔
779
    }
780

781
    // Travel distance.
782
    int32_t distance = pbio_angle_diff_mdeg(&c.position_end, &c.position_start);
270,101✔
783

784
    // Return empty maneuver for zero angle or zero speed
785
    if (c.speed_target == 0 || distance == 0) {
270,101✔
786
        c.speed_target = 0;
81,034✔
787
        c.continue_running = false;
81,034✔
788
        pbio_trajectory_make_constant(trj, &c);
81,034✔
789
        return PBIO_SUCCESS;
81,034✔
790
    }
791

792
    // Direction is solely defined in terms of sign of the angle th3.
793
    // For speed, only the *magnitude* is relevant. Certain end-user APIs
794
    // allow specifying physically impossible scenarios like negative speed
795
    // with a positive relative position. Those cases are not handled here and
796
    // should be appropriately handled at higher levels.
797
    c.speed_target = pbio_int_math_abs(c.speed_target);
189,067✔
798

799
    // Bind target speed by maximum speed.
800
    c.speed_target = pbio_int_math_min(c.speed_target, c.speed_max);
189,067✔
801

802
    // Check if the original user-specified maneuver is backward.
803
    bool backward = distance < 0;
189,067✔
804

805
    // Convert user command into a forward maneuver to simplify computations.
806
    if (backward) {
189,067✔
807
        c.position_end = c.position_start;
94,525✔
808
        pbio_angle_add_mdeg(&c.position_end, -distance);
94,525✔
809
        c.speed_start *= -1;
94,525✔
810
    }
811

812
    // Calculate the trajectory, assumed to be forward.
813
    pbio_error_t err = pbio_trajectory_new_forward_angle_command(trj, &c);
189,067✔
814
    if (err != PBIO_SUCCESS) {
189,067✔
815
        return err;
34,260✔
816
    }
817

818
    // Reverse the maneuver if the original arguments imposed backward motion.
819
    if (backward) {
154,807✔
820
        reverse_trajectory(trj);
77,605✔
821
    }
822

823
    return PBIO_SUCCESS;
154,807✔
824
}
825

826
/**
827
 * Populates reference point with the right units and offset.
828
 *
829
 * @param [out] ref     An uninitialized trajectory reference point to hold the result.
830
 * @param [in]  start   The starting trajectory reference point.
831
 * @param [in]  t       The time in s*10^-4.
832
 * @param [in]  th      The angle in mdeg.
833
 * @param [in]  w       The rotational speed in ddeg/s.
834
 * @param [in]  a       The acceleration in deg/s^2.
835
 */
836
static void pbio_trajectory_offset_start(pbio_trajectory_reference_t *ref, const pbio_trajectory_reference_t *start, int32_t t, int32_t th, int32_t w, int32_t a) {
2,147,559,737✔
837

838
    // Convert local trajectory units to global pbio units.
839
    ref->time = TO_CONTROL_TIME(t) + start->time;
2,147,559,737✔
840
    ref->speed = to_control_speed(w);
2,147,559,737✔
841
    ref->acceleration = to_control_accel(a);
2,147,559,737✔
842

843
    // Reference position is starting point plus progress in this maneuver.
844
    ref->position = start->position;
2,147,559,737✔
845
    pbio_angle_add_mdeg(&ref->position, th);
2,147,559,737✔
846
}
2,147,559,737✔
847

848
/**
849
 * Gets the last vertex of a trajectory before a given point in time.
850
 *
851
 * @param [in]  trj         The trajectory instance.
852
 * @param [in]  time_ref    The duration of time after the start of the trajectory in s*10^-4.
853
 * @param [out] vertex      An uninitialized trajectory reference point to hold the result.
854
 */
855
void pbio_trajectory_get_last_vertex(const pbio_trajectory_t *trj, uint32_t time_ref, pbio_trajectory_reference_t *vertex) {
2,147,483,711✔
856

857
    // Relative time within ongoing maneuver.
858
    int32_t time = TO_TRAJECTORY_TIME(time_ref - trj->start.time);
2,147,483,711✔
859
    assert_time(time);
2,147,483,711✔
860

861
    // Find which section of the ongoing maneuver we were in, and take
862
    // corresponding segment starting point. Acceleration is undefined but not
863
    // used when synchronizing trajectories, so set to zero.
864
    if (time - trj->t1 < 0 || (trj->t1 == 0 && time == 0)) {
2,147,483,711✔
865
        // Acceleration segment.
866
        pbio_trajectory_offset_start(vertex, &trj->start, 0, 0, trj->w0, 0);
192,050,031✔
867
    } else if (time - trj->t2 < 0) {
2,147,483,711✔
868
        // Constant speed segment.
869
        pbio_trajectory_offset_start(vertex, &trj->start, trj->t1, trj->th1, trj->w1, 0);
2,147,483,648✔
870
    } else if (time - trj->t3 < 0) {
93,444,738✔
871
        // Deceleration segment.
872
        pbio_trajectory_offset_start(vertex, &trj->start, trj->t2, trj->th2, trj->w1, 0);
45,157,914✔
873
    } else {
874
        // Final speed segment.
875
        pbio_trajectory_offset_start(vertex, &trj->start, trj->t3, trj->th3, trj->w3, 0);
48,286,824✔
876
    }
877
}
2,147,483,711✔
878

879
/**
880
 * Gets the trajectory endpoint.
881
 *
882
 * @param [in]  trj         The trajectory instance.
883
 * @param [out] end         An uninitialized trajectory reference point to hold the result.
884
 */
885
void pbio_trajectory_get_endpoint(const pbio_trajectory_t *trj, pbio_trajectory_reference_t *end) {
283,426✔
886
    pbio_trajectory_offset_start(end, &trj->start, trj->t3, trj->th3, trj->w3, 0);
283,426✔
887
}
283,426✔
888

889
/**
890
 * Gets the originally commanded speed for the current trajectory, even if
891
 * this top speed is not reached.
892
 *
893
 * This is an indicator of the expected speed, which may be used to to alter
894
 * control behavior for motion with sustained low speed values.
895
 *
896
 * @param [in]  trj         The trajectory instance.
897
 * @return                  The user given speed in control units (mdeg/s).
898
 */
899
int32_t pbio_trajectory_get_abs_command_speed(const pbio_trajectory_t *trj) {
47,669✔
900
    return to_control_speed(pbio_int_math_abs(trj->wu));
47,669✔
901
}
902

903
/**
904
 * Gets the trajectory duration.
905
 *
906
 * The duration is the time at when the endpoint is expected to occur.
907
 *
908
 * @param [in]  trj         The trajectory instance.
909
 * @return                  The duration of time after the start of the trajectory in s*10^-4.
910
 */
911
uint32_t pbio_trajectory_get_duration(const pbio_trajectory_t *trj) {
248,521✔
912
    assert_time(trj->t3);
248,521✔
913
    return TO_CONTROL_TIME(trj->t3);
248,521✔
914
}
915

916
/**
917
 * Gets the calculated reference speed and velocity of the trajectory at the (shifted) time.
918
 *
919
 * @param [in]  trj         The trajectory instance.
920
 * @param [in]  time_ref    The duration of time after the start of the trajectory in s*10^-4.
921
 * @param [out] ref         An uninitialized trajectory reference point to hold the result.
922
 */
923
void pbio_trajectory_get_reference(pbio_trajectory_t *trj, uint32_t time_ref, pbio_trajectory_reference_t *ref) {
1,342,027,884✔
924

925
    // Time within maneuver since start.
926
    int32_t time = TO_TRAJECTORY_TIME(time_ref - trj->start.time);
1,342,027,884✔
927
    assert_time(time);
1,342,027,884✔
928

929
    // Get angle, speed, and acceleration along reference
930
    int32_t th;
931
    int32_t w;
932
    int32_t a;
933

934
    if (time - trj->t1 < 0 || (trj->t1 == 0 && time == 0)) {
1,342,027,884✔
935
        // If we are here, then we are still in the acceleration phase.
936
        // Includes conversion from microseconds to seconds, in two steps to
937
        // avoid overflows and round off errors
938
        w = trj->w0 + mul_a_by_t(trj->a0, time);
96,588,734✔
939
        th = mul_w_by_t(trj->w0, time) + mul_a_by_t2(trj->a0, time);
96,588,734✔
940
        a = trj->a0;
96,588,734✔
941
    } else if (time - trj->t2 < 0) {
1,245,439,150✔
942
        // If we are here, then we are in the constant speed phase
943
        w = trj->w1;
1,198,597,149✔
944
        th = trj->th1 + mul_w_by_t(trj->w1, time - trj->t1);
1,198,597,149✔
945
        a = 0;
1,198,597,149✔
946
    } else if (time - trj->t3 < 0) {
46,842,001✔
947
        // If we are here, then we are in the deceleration phase
948
        w = trj->w1 + mul_a_by_t(trj->a2, time - trj->t2);
22,583,841✔
949
        th = trj->th2 + mul_w_by_t(trj->w1, time - trj->t2) + mul_a_by_t2(trj->a2, time - trj->t2);
22,583,841✔
950
        a = trj->a2;
22,583,841✔
951
    } else {
952
        // If we are here, we are in the constant speed phase after the
953
        // maneuver completes
954
        w = trj->w3;
24,258,160✔
955
        th = trj->th3 + mul_w_by_t(trj->w3, time - trj->t3);
24,258,160✔
956
        a = 0;
24,258,160✔
957

958
        // To avoid any overflows of the aforementioned time comparisons,
959
        // rebase the trajectory if it has been running a long time.
960
        if (time > PBIO_TRAJECTORY_DURATION_FOREVER_MS * PBIO_TRAJECTORY_TICKS_PER_MS) {
24,258,160✔
961
            pbio_angle_t start = trj->start.position;
13,962✔
962
            pbio_angle_add_mdeg(&start, th);
13,962✔
963

964
            pbio_trajectory_command_t command = {
27,924✔
965
                .time_start = time_ref,
966
                .speed_target = to_control_speed(trj->w3),
13,962✔
967
                .continue_running = true,
968
                .position_start = start,
969
            };
970
            pbio_trajectory_make_constant(trj, &command);
13,962✔
971

972
            // w, and a are already set above. Time and angle are 0, since this
973
            // is the start of the new maneuver with its new starting point.
974
            time = 0;
13,962✔
975
            th = 0;
13,962✔
976
        }
977
    }
978

979
    // Assert that results are bounded
980
    assert_time(time);
1,342,027,884✔
981
    assert_angle(th);
1,342,027,884✔
982
    assert_speed(w);
1,342,027,884✔
983

984
    // Convert back to absolute points by adding starting point.
985
    pbio_trajectory_offset_start(ref, &trj->start, time, th, w, a);
1,342,027,884✔
986
}
1,342,027,884✔
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