Files
clearpilot/selfdrive/clearpilot/dashcamd.cc
Brian Hanson b85ce8176d modeld standby on lat inactive, dashcamd always-on, alert suppression
- modeld: enter standby when latActive=false (not just standstill),
  exception for lane changes (no_lat_lane_change). Fix Python capnp
  property access (.latActive not getLatActive())
- controlsd: move model_suppress computation early, suppress radarFault,
  posenetInvalid, locationdTemporaryError, paramsdTemporaryError during
  model standby + 2s grace period. All cascade from modeld not publishing
- dashcamd: always_run (manages own trip lifecycle), wait for valid frame
  dimensions before encoding (fix SIGSEGV on early start)
- Fan: driving range 15-100% (was 30-100%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 02:13:40 -05:00

397 lines
13 KiB
C++

/*
* CLEARPILOT dashcamd — records raw camera footage to MP4 using OMX H.264 hardware encoder.
*
* Connects to camerad via VisionIPC, receives NV12 frames, and feeds them directly
* to the Qualcomm OMX encoder. Produces 3-minute MP4 segments organized by trip.
*
* Trip directory structure:
* /data/media/0/videos/YYYYMMDD-HHMMSS/ (trip directory, named at trip start)
* YYYYMMDD-HHMMSS.mp4 (3-minute segments)
*
* Trip lifecycle state machine:
*
* On process start (after time-valid wait):
* - Create trip directory, start recording immediately with 10-min idle timer
* - If car is already in drive, timer is cancelled and recording continues
* - If car stays parked/off for 10 minutes, trip ends
*
* IDLE_TIMEOUT → RECORDING:
* - Car enters drive gear before timer expires → cancel timer, resume recording
* in the same trip (no new trip directory)
*
* RECORDING → IDLE_TIMEOUT:
* - Car leaves drive gear (park, off, neutral) → start 10-minute idle timer,
* continue recording frames during the timeout period
*
* IDLE_TIMEOUT → TRIP_ENDED:
* - 10-minute timer expires → close segment, trip is over
*
* TRIP_ENDED → RECORDING (new trip):
* - Car enters drive gear → create new trip directory, start recording
*
* Any state → (new trip) on ignition off→on:
* - Ignition transitions off→on → close current trip if active, create new trip
* directory, start recording with 10-min idle timer. This applies even from
* TRIP_ENDED — turning the car on always starts a new trip. If the car is in
* park after ignition on, the idle timer runs; entering drive cancels it.
*
* Graceful shutdown (DashcamShutdown param):
* - thermald sets DashcamShutdown="1" before device power-off
* - dashcamd closes current segment, sets DashcamShutdown="0" (ack), exits
* - thermald waits up to 15s for ack, then proceeds with shutdown
*
* GPS subtitle track:
* - Each .mp4 segment has a companion .srt subtitle file
* - Updated at most once per second from gpsLocation cereal messages
* - Format: "35 MPH | 44.9216°N 93.3260°W | 2026-04-13 05:19:00 UTC"
* - Most players auto-detect .srt files alongside .mp4 files
*/
#include <cstdio>
#include <ctime>
#include <string>
#include <sys/stat.h>
#include <sys/resource.h>
#include <unistd.h>
#include "cereal/messaging/messaging.h"
#include "cereal/visionipc/visionipc_client.h"
#include "common/params.h"
#include "common/timing.h"
#include "common/swaglog.h"
#include "common/util.h"
#include "selfdrive/frogpilot/screenrecorder/omx_encoder.h"
const std::string VIDEOS_BASE = "/data/media/0/videos";
const int SEGMENT_SECONDS = 180; // 3 minutes
const int SOURCE_FPS = 20;
const int CAMERA_FPS = 10;
const int FRAMES_PER_SEGMENT = SEGMENT_SECONDS * CAMERA_FPS;
const int BITRATE = 2500 * 1024; // 2500 kbps
const double IDLE_TIMEOUT_SECONDS = 600.0; // 10 minutes
ExitHandler do_exit;
enum TripState {
IDLE, // no trip active, waiting for drive
RECORDING, // actively recording, car in drive
IDLE_TIMEOUT, // car parked/off, recording with 10-min timer
TRIP_ENDED, // trip closed, waiting for next drive
};
static std::string make_timestamp() {
char buf[64];
time_t t = time(NULL);
struct tm tm = *localtime(&t);
snprintf(buf, sizeof(buf), "%04d%02d%02d-%02d%02d%02d",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
return std::string(buf);
}
static bool system_time_valid() {
time_t t = time(NULL);
struct tm tm = *gmtime(&t);
return (tm.tm_year + 1900) >= 2024;
}
static std::string make_utc_timestamp() {
char buf[32];
time_t t = time(NULL);
struct tm tm = *gmtime(&t);
snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d UTC",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
return std::string(buf);
}
// Format SRT timestamp: HH:MM:SS,mmm
static std::string srt_time(int seconds) {
int h = seconds / 3600;
int m = (seconds % 3600) / 60;
int s = seconds % 60;
char buf[16];
snprintf(buf, sizeof(buf), "%02d:%02d:%02d,000", h, m, s);
return std::string(buf);
}
int main(int argc, char *argv[]) {
setpriority(PRIO_PROCESS, 0, -10);
// Ensure base output directory exists
mkdir(VIDEOS_BASE.c_str(), 0755);
// Wait for valid system time so trip/segment names have real timestamps
LOGW("dashcamd: waiting for system time");
while (!do_exit) {
if (system_time_valid()) break;
usleep(1000000); // 1 Hz poll
}
if (do_exit) return 0;
LOGW("dashcamd: system time valid");
LOGW("dashcamd: started, connecting to camerad road stream");
VisionIpcClient vipc("camerad", VISION_STREAM_ROAD, false);
while (!do_exit && !vipc.connect(false)) {
usleep(100000);
}
if (do_exit) return 0;
LOGW("dashcamd: vipc connected, waiting for valid frame");
// Wait for a frame with valid dimensions (camerad may still be initializing)
VisionBuf *init_buf = nullptr;
while (!do_exit) {
init_buf = vipc.recv();
if (init_buf != nullptr && init_buf->width > 0 && init_buf->height > 0) break;
usleep(100000);
}
if (do_exit) return 0;
int width = init_buf->width;
int height = init_buf->height;
int y_stride = init_buf->stride;
int uv_stride = y_stride;
LOGW("dashcamd: first valid frame %dx%d stride=%d", width, height, y_stride);
// Subscribe to carState (gear), deviceState (ignition), gpsLocation (subtitles)
SubMaster sm({"carState", "deviceState", "gpsLocation"});
Params params;
// Trip state
TripState state = IDLE;
OmxEncoder *encoder = nullptr;
std::string trip_dir;
int frame_count = 0;
int recv_count = 0;
uint64_t segment_start_ts = 0;
double idle_timer_start = 0.0;
// SRT subtitle state
FILE *srt_file = nullptr;
int srt_index = 0; // subtitle entry counter (1-based)
int srt_segment_sec = 0; // seconds elapsed in current segment
double last_srt_write = 0; // monotonic time of last SRT write
// Ignition tracking for off→on detection
bool prev_started = false;
bool started_initialized = false;
// Param check throttle (don't hit filesystem every frame)
int param_check_counter = 0;
// Helper: start a new trip with recording + optional idle timer
auto start_new_trip = [&]() {
// Close existing encoder if any
if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close();
}
delete encoder;
encoder = nullptr;
}
trip_dir = VIDEOS_BASE + "/" + make_timestamp();
mkdir(trip_dir.c_str(), 0755);
LOGW("dashcamd: new trip %s", trip_dir.c_str());
encoder = new OmxEncoder(trip_dir.c_str(), width, height, CAMERA_FPS, BITRATE, false, false);
std::string seg_name = make_timestamp();
LOGW("dashcamd: opening segment %s", seg_name.c_str());
encoder->encoder_open((seg_name + ".mp4").c_str());
// Open companion SRT file
std::string srt_path = trip_dir + "/" + seg_name + ".srt";
srt_file = fopen(srt_path.c_str(), "w");
srt_index = 0;
srt_segment_sec = 0;
last_srt_write = 0;
frame_count = 0;
segment_start_ts = nanos_since_boot();
state = RECORDING;
};
auto close_trip = [&]() {
if (srt_file) { fclose(srt_file); srt_file = nullptr; }
if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close();
LOGW("dashcamd: segment closed");
}
delete encoder;
encoder = nullptr;
}
state = TRIP_ENDED;
frame_count = 0;
idle_timer_start = 0.0;
LOGW("dashcamd: trip ended");
};
// Start recording immediately — if the car is in drive, great; if parked/off,
// the 10-min idle timer will stop the trip if drive is never detected.
start_new_trip();
idle_timer_start = nanos_since_boot() / 1e9;
state = IDLE_TIMEOUT;
LOGW("dashcamd: recording started, waiting for drive (10-min idle timer active)");
while (!do_exit) {
VisionBuf *buf = vipc.recv();
if (buf == nullptr) continue;
// CLEARPILOT: skip frames to match target FPS (SOURCE_FPS -> CAMERA_FPS)
recv_count++;
if (SOURCE_FPS > CAMERA_FPS && (recv_count % (SOURCE_FPS / CAMERA_FPS)) != 0) continue;
sm.update(0);
double now = nanos_since_boot() / 1e9;
// Read vehicle state
bool started = sm.valid("deviceState") &&
sm["deviceState"].getDeviceState().getStarted();
auto gear = sm.valid("carState") ?
sm["carState"].getCarState().getGearShifter() :
cereal::CarState::GearShifter::UNKNOWN;
bool in_drive = (gear == cereal::CarState::GearShifter::DRIVE ||
gear == cereal::CarState::GearShifter::SPORT ||
gear == cereal::CarState::GearShifter::LOW ||
gear == cereal::CarState::GearShifter::MANUMATIC);
// Detect ignition off→on transition (new ignition cycle = new trip)
if (started_initialized && !prev_started && started) {
LOGW("dashcamd: ignition on — new cycle");
if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip();
}
// Start recording immediately, idle timer until drive is detected
start_new_trip();
idle_timer_start = now;
state = IDLE_TIMEOUT;
}
prev_started = started;
started_initialized = true;
// Check for graceful shutdown request (every ~1 second = 20 frames)
if (++param_check_counter >= CAMERA_FPS) {
param_check_counter = 0;
if (params.getBool("DashcamShutdown")) {
LOGW("dashcamd: shutdown requested, closing trip");
if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip();
}
params.putBool("DashcamShutdown", false);
LOGW("dashcamd: shutdown ack sent, exiting");
break;
}
}
// State machine transitions
switch (state) {
case IDLE:
case TRIP_ENDED:
if (in_drive) {
start_new_trip();
}
break;
case RECORDING:
if (!in_drive) {
// Car left drive — start idle timeout
idle_timer_start = now;
state = IDLE_TIMEOUT;
LOGW("dashcamd: car not in drive, starting 10-min idle timer");
}
break;
case IDLE_TIMEOUT:
if (in_drive) {
// Back in drive — cancel timer, resume recording same trip
idle_timer_start = 0.0;
state = RECORDING;
LOGW("dashcamd: back in drive, resuming trip");
} else if ((now - idle_timer_start) >= IDLE_TIMEOUT_SECONDS) {
// Timer expired — end trip
LOGW("dashcamd: idle timeout expired");
close_trip();
}
break;
}
// Only encode frames when we have an active recording
if (state != RECORDING && state != IDLE_TIMEOUT) continue;
// Segment rotation
if (frame_count >= FRAMES_PER_SEGMENT) {
if (srt_file) { fclose(srt_file); srt_file = nullptr; }
encoder->encoder_close();
std::string seg_name = make_timestamp();
LOGW("dashcamd: opening segment %s", seg_name.c_str());
encoder->encoder_open((seg_name + ".mp4").c_str());
std::string srt_path = trip_dir + "/" + seg_name + ".srt";
srt_file = fopen(srt_path.c_str(), "w");
srt_index = 0;
srt_segment_sec = 0;
last_srt_write = 0;
frame_count = 0;
segment_start_ts = nanos_since_boot();
}
uint64_t ts = nanos_since_boot() - segment_start_ts;
// Feed NV12 frame directly to OMX encoder
encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts);
frame_count++;
// Write GPS subtitle at most once per second
if (srt_file && (now - last_srt_write) >= 1.0) {
last_srt_write = now;
srt_index++;
// Read GPS data
double lat = 0, lon = 0, speed_ms = 0;
bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix();
if (has_gps) {
auto gps = sm["gpsLocation"].getGpsLocation();
lat = gps.getLatitude();
lon = gps.getLongitude();
speed_ms = gps.getSpeed();
}
double speed_mph = speed_ms * 2.23694;
std::string utc = make_utc_timestamp();
std::string t_start = srt_time(srt_segment_sec);
std::string t_end = srt_time(srt_segment_sec + 1);
srt_segment_sec++;
if (has_gps) {
const char *deg = "\xC2\xB0"; // UTF-8 degree sign
fprintf(srt_file, "%d\n%s --> %s\n%.0f MPH | %.4f%s%c %.4f%s%c | %s\n\n",
srt_index, t_start.c_str(), t_end.c_str(),
speed_mph,
std::abs(lat), deg, lat >= 0 ? 'N' : 'S',
std::abs(lon), deg, lon >= 0 ? 'E' : 'W',
utc.c_str());
} else {
fprintf(srt_file, "%d\n%s --> %s\nNo GPS | %s\n\n",
srt_index, t_start.c_str(), t_end.c_str(), utc.c_str());
}
fflush(srt_file);
}
}
// Clean exit
if (srt_file) { fclose(srt_file); srt_file = nullptr; }
if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close();
}
delete encoder;
}
LOGW("dashcamd: stopped");
return 0;
}