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

View File

@@ -15,7 +15,7 @@ ClearPilot is a custom fork of **FrogPilot** (itself a fork of comma.ai's openpi
- **Native dashcamd**: C++ process capturing raw camera frames via VisionIPC with OMX H.264 hardware encoding - **Native dashcamd**: C++ process capturing raw camera frames via VisionIPC with OMX H.264 hardware encoding
- **Standstill power saving**: model inference throttled to 1fps and fan quieted when car is stopped - **Standstill power saving**: model inference throttled to 1fps and fan quieted when car is stopped
- **ClearPilot menu**: sidebar settings panel replacing stock home screen (Home, Dashcam, Debug panels) - **ClearPilot menu**: sidebar settings panel replacing stock home screen (Home, Dashcam, Debug panels)
- **Status window**: live system stats (temp, fan, storage, RAM, WiFi, VPN, GPS, telemetry status) - **Status window**: live system stats (temp, fan, storage, RAM, WiFi, VPN, GPS, telemetry, dashcam status)
- **Debug button (LFA)**: steering wheel button repurposed for screen toggle and future UI actions - **Debug button (LFA)**: steering wheel button repurposed for screen toggle and future UI actions
- **Telemetry system**: diff-based CSV logger via ZMQ IPC, toggleable from Debug panel - **Telemetry system**: diff-based CSV logger via ZMQ IPC, toggleable from Debug panel
- **Bench mode**: `--bench` flag for onroad UI testing without car connected - **Bench mode**: `--bench` flag for onroad UI testing without car connected
@@ -101,7 +101,7 @@ ClearPilot uses memory params (`/dev/shm/params/d/`) for UI toggles that should
- **Python access**: Use `Params("/dev/shm/params")` - **Python access**: Use `Params("/dev/shm/params")`
- **Defaults**: Set in `manager_init()` via `Params("/dev/shm/params").put(key, value)` - **Defaults**: Set in `manager_init()` via `Params("/dev/shm/params").put(key, value)`
- **UI toggles**: Use `ToggleControl` with manual `toggleFlipped` lambda that writes via `Params("/dev/shm/params")`. Do NOT use `ParamControl` for memory params — it reads/writes persistent params only - **UI toggles**: Use `ToggleControl` with manual `toggleFlipped` lambda that writes via `Params("/dev/shm/params")`. Do NOT use `ParamControl` for memory params — it reads/writes persistent params only
- **Current memory params**: `TelemetryEnabled` (default "0"), `VpnEnabled` (default "1"), `ModelStandby` (default "0"), `ScreenDisplayMode` - **Current memory params**: `TelemetryEnabled` (default "0"), `VpnEnabled` (default "1"), `ModelStandby` (default "0"), `ScreenDisplayMode`, `DashcamState` (default "stopped"), `DashcamFrames` (default "0")
- **IMPORTANT — method names differ between C++ and Python**: C++ uses camelCase (`putBool`, `getBool`, `getInt`), Python uses snake_case (`put_bool`, `get_bool`, `get_int`). This is a common source of silent failures — the wrong casing compiles/runs but doesn't work. - **IMPORTANT — method names differ between C++ and Python**: C++ uses camelCase (`putBool`, `getBool`, `getInt`), Python uses snake_case (`put_bool`, `get_bool`, `get_int`). This is a common source of silent failures — the wrong casing compiles/runs but doesn't work.
### Building Native (C++) Processes ### Building Native (C++) Processes
@@ -245,20 +245,21 @@ A single `session.log` in each session directory records major events:
- **Segment length**: 3 minutes per file - **Segment length**: 3 minutes per file
- **Save path**: `/data/media/0/videos/YYYYMMDD-HHMMSS/YYYYMMDD-HHMMSS.mp4` (trip directories) - **Save path**: `/data/media/0/videos/YYYYMMDD-HHMMSS/YYYYMMDD-HHMMSS.mp4` (trip directories)
- **GPS subtitles**: companion `.srt` file per segment with 1Hz entries (speed MPH, lat/lon, UTC timestamp) - **GPS subtitles**: companion `.srt` file per segment with 1Hz entries (speed MPH, lat/lon, UTC timestamp)
- **Trip lifecycle**: starts recording on launch with 10-min idle timer; car in drive cancels timer; park/off restarts timer; ignition cycle = new trip - **Trip lifecycle**: waits in WAITING state until valid system time + GPS fix + car in drive; records until car parked 10 min or ignition off; then returns to WAITING
- **Graceful shutdown**: thermald sets `DashcamShutdown` param, dashcamd closes segment and acks within 15s - **Graceful shutdown**: thermald sets `DashcamShutdown` param, dashcamd closes segment and acks within 15s
- **Storage**: ~56 MB per 3-minute segment at 2500 kbps - **Storage**: ~56 MB per 3-minute segment at 2500 kbps (verified: actual bitrate ~2570 kbps)
- **Crash handler**: SIGSEGV/SIGABRT handler writes backtrace to `/tmp/dashcamd_crash.log`
- **Storage device**: WDC SDINDDH4-128G UFS 2.1 — automotive grade, ~384 TB write endurance, no concern for continuous writes - **Storage device**: WDC SDINDDH4-128G UFS 2.1 — automotive grade, ~384 TB write endurance, no concern for continuous writes
### Key Differences from Old Screen Recorder ### OmxEncoder
| | Old (screen recorder) | New (dashcamd) | The OMX encoder (`selfdrive/frogpilot/screenrecorder/omx_encoder.cc`) was ported from upstream FrogPilot with the following key properties:
|---|---|---|
| Source | `QWidget::grab()` screen capture | Raw NV12 from VisionIPC | - Each encoder instance calls `OMX_Init()` in constructor and `OMX_Deinit()` in destructor — manages its own OMX lifecycle
| Resolution | 1440x720 | 1928x1208 | - Constructor takes 5 args: `(path, width, height, fps, bitrate)` — no h265/downscale params
| Works with screen off | No (needs visible widget) | Yes (independent of UI) | - `encoder_close()` calls `av_write_trailer` + ffmpeg faststart remux (`-movflags +faststart`)
| Process type | Part of UI process | Standalone native process | - Destructor has null guards and error handling on all OMX state transitions
| Encoder input | RGBA -> NV12 conversion | NV12 direct (added `encode_frame_nv12`) | - ClearPilot addition: `encode_frame_nv12()` for direct NV12 input (dashcamd), alongside original `encode_frame_rgba()` (screen recorder)
### Key Files ### Key Files
@@ -266,7 +267,7 @@ A single `session.log` in each session directory records major events:
|------|------| |------|------|
| `selfdrive/clearpilot/dashcamd.cc` | Main dashcam process — VisionIPC -> OMX encoder | | `selfdrive/clearpilot/dashcamd.cc` | Main dashcam process — VisionIPC -> OMX encoder |
| `selfdrive/clearpilot/SConscript` | Build config for dashcamd | | `selfdrive/clearpilot/SConscript` | Build config for dashcamd |
| `selfdrive/frogpilot/screenrecorder/omx_encoder.cc` | OMX encoder (added `encode_frame_nv12` method) | | `selfdrive/frogpilot/screenrecorder/omx_encoder.cc` | OMX encoder (upstream FrogPilot port + `encode_frame_nv12`) |
| `selfdrive/frogpilot/screenrecorder/omx_encoder.h` | Encoder header | | `selfdrive/frogpilot/screenrecorder/omx_encoder.h` | Encoder header |
| `selfdrive/manager/process_config.py` | dashcamd registered as NativeProcess, camerad always_run, encoderd disabled | | `selfdrive/manager/process_config.py` | dashcamd registered as NativeProcess, camerad always_run, encoderd disabled |
| `system/loggerd/deleter.py` | Trip-aware storage rotation (oldest trip first, then segments within active trip) | | `system/loggerd/deleter.py` | Trip-aware storage rotation (oldest trip first, then segments within active trip) |
@@ -275,12 +276,14 @@ A single `session.log` in each session directory records major events:
- `DashcamDebug` — when `"1"`, dashcamd runs even without car connected (for bench testing) - `DashcamDebug` — when `"1"`, dashcamd runs even without car connected (for bench testing)
- `DashcamShutdown` — set by thermald before power-off, dashcamd acks by clearing it - `DashcamShutdown` — set by thermald before power-off, dashcamd acks by clearing it
- `DashcamState` (memory param) — "stopped", "waiting", or "recording" — published every 5s
- `DashcamFrames` (memory param) — per-trip encoded frame count, resets each trip — published every 5s
## Standstill Power Saving ## Standstill Power Saving
When `carState.standstill` is true: When `carState.standstill` is true:
- **modeld**: skips GPU inference on 19/20 frames (1fps vs 20fps), reports 0 frame drops to avoid triggering `modeldLagging` in controlsd - **modeld**: skips GPU inference on 19/20 frames (1fps vs 20fps), reports 0 frame drops to avoid triggering `modeldLagging` in controlsd. Runs full 20fps during calibration (`liveCalibration.calStatus != calibrated`)
- **dmonitoringmodeld**: same 1fps throttle, added `carState` subscription - **dmonitoringmodeld**: same 1fps throttle, added `carState` subscription
- **Fan controller**: uses offroad clamps (0-30%) instead of onroad (30-100%) at standstill; thermal protection still active via feedforward if temp > 60°C - **Fan controller**: uses offroad clamps (0-30%) instead of onroad (30-100%) at standstill; thermal protection still active via feedforward if temp > 60°C
@@ -321,6 +324,12 @@ The Hyundai Tucson's LFA steering wheel button cycles through 5 display modes vi
**Not in drive (parked/off):** any except 3 → 3 (screen off), state 3 → 0 (auto-normal) **Not in drive (parked/off):** any except 3 → 3 (screen off), state 3 → 0 (auto-normal)
**Shift to drive from screen off:** auto-resets to mode 0 (auto-normal) via `controlsd`
**Shift to park from nightrider:** auto-switches to mode 3 (screen off) via `home.cc`
**Tap screen while screen off:** resets to mode 0 (auto-normal) via `window.cc` touch handler
### Nightrider Mode ### Nightrider Mode
- Camera feed suppressed (OpenGL clears to black instead of rendering camera texture) - Camera feed suppressed (OpenGL clears to black instead of rendering camera texture)
@@ -357,7 +366,10 @@ Display power is managed by `Device::updateWakefulness()` in `selfdrive/ui/ui.cc
- **Ignition off (offroad)**: screen blanks after `ScreenTimeout` seconds (default 120) of no touch. Tapping wakes it. - **Ignition off (offroad)**: screen blanks after `ScreenTimeout` seconds (default 120) of no touch. Tapping wakes it.
- **Ignition on (onroad)**: screen stays on unconditionally — ignition=true short-circuits the timeout check. - **Ignition on (onroad)**: screen stays on unconditionally — ignition=true short-circuits the timeout check.
- **Debug button (LFA)**: cycles through display modes including screen off (state 3). Only state 3 calls `Hardware::set_display_power(false)`. - **ScreenDisplayMode 3 override**: `updateWakefulness` checks `ScreenDisplayMode` first — if mode 3, calls `setAwake(false)` unconditionally, preventing ignition-on from overriding screen-off.
- **Debug button (LFA)**: cycles through display modes including screen off (state 3).
- **Park transition**: always shows splash screen; if coming from nightrider mode, auto-switches to screen off (mode 3) via `home.cc`.
- **Touch wake**: tapping screen while in mode 3 resets to mode 0 via `window.cc` event filter.
## Offroad UI (ClearPilot Menu) ## Offroad UI (ClearPilot Menu)
@@ -500,7 +512,7 @@ Power On
- **GPS data**: logged directly by telemetryd via cereal `gpsLocation` subscription at 1Hz — group: `gps` (latitude, longitude, speed, altitude, bearing, accuracy) - **GPS data**: logged directly by telemetryd via cereal `gpsLocation` subscription at 1Hz — group: `gps` (latitude, longitude, speed, altitude, bearing, accuracy)
- **CSV location**: `/data/log2/current/telemetry.csv` (or session directory) - **CSV location**: `/data/log2/current/telemetry.csv` (or session directory)
- **Onroad overlay**: when telemetry enabled, status bar shows temp, fan %, model FPS, and STANDSTILL indicator - **Onroad overlay**: when telemetry enabled, status bar shows temp, fan %, model FPS, and STANDSTILL indicator
- **Speed limit**: `speed_limit.calculated` is the final computed speed limit value (in vehicle units, MPH when `is_metric=False`). This is the value that will be used for the future speed limit warning chime feature - **Speed limit**: processed by `selfdrive/clearpilot/speed_logic.py` (SpeedState class), converts m/s to display units, writes to memory params. Cruise warning signs appear when cruise set speed exceeds speed limit by threshold (10 mph if limit >= 50, 7 mph if < 50) or is 5+ mph under. Ding sound plays when warning sign appears or speed limit changes while visible (30s cooldown)
### Key Dependencies ### Key Dependencies

View File

@@ -111,6 +111,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"DashcamDebug", PERSISTENT}, {"DashcamDebug", PERSISTENT},
{"DashcamFrames", PERSISTENT}, {"DashcamFrames", PERSISTENT},
{"DashcamState", PERSISTENT},
{"DashcamShutdown", CLEAR_ON_MANAGER_START}, {"DashcamShutdown", CLEAR_ON_MANAGER_START},
{"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"DisablePowerDown", PERSISTENT}, {"DisablePowerDown", PERSISTENT},

Binary file not shown.

View File

@@ -7,44 +7,32 @@
* Trip directory structure: * Trip directory structure:
* /data/media/0/videos/YYYYMMDD-HHMMSS/ (trip directory, named at trip start) * /data/media/0/videos/YYYYMMDD-HHMMSS/ (trip directory, named at trip start)
* YYYYMMDD-HHMMSS.mp4 (3-minute segments) * YYYYMMDD-HHMMSS.mp4 (3-minute segments)
* YYYYMMDD-HHMMSS.srt (GPS subtitle sidecar)
* *
* Trip lifecycle state machine: * Trip lifecycle state machine:
* *
* On process start (after time-valid wait): * WAITING:
* - Create trip directory, start recording immediately with 10-min idle timer * - Process starts in this state
* - If car is already in drive, timer is cancelled and recording continues * - Waits for valid system time (year >= 2024) AND car in drive gear
* - If car stays parked/off for 10 minutes, trip ends * - Transitions to RECORDING when both conditions met
* *
* IDLE_TIMEOUT → RECORDING: * RECORDING:
* - Car enters drive gear before timer expires → cancel timer, resume recording * - Actively encoding frames, car is in drive
* in the same trip (no new trip directory) * - Car leaves drive → start 10-min idle timer → IDLE_TIMEOUT
* *
* RECORDING → IDLE_TIMEOUT: * IDLE_TIMEOUT:
* - Car leaves drive gear (park, off, neutral) → start 10-minute idle timer, * - Car left drive, still recording with timer running
* continue recording frames during the timeout period * - Car re-enters drive → cancel timer → RECORDING
* * - Timer expires → close trip → WAITING
* IDLE_TIMEOUT → TRIP_ENDED: * - Ignition off → close trip → WAITING
* - 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): * Graceful shutdown (DashcamShutdown param):
* - thermald sets DashcamShutdown="1" before device power-off * - thermald sets DashcamShutdown="1" before device power-off
* - dashcamd closes current segment, sets DashcamShutdown="0" (ack), exits * - dashcamd closes current segment, acks, exits
* - thermald waits up to 15s for ack, then proceeds with shutdown
* *
* GPS subtitle track: * Published params (memory, every 5s):
* - Each .mp4 segment has a companion .srt subtitle file * - DashcamState: "waiting" or "recording"
* - Updated at most once per second from gpsLocation cereal messages * - DashcamFrames: per-trip encoded frame count (resets each trip)
* - 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 <cstdio>
@@ -75,10 +63,9 @@ const double IDLE_TIMEOUT_SECONDS = 600.0; // 10 minutes
ExitHandler do_exit; ExitHandler do_exit;
enum TripState { enum TripState {
IDLE, // no trip active, waiting for drive WAITING, // no trip, waiting for valid time + drive gear
RECORDING, // actively recording, car in drive RECORDING, // actively recording, car in drive
IDLE_TIMEOUT, // car parked/off, recording with 10-min timer IDLE_TIMEOUT, // car left drive, recording with 10-min timer
TRIP_ENDED, // trip closed, waiting for next drive
}; };
static std::string make_timestamp() { static std::string make_timestamp() {
@@ -137,15 +124,6 @@ int main(int argc, char *argv[]) {
// Ensure base output directory exists // Ensure base output directory exists
mkdir(VIDEOS_BASE.c_str(), 0755); 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"); LOGW("dashcamd: started, connecting to camerad road stream");
VisionIpcClient vipc("camerad", VISION_STREAM_ROAD, false); VisionIpcClient vipc("camerad", VISION_STREAM_ROAD, false);
while (!do_exit && !vipc.connect(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) // Subscribe to carState (gear), deviceState (ignition), gpsLocation (subtitles)
SubMaster sm({"carState", "deviceState", "gpsLocation"}); SubMaster sm({"carState", "deviceState", "gpsLocation"});
Params params; Params params;
Params params_memory("/dev/shm/params");
// Trip state // Trip state
TripState state = IDLE; TripState state = WAITING;
OmxEncoder *encoder = nullptr; OmxEncoder *encoder = nullptr;
std::string trip_dir; 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; int recv_count = 0;
uint64_t segment_start_ts = 0; uint64_t segment_start_ts = 0;
double idle_timer_start = 0.0; double idle_timer_start = 0.0;
// SRT subtitle state // SRT subtitle state
FILE *srt_file = nullptr; FILE *srt_file = nullptr;
int srt_index = 0; // subtitle entry counter (1-based) int srt_index = 0;
int srt_segment_sec = 0; // seconds elapsed in current segment int srt_segment_sec = 0;
double last_srt_write = 0; // monotonic time of last SRT write double last_srt_write = 0;
// Ignition tracking for off→on detection // Ignition tracking
bool prev_started = false; bool prev_started = false;
bool started_initialized = false; bool started_initialized = false;
// Param check throttle (don't hit filesystem every frame) // Param publish throttle
int param_check_counter = 0; int param_check_counter = 0;
double last_param_write = 0;
// Total encoded frames counter + param writer // Publish initial state
Params params_memory("/dev/shm/params"); params_memory.put("DashcamState", "waiting");
int total_frames = 0; params_memory.put("DashcamFrames", "0");
double last_frame_count_write = 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 = [&]() { 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(); trip_dir = VIDEOS_BASE + "/" + make_timestamp();
mkdir(trip_dir.c_str(), 0755); mkdir(trip_dir.c_str(), 0755);
LOGW("dashcamd: new trip %s", trip_dir.c_str()); 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()); LOGW("dashcamd: opening segment %s", seg_name.c_str());
encoder->encoder_open((seg_name + ".mp4").c_str()); encoder->encoder_open((seg_name + ".mp4").c_str());
// Open companion SRT file
std::string srt_path = trip_dir + "/" + seg_name + ".srt"; std::string srt_path = trip_dir + "/" + seg_name + ".srt";
srt_file = fopen(srt_path.c_str(), "w"); srt_file = fopen(srt_path.c_str(), "w");
srt_index = 0; srt_index = 0;
@@ -229,38 +201,38 @@ int main(int argc, char *argv[]) {
last_srt_write = 0; last_srt_write = 0;
frame_count = 0; frame_count = 0;
trip_frames = 0;
segment_start_ts = nanos_since_boot(); segment_start_ts = nanos_since_boot();
state = RECORDING; state = RECORDING;
params_memory.put("DashcamState", "recording");
params_memory.put("DashcamFrames", "0");
}; };
// Helper: close current trip
auto close_trip = [&]() { auto close_trip = [&]() {
if (srt_file) { fclose(srt_file); srt_file = nullptr; } if (srt_file) { fclose(srt_file); srt_file = nullptr; }
if (encoder) { if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close(); encoder->encoder_close();
LOGW("dashcamd: segment closed"); LOGW("dashcamd: segment closed");
}
delete encoder; delete encoder;
encoder = nullptr; encoder = nullptr;
} }
state = TRIP_ENDED; state = WAITING;
frame_count = 0; frame_count = 0;
trip_frames = 0;
idle_timer_start = 0.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, params_memory.put("DashcamState", "waiting");
// the 10-min idle timer will stop the trip if drive is never detected. params_memory.put("DashcamFrames", "0");
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) { while (!do_exit) {
VisionBuf *buf = vipc.recv(); VisionBuf *buf = vipc.recv();
if (buf == nullptr) continue; 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++; recv_count++;
if (SOURCE_FPS > CAMERA_FPS && (recv_count % (SOURCE_FPS / CAMERA_FPS)) != 0) continue; 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::LOW ||
gear == cereal::CarState::GearShifter::MANUMATIC); gear == cereal::CarState::GearShifter::MANUMATIC);
// Detect ignition off→on transition (new ignition cycle = new trip) // Detect ignition off → close any active trip
if (started_initialized && !prev_started && started) { if (started_initialized && prev_started && !started) {
LOGW("dashcamd: ignition on — new cycle"); LOGW("dashcamd: ignition off");
if (state == RECORDING || state == IDLE_TIMEOUT) { if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip(); close_trip();
} }
// Start recording immediately, idle timer until drive is detected
start_new_trip();
idle_timer_start = now;
state = IDLE_TIMEOUT;
} }
prev_started = started; prev_started = started;
started_initialized = true; 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) { if (++param_check_counter >= CAMERA_FPS) {
param_check_counter = 0; param_check_counter = 0;
if (params.getBool("DashcamShutdown")) { if (params.getBool("DashcamShutdown")) {
LOGW("dashcamd: shutdown requested, closing trip"); LOGW("dashcamd: shutdown requested");
if (state == RECORDING || state == IDLE_TIMEOUT) { if (state == RECORDING || state == IDLE_TIMEOUT) {
close_trip(); close_trip();
} }
@@ -306,32 +274,30 @@ int main(int argc, char *argv[]) {
} }
} }
// State machine transitions // State machine
switch (state) { switch (state) {
case IDLE: case WAITING: {
case TRIP_ENDED: bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix();
if (in_drive) { if (in_drive && system_time_valid() && has_gps) {
start_new_trip(); start_new_trip();
} }
break; break;
}
case RECORDING: case RECORDING:
if (!in_drive) { if (!in_drive) {
// Car left drive — start idle timeout
idle_timer_start = now; idle_timer_start = now;
state = IDLE_TIMEOUT; 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; break;
case IDLE_TIMEOUT: case IDLE_TIMEOUT:
if (in_drive) { if (in_drive) {
// Back in drive — cancel timer, resume recording same trip
idle_timer_start = 0.0; idle_timer_start = 0.0;
state = RECORDING; state = RECORDING;
LOGW("dashcamd: back in drive, resuming trip"); LOGW("dashcamd: back in drive, resuming trip");
} else if ((now - idle_timer_start) >= IDLE_TIMEOUT_SECONDS) { } else if ((now - idle_timer_start) >= IDLE_TIMEOUT_SECONDS) {
// Timer expired — end trip
LOGW("dashcamd: idle timeout expired"); LOGW("dashcamd: idle timeout expired");
close_trip(); close_trip();
} }
@@ -368,19 +334,15 @@ int main(int argc, char *argv[]) {
continue; 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 // Feed NV12 frame directly to OMX encoder
encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts); encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts);
frame_count++; frame_count++;
total_frames++; trip_frames++;
// Write total frame count to params_memory every 5 seconds // Publish state every 5 seconds
if (now - last_frame_count_write >= 5.0) { if (now - last_param_write >= 5.0) {
params_memory.put("DashcamFrames", std::to_string(total_frames)); params_memory.put("DashcamFrames", std::to_string(trip_frames));
last_frame_count_write = now; last_param_write = now;
} }
// Write GPS subtitle at most once per second // Write GPS subtitle at most once per second
@@ -388,7 +350,6 @@ int main(int argc, char *argv[]) {
last_srt_write = now; last_srt_write = now;
srt_index++; srt_index++;
// Read GPS data
double lat = 0, lon = 0, speed_ms = 0; double lat = 0, lon = 0, speed_ms = 0;
bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix(); bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix();
if (has_gps) { if (has_gps) {
@@ -421,13 +382,11 @@ int main(int argc, char *argv[]) {
} }
// Clean exit // Clean exit
if (srt_file) { fclose(srt_file); srt_file = nullptr; }
if (encoder) {
if (state == RECORDING || state == IDLE_TIMEOUT) { if (state == RECORDING || state == IDLE_TIMEOUT) {
encoder->encoder_close(); close_trip();
}
delete encoder;
} }
params_memory.put("DashcamState", "stopped");
params_memory.put("DashcamFrames", "0");
LOGW("dashcamd: stopped"); LOGW("dashcamd: stopped");
return 0; return 0;

View File

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

View File

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

View File

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