← Projects

F1TENTH: Automatic Emergency Braking with Time-to-Collision

C++ | ROS2 | NVIDIA Jetson Orin Nano | 2D LiDAR (270°, 40 Hz) | 2026 – Present

What I did: brought up a 1/10-scale autonomous car on an NVIDIA Jetson Orin Nano and wrote the automatic emergency braking (AEB) node in C++. It reads the 2D LiDAR scan and wheel odometry over ROS2, computes a time-to-collision (TTC) for every forward beam, and commands the motor controller to stop through Ackermann drive messages before the car hits anything.

RViz showing the F1TENTH gym simulator: the car in a corridor with its LaserScan returns drawn along both walls, colored by range
The F1TENTH gym simulator in RViz. The LiDAR scan lights up both walls, colored by range, which is the same scan the AEB node reads.

The idea: time-to-collision per beam

A distance threshold alone brakes too late at speed and too early when crawling. TTC asks a better question: at the current speed, how many seconds until I reach what this beam is seeing?

Each LiDAR beam i has a range r and a bearing θ. Only the part of the car's forward velocity vx that points along the beam closes the gap, so the range rate is:

ṙ = −vx cos θ    iTTC = r / max(−ṙ, 0)

The max(·, 0) matters: beams whose range is growing (things behind or beside the car) get an infinite TTC and never trigger a brake. The car brakes when the minimum TTC across the scan drops below a threshold, which I tuned against measured stopping distance on the real car.

The C++ node

The core of scan_callback from safety_node.cpp:

void scan_callback(const sensor_msgs::msg::LaserScan::ConstSharedPtr msg)
{
    double v = speed_.load();               // latest odom speed (atomic)
    bool should_brake = false;
    double braking_threshold = .5;          // seconds
    const double fov_half = .35;            // only look ~20 deg either side of forward

    for (size_t i {}; i < msg->ranges.size(); ++i) {
        double theta = msg->angle_min + i * msg->angle_increment;
        if (std::abs(theta) > fov_half) continue;   // ignore side beams
        double r = msg->ranges[i];
        if (!std::isfinite(r)) continue;             // drop inf / NaN returns

        double closing = v * std::cos(theta);        // -r_dot for this beam
        if (closing > 0.0) {                         // only beams we're closing on
            double ttc = r / closing;
            if (ttc < braking_threshold) {
                should_brake = true;
                break;
            }
        }
    }

    if (should_brake) {
        auto brake = ackermann_msgs::msg::AckermannDriveStamped();
        brake.drive.speed = 0.0;
        publisher_->publish(brake);           // /drive -> motor controller
    }
}

Engineering details I had to get right

View the code on GitHub