feat: dashcamd trip lifecycle, status indicator, CLAUDE.md updates
Some checks failed
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled

dashcamd now waits for valid system time + GPS fix + drive gear before
starting a trip. Returns to waiting state on 10-min park timeout or
ignition off. Publishes DashcamState and per-trip DashcamFrames to
memory params. Status window shows stopped/waiting/recording states.

Updated CLAUDE.md with current display mode behavior, OmxEncoder port
details, speed limit warning thresholds, and dashcam param docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 01:26:58 -05:00
parent dfb7b7404f
commit 2331aa00a0
7 changed files with 106 additions and 130 deletions

Binary file not shown.

View File

@@ -7,44 +7,32 @@
* Trip directory structure:
* /data/media/0/videos/YYYYMMDD-HHMMSS/ (trip directory, named at trip start)
* YYYYMMDD-HHMMSS.mp4 (3-minute segments)
* YYYYMMDD-HHMMSS.srt (GPS subtitle sidecar)
*
* 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
* WAITING:
* - Process starts in this state
* - Waits for valid system time (year >= 2024) AND car in drive gear
* - Transitions to RECORDING when both conditions met
*
* IDLE_TIMEOUT → RECORDING:
* - Car enters drive gear before timer expires → cancel timer, resume recording
* in the same trip (no new trip directory)
* RECORDING:
* - Actively encoding frames, car is in drive
* - Car leaves drive → start 10-min idle timer → IDLE_TIMEOUT
*
* 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.
* IDLE_TIMEOUT:
* - Car left drive, still recording with timer running
* - Car re-enters drive → cancel timer → RECORDING
* - Timer expires → close trip → WAITING
* - Ignition off → close trip → WAITING
*
* 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
* - dashcamd closes current segment, acks, exits
*
* 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
* Published params (memory, every 5s):
* - DashcamState: "waiting" or "recording"
* - DashcamFrames: per-trip encoded frame count (resets each trip)
*/
#include <cstdio>
@@ -75,10 +63,9 @@ const double IDLE_TIMEOUT_SECONDS = 600.0; // 10 minutes
ExitHandler do_exit;
enum TripState {
IDLE, // no trip active, waiting for drive
WAITING, // no trip, waiting for valid time + drive gear
RECORDING, // actively recording, car in drive
IDLE_TIMEOUT, // car parked/off, recording with 10-min timer
TRIP_ENDED, // trip closed, waiting for next drive
IDLE_TIMEOUT, // car left drive, recording with 10-min timer
};
static std::string make_timestamp() {
@@ -137,15 +124,6 @@ int main(int argc, char *argv[]) {
// 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)) {
@@ -172,45 +150,40 @@ int main(int argc, char *argv[]) {
// Subscribe to carState (gear), deviceState (ignition), gpsLocation (subtitles)
SubMaster sm({"carState", "deviceState", "gpsLocation"});
Params params;
Params params_memory("/dev/shm/params");
// Trip state
TripState state = IDLE;
TripState state = WAITING;
OmxEncoder *encoder = nullptr;
std::string trip_dir;
int frame_count = 0;
int frame_count = 0; // per-segment (for rotation)
int trip_frames = 0; // per-trip (published to params)
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
int srt_index = 0;
int srt_segment_sec = 0;
double last_srt_write = 0;
// Ignition tracking for off→on detection
// Ignition tracking
bool prev_started = false;
bool started_initialized = false;
// Param check throttle (don't hit filesystem every frame)
// Param publish throttle
int param_check_counter = 0;
double last_param_write = 0;
// Total encoded frames counter + param writer
Params params_memory("/dev/shm/params");
int total_frames = 0;
double last_frame_count_write = 0;
// Publish initial state
params_memory.put("DashcamState", "waiting");
params_memory.put("DashcamFrames", "0");
// Helper: start a new trip with recording + optional idle timer
LOGW("dashcamd: entering main loop in WAITING state");
// Helper: start a new trip
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());
@@ -221,7 +194,6 @@ int main(int argc, char *argv[]) {
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;
@@ -229,38 +201,38 @@ int main(int argc, char *argv[]) {
last_srt_write = 0;
frame_count = 0;
trip_frames = 0;
segment_start_ts = nanos_since_boot();
state = RECORDING;
params_memory.put("DashcamState", "recording");
params_memory.put("DashcamFrames", "0");
};
// Helper: close current trip
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");
}
encoder->encoder_close();
LOGW("dashcamd: segment closed");
delete encoder;
encoder = nullptr;
}
state = TRIP_ENDED;
state = WAITING;
frame_count = 0;
trip_frames = 0;
idle_timer_start = 0.0;
LOGW("dashcamd: trip ended");
};
LOGW("dashcamd: trip ended, returning to WAITING");
// 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)");
params_memory.put("DashcamState", "waiting");
params_memory.put("DashcamFrames", "0");
};
while (!do_exit) {
VisionBuf *buf = vipc.recv();
if (buf == nullptr) continue;
// CLEARPILOT: skip frames to match target FPS (SOURCE_FPS -> CAMERA_FPS)
// 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;
@@ -278,25 +250,21 @@ int main(int argc, char *argv[]) {
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");
// Detect ignition off → close any active trip
if (started_initialized && prev_started && !started) {
LOGW("dashcamd: ignition off");
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)
// Check for graceful shutdown request (every ~1 second)
if (++param_check_counter >= CAMERA_FPS) {
param_check_counter = 0;
if (params.getBool("DashcamShutdown")) {
LOGW("dashcamd: shutdown requested, closing trip");
LOGW("dashcamd: shutdown requested");
if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip();
}
@@ -306,32 +274,30 @@ int main(int argc, char *argv[]) {
}
}
// State machine transitions
// State machine
switch (state) {
case IDLE:
case TRIP_ENDED:
if (in_drive) {
case WAITING: {
bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix();
if (in_drive && system_time_valid() && has_gps) {
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");
LOGW("dashcamd: car left 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();
}
@@ -368,19 +334,15 @@ int main(int argc, char *argv[]) {
continue;
}
if (frame_count == 0) {
LOGW("dashcamd: first encode w=%d h=%d stride=%d buf_y=%p buf_uv=%p", width, height, y_stride, buf->y, buf->uv);
}
// Feed NV12 frame directly to OMX encoder
encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts);
frame_count++;
total_frames++;
trip_frames++;
// Write total frame count to params_memory every 5 seconds
if (now - last_frame_count_write >= 5.0) {
params_memory.put("DashcamFrames", std::to_string(total_frames));
last_frame_count_write = now;
// Publish state every 5 seconds
if (now - last_param_write >= 5.0) {
params_memory.put("DashcamFrames", std::to_string(trip_frames));
last_param_write = now;
}
// Write GPS subtitle at most once per second
@@ -388,7 +350,6 @@ int main(int argc, char *argv[]) {
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) {
@@ -421,13 +382,11 @@ int main(int argc, char *argv[]) {
}
// Clean exit
if (srt_file) { fclose(srt_file); srt_file = nullptr; }
if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close();
}
delete encoder;
if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip();
}
params_memory.put("DashcamState", "stopped");
params_memory.put("DashcamFrames", "0");
LOGW("dashcamd: stopped");
return 0;

View File

@@ -88,6 +88,7 @@ def manager_init(frogpilot_functions) -> None:
params_memory.put("TelemetryEnabled", "0")
params_memory.put("VpnEnabled", "1")
params_memory.put("DashcamFrames", "0")
params_memory.put("DashcamState", "stopped")
params_memory.put("ModelStandby", "0")
params_memory.put("ModelStandbyTs", "0")
params_memory.put("CarIsMetric", "0")

View File

@@ -264,8 +264,8 @@ static StatusWindow::StatusData collectStatus() {
d.telemetry = readFile("/data/params/d/TelemetryEnabled");
// Dashcam
QString dashcam_pid = shellCmd("pgrep -x dashcamd");
d.dashcam_status = dashcam_pid.isEmpty() ? "stopped" : "recording";
d.dashcam_state = readFile("/dev/shm/params/d/DashcamState");
if (d.dashcam_state.isEmpty()) d.dashcam_state = "stopped";
d.dashcam_frames = readFile("/dev/shm/params/d/DashcamFrames");
// Panda: checked on UI thread in applyResults() via scene.pandaType
@@ -383,11 +383,14 @@ void StatusWindow::applyResults() {
telemetry_label->setStyleSheet("color: grey; font-size: 38px;");
}
if (d.dashcam_status == "recording") {
if (d.dashcam_state == "recording") {
QString text = "Recording";
if (!d.dashcam_frames.isEmpty() && d.dashcam_frames != "0") text += " (" + d.dashcam_frames + " frames)";
dashcam_label->setText(text);
dashcam_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else if (d.dashcam_state == "waiting") {
dashcam_label->setText("Waiting");
dashcam_label->setStyleSheet("color: #ffaa00; font-size: 38px;");
} else {
dashcam_label->setText("Stopped");
dashcam_label->setStyleSheet("color: #ff4444; font-size: 38px;");

View File

@@ -20,7 +20,7 @@ public:
struct StatusData {
QString time, storage, ram, load, temp, fan, ip, wifi;
QString vpn_status, vpn_ip, gps, telemetry;
QString dashcam_status, dashcam_frames;
QString dashcam_state, dashcam_frames;
float temp_c = 0;
};