feat: dashcamd trip lifecycle, status indicator, CLAUDE.md updates
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:
44
CLAUDE.md
44
CLAUDE.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
@@ -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;
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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;");
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user