dashcamd v3: trip directories, state machine, graceful shutdown
Dashcam recording now organized by trip in /data/media/0/videos/YYYYMMDD-HHMMSS/. Starts recording immediately on launch (with 10-min idle timer), transitions to continuous recording when drive gear detected. New trip on every ignition cycle. Graceful shutdown via DashcamShutdown param with 15s ack timeout in thermald. - Bitrate reduced to 2500 kbps (was 4 Mbps) - Trip state machine: IDLE → RECORDING ↔ IDLE_TIMEOUT → TRIP_ENDED - Deleter: trip-aware deletion (oldest trip first, then segments within active trip) - camerad changed to always_run (was driverview) so dashcam works offroad - DashcamShutdown param for graceful close before device power-off Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,7 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"CurrentBootlog", PERSISTENT},
|
||||
{"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"DashcamDebug", PERSISTENT},
|
||||
{"DashcamShutdown", CLEAR_ON_MANAGER_START},
|
||||
{"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"DisablePowerDown", PERSISTENT},
|
||||
{"DisableUpdates", PERSISTENT},
|
||||
|
||||
Binary file not shown.
@@ -2,9 +2,43 @@
|
||||
* 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 in /data/media/0/videos/.
|
||||
* to the Qualcomm OMX encoder. Produces 3-minute MP4 segments organized by trip.
|
||||
*
|
||||
* Suspends recording after 10 minutes of standstill, resumes when car moves.
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
@@ -22,33 +56,54 @@
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/frogpilot/screenrecorder/omx_encoder.h"
|
||||
|
||||
const std::string VIDEOS_DIR = "/data/media/0/videos";
|
||||
const std::string VIDEOS_BASE = "/data/media/0/videos";
|
||||
const int SEGMENT_SECONDS = 180; // 3 minutes
|
||||
const int CAMERA_FPS = 20;
|
||||
const int FRAMES_PER_SEGMENT = SEGMENT_SECONDS * CAMERA_FPS;
|
||||
const int BITRATE = 4 * 1024 * 1024; // 4 Mbps
|
||||
const double STANDSTILL_TIMEOUT_SECONDS = 600.0; // 10 minutes
|
||||
const int BITRATE = 2500 * 1024; // 2500 kbps
|
||||
const double IDLE_TIMEOUT_SECONDS = 600.0; // 10 minutes
|
||||
|
||||
ExitHandler do_exit;
|
||||
|
||||
static std::string make_filename() {
|
||||
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.mp4",
|
||||
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;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
setpriority(PRIO_PROCESS, 0, -10);
|
||||
|
||||
// Ensure output directory exists
|
||||
mkdir(VIDEOS_DIR.c_str(), 0755);
|
||||
// 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: connecting to camerad road stream");
|
||||
|
||||
VisionIpcClient vipc("camerad", VISION_STREAM_ROAD, false);
|
||||
while (!do_exit && !vipc.connect(false)) {
|
||||
usleep(100000);
|
||||
@@ -59,80 +114,178 @@ int main(int argc, char *argv[]) {
|
||||
int height = vipc.buffers[0].height;
|
||||
int y_stride = vipc.buffers[0].stride;
|
||||
int uv_stride = y_stride;
|
||||
|
||||
LOGW("dashcamd: connected %dx%d, stride=%d", width, height, y_stride);
|
||||
|
||||
// Subscribe to carState for standstill detection
|
||||
SubMaster sm({"carState"});
|
||||
// Subscribe to carState (gear, standstill) and deviceState (ignition/started)
|
||||
SubMaster sm({"carState", "deviceState"});
|
||||
Params params;
|
||||
|
||||
// Create encoder — H.264, no downscale, with MP4 remuxing (h265=false)
|
||||
OmxEncoder encoder(VIDEOS_DIR.c_str(), width, height, CAMERA_FPS, BITRATE, false, false);
|
||||
// Trip state
|
||||
TripState state = IDLE;
|
||||
OmxEncoder *encoder = nullptr;
|
||||
std::string trip_dir;
|
||||
int frame_count = 0;
|
||||
uint64_t segment_start_ts = 0;
|
||||
double idle_timer_start = 0.0;
|
||||
|
||||
int frame_count = FRAMES_PER_SEGMENT; // force new segment on first frame
|
||||
uint64_t start_ts = 0;
|
||||
bool recording = false;
|
||||
bool suspended = false;
|
||||
double standstill_start = 0.0;
|
||||
// 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 filename = make_timestamp() + ".mp4";
|
||||
LOGW("dashcamd: opening segment %s", filename.c_str());
|
||||
encoder->encoder_open(filename.c_str());
|
||||
frame_count = 0;
|
||||
segment_start_ts = nanos_since_boot();
|
||||
state = RECORDING;
|
||||
};
|
||||
|
||||
auto close_trip = [&]() {
|
||||
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;
|
||||
|
||||
// Check standstill state
|
||||
sm.update(0);
|
||||
bool is_standstill = sm.valid("carState") && sm["carState"].getCarState().getStandstill();
|
||||
double now = nanos_since_boot() / 1e9;
|
||||
|
||||
if (is_standstill) {
|
||||
if (standstill_start == 0.0) {
|
||||
standstill_start = now;
|
||||
// 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();
|
||||
}
|
||||
// Suspend after 10 minutes of continuous standstill
|
||||
if (!suspended && (now - standstill_start) >= STANDSTILL_TIMEOUT_SECONDS) {
|
||||
LOGW("dashcamd: suspending — standstill for 10 minutes");
|
||||
if (recording) {
|
||||
encoder.encoder_close();
|
||||
recording = false;
|
||||
frame_count = FRAMES_PER_SEGMENT;
|
||||
// Start recording immediately, idle timer until drive is detected
|
||||
start_new_trip();
|
||||
idle_timer_start = now;
|
||||
state = IDLE_TIMEOUT;
|
||||
}
|
||||
suspended = true;
|
||||
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();
|
||||
}
|
||||
} else {
|
||||
standstill_start = 0.0;
|
||||
if (suspended) {
|
||||
LOGW("dashcamd: resuming — car moving");
|
||||
suspended = false;
|
||||
params.putBool("DashcamShutdown", false);
|
||||
LOGW("dashcamd: shutdown ack sent, exiting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (suspended) continue;
|
||||
// State machine transitions
|
||||
switch (state) {
|
||||
case IDLE:
|
||||
case TRIP_ENDED:
|
||||
if (in_drive) {
|
||||
start_new_trip();
|
||||
}
|
||||
break;
|
||||
|
||||
// Start new segment if needed
|
||||
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 (recording) {
|
||||
encoder.encoder_close();
|
||||
}
|
||||
|
||||
std::string filename = make_filename();
|
||||
encoder->encoder_close();
|
||||
std::string filename = make_timestamp() + ".mp4";
|
||||
LOGW("dashcamd: opening segment %s", filename.c_str());
|
||||
encoder.encoder_open(filename.c_str());
|
||||
encoder->encoder_open(filename.c_str());
|
||||
frame_count = 0;
|
||||
start_ts = nanos_since_boot();
|
||||
recording = true;
|
||||
segment_start_ts = nanos_since_boot();
|
||||
}
|
||||
|
||||
uint64_t ts = nanos_since_boot() - start_ts;
|
||||
uint64_t ts = nanos_since_boot() - segment_start_ts;
|
||||
|
||||
// Feed NV12 frame directly to OMX encoder
|
||||
uint8_t *y_ptr = buf->y;
|
||||
uint8_t *uv_ptr = buf->uv;
|
||||
encoder.encode_frame_nv12(y_ptr, y_stride, uv_ptr, uv_stride, width, height, ts);
|
||||
|
||||
encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts);
|
||||
frame_count++;
|
||||
}
|
||||
|
||||
if (recording) {
|
||||
encoder.encoder_close();
|
||||
// Clean exit
|
||||
if (encoder) {
|
||||
if (state == RECORDING || state == IDLE_TIMEOUT) {
|
||||
encoder->encoder_close();
|
||||
}
|
||||
delete encoder;
|
||||
}
|
||||
LOGW("dashcamd: stopped");
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ def dashcam_should_run(started, params, CP: car.CarParams) -> bool:
|
||||
procs = [
|
||||
DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"),
|
||||
|
||||
NativeProcess("camerad", "system/camerad", ["./camerad"], driverview),
|
||||
NativeProcess("camerad", "system/camerad", ["./camerad"], always_run),
|
||||
NativeProcess("logcatd", "system/logcatd", ["./logcatd"], allow_logging),
|
||||
NativeProcess("proclogd", "system/proclogd", ["./proclogd"], allow_logging),
|
||||
PythonProcess("logmessaged", "system.logmessaged", allow_logging),
|
||||
|
||||
@@ -411,6 +411,16 @@ def thermald_thread(end_event, hw_queue) -> None:
|
||||
# Check if we need to shut down
|
||||
if power_monitor.should_shutdown(onroad_conditions["ignition"], in_car, off_ts, started_seen):
|
||||
cloudlog.warning(f"shutting device down, offroad since {off_ts}")
|
||||
# CLEARPILOT: signal dashcamd to close recording gracefully before power-off
|
||||
params.put_bool("DashcamShutdown", True)
|
||||
deadline = time.monotonic() + 15.0
|
||||
while time.monotonic() < deadline:
|
||||
if not params.getBool("DashcamShutdown"):
|
||||
cloudlog.info("dashcamd shutdown ack received")
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
cloudlog.warning("dashcamd shutdown ack timeout, proceeding")
|
||||
params.put_bool("DoShutdown", True)
|
||||
|
||||
msg.deviceState.started = started_ts is not None
|
||||
|
||||
@@ -49,18 +49,51 @@ def get_preserved_segments(dirs_by_creation: list[str]) -> list[str]:
|
||||
|
||||
|
||||
def delete_oldest_video():
|
||||
"""CLEARPILOT: delete the oldest screen recording MP4 when disk space is low."""
|
||||
"""CLEARPILOT: delete oldest dashcam footage when disk space is low.
|
||||
Trip directories are /data/media/0/videos/YYYYMMDD-HHMMSS/ containing .mp4 segments.
|
||||
Deletes entire oldest trip directory first. If only one trip remains (active),
|
||||
deletes individual segments oldest-first within it. Also cleans up legacy flat .mp4 files."""
|
||||
try:
|
||||
if not os.path.isdir(VIDEOS_DIR):
|
||||
return False
|
||||
videos = sorted(
|
||||
(f for f in os.listdir(VIDEOS_DIR) if f.endswith('.mp4')),
|
||||
key=lambda f: os.path.getctime(os.path.join(VIDEOS_DIR, f))
|
||||
)
|
||||
if not videos:
|
||||
|
||||
# Collect legacy flat mp4 files and trip directories
|
||||
legacy_files = []
|
||||
trip_dirs = []
|
||||
for entry in os.listdir(VIDEOS_DIR):
|
||||
path = os.path.join(VIDEOS_DIR, entry)
|
||||
if os.path.isfile(path) and entry.endswith('.mp4'):
|
||||
legacy_files.append(entry)
|
||||
elif os.path.isdir(path):
|
||||
trip_dirs.append(entry)
|
||||
|
||||
# Delete legacy flat files first (oldest by name)
|
||||
if legacy_files:
|
||||
legacy_files.sort()
|
||||
delete_path = os.path.join(VIDEOS_DIR, legacy_files[0])
|
||||
cloudlog.info(f"deleting legacy video {delete_path}")
|
||||
os.remove(delete_path)
|
||||
return True
|
||||
|
||||
if not trip_dirs:
|
||||
return False
|
||||
delete_path = os.path.join(VIDEOS_DIR, videos[0])
|
||||
cloudlog.info(f"deleting video {delete_path}")
|
||||
|
||||
trip_dirs.sort() # sorted by timestamp name = chronological order
|
||||
|
||||
# If more than one trip, delete the oldest entire trip directory
|
||||
if len(trip_dirs) > 1:
|
||||
delete_path = os.path.join(VIDEOS_DIR, trip_dirs[0])
|
||||
cloudlog.info(f"deleting trip {delete_path}")
|
||||
shutil.rmtree(delete_path)
|
||||
return True
|
||||
|
||||
# Only one trip left (likely active) — delete oldest segment within it
|
||||
trip_path = os.path.join(VIDEOS_DIR, trip_dirs[0])
|
||||
segments = sorted(f for f in os.listdir(trip_path) if f.endswith('.mp4'))
|
||||
if not segments:
|
||||
return False
|
||||
delete_path = os.path.join(trip_path, segments[0])
|
||||
cloudlog.info(f"deleting segment {delete_path}")
|
||||
os.remove(delete_path)
|
||||
return True
|
||||
except OSError:
|
||||
|
||||
Reference in New Issue
Block a user