Six ROS 2 nodes, one 115200-baud serial line and four Arduino Megas. The interesting design decision is that autonomy never gets its own channel — it impersonates the driver.
The detector never talks to hardware and the bridge never touches the camera. They meet on two
Float32 topics, which makes either side independently testable — you can publish
/value_x by hand and watch the robot move.
/basket_distance and
/detection_image are published for humans and rosbag, not consumed by the bridge —
the control signal is only the two Float32 topics.
There is no framing, no checksum and no acknowledgement. A packet is the axis list, a semicolon,
the button list and a newline. The Arduino accumulates printable ASCII until it sees
\n, then splits on the first ;.
smart_drive_bridge overwrites them with PID output before the
line is written.
Because the firmware then needs no autonomous code path at all — auto_mode only
changes which axes the motor mixer reads. The cost is that the driver's real stick
values keep streaming underneath, so the arbitration has to be exactly right on both ends.
controlMotors() is a single if/else chain. It is not a mixing matrix: each branch
writes an explicit direction bit and PWM value per wheel. Order matters — the first matching
branch wins, and rotation is only reachable when translation is exactly zero.
Four wheels on a cross: front and back share the X pair, left and right share the Y pair. In the diagonal branches all four run; on a straight move only one pair is energised.
Depth is aligned to colour before anything else, so a pixel in the YOLO box maps directly to a depth sample. Rather than trusting one pixel, the node takes an 11×11 patch at the box centre, drops zero returns, sorts, trims the extremes and averages the rest.
target_distance is 5.726 m, but the value actually
compared against is 5.726 × cos(55°) = 3.284 m — while the measured side of that
comparison had its cosine correction commented out. See
finding 1.
As committed, both controllers are pure proportional — the integral and derivative machinery in
PIDController runs but contributes nothing. With Kp = 0.5 and a ±0.5 clamp, the loop
saturates for any error above 1.0, so approach speed is constant until the last metre.
You can feel this in the alignment simulator.
An autonomous shot cannot start on vision alone. Override must be off and a human must have granted permission with button 8; the permission is then consumed by the shot and must be re-granted for the next one.
Rotate_angle()
steps 5° every 15 ms inside a while loop, and shoot() adds a 1000 ms
delay() — during which no serial is read. See finding 6.
| Wheel | Position | Bot 1 PWM | Bot 1 DIR | Bot 2 PWM | Bot 2 DIR |
|---|---|---|---|---|---|
| M1 | Front | 10 | 22 | 5 | 24 |
| M2 | Left | 8 | 26 | 7 | 28 |
| M3 | Back | 6 | 30 | 9 | 32 |
| M4 | Right | 4 | 34 | 11 | 36 |
| Function | Bot 1 PWM | Bot 1 DIR | Bot 2 PWM | Bot 2 DIR |
|---|---|---|---|---|
| Right flywheel | 9 | 22 | 10 | 20 |
| Left flywheel | 11 | 26 | 8 | 6 |
| Angle motor | 14 | 13 | 4 | 18 |
| Trigger servo | signal 5, rest 90° | signal 16, rest 100° | ||
| Speed presets | 100 / 120 / 150 | 50 / 80 / 120 | ||
| Index | Board | Action | Behaviour |
|---|---|---|---|
| b0 / b1 / b3 | Shoot | Speed preset 1 / 2 / 3 | Latches one mode, clears the others and resets the trim |
| b2 | Shoot | Stop | Motors off, servo back to rest |
| b7 | Drive | Auto-align toggle | Debounced 200 ms, toggled on both host and firmware |
| b8 | Shoot | Shoot permission | Refused while Override is on; consumed after each shot |
| b9 | Both | Override | boots ON Robot is inert until the driver clears it |
| b10 | Shoot | Manual fire | Only in manual mode; 250 ms pre-delay |
| b11 / b12 | Shoot | Trim ±10 PWM | Unbounded — no clamp on the accumulated trim |
| b13 / b14 | Shoot | Angle down / up | Held, blocked while any shoot mode is latched |