diff --git a/selfdrive/clearpilot/dashcamd b/selfdrive/clearpilot/dashcamd index 007f2c3..59ffc45 100755 Binary files a/selfdrive/clearpilot/dashcamd and b/selfdrive/clearpilot/dashcamd differ diff --git a/selfdrive/clearpilot/dashcamd.cc b/selfdrive/clearpilot/dashcamd.cc index c0b07df..a20edfe 100644 --- a/selfdrive/clearpilot/dashcamd.cc +++ b/selfdrive/clearpilot/dashcamd.cc @@ -39,6 +39,12 @@ * - thermald sets DashcamShutdown="1" before device power-off * - dashcamd closes current segment, sets DashcamShutdown="0" (ack), exits * - thermald waits up to 15s for ack, then proceeds with shutdown + * + * GPS subtitle track: + * - Each .mp4 segment has a companion .srt subtitle file + * - Updated at most once per second from gpsLocation cereal messages + * - Format: "35 MPH | 44.9216°N 93.3260°W | 2026-04-13 05:19:00 UTC" + * - Most players auto-detect .srt files alongside .mp4 files */ #include @@ -88,6 +94,26 @@ static bool system_time_valid() { return (tm.tm_year + 1900) >= 2024; } +static std::string make_utc_timestamp() { + char buf[32]; + time_t t = time(NULL); + struct tm tm = *gmtime(&t); + snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d UTC", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec); + return std::string(buf); +} + +// Format SRT timestamp: HH:MM:SS,mmm +static std::string srt_time(int seconds) { + int h = seconds / 3600; + int m = (seconds % 3600) / 60; + int s = seconds % 60; + char buf[16]; + snprintf(buf, sizeof(buf), "%02d:%02d:%02d,000", h, m, s); + return std::string(buf); +} + int main(int argc, char *argv[]) { setpriority(PRIO_PROCESS, 0, -10); @@ -116,8 +142,8 @@ int main(int argc, char *argv[]) { int uv_stride = y_stride; LOGW("dashcamd: connected %dx%d, stride=%d", width, height, y_stride); - // Subscribe to carState (gear, standstill) and deviceState (ignition/started) - SubMaster sm({"carState", "deviceState"}); + // Subscribe to carState (gear), deviceState (ignition), gpsLocation (subtitles) + SubMaster sm({"carState", "deviceState", "gpsLocation"}); Params params; // Trip state @@ -128,6 +154,12 @@ int main(int argc, char *argv[]) { uint64_t segment_start_ts = 0; double idle_timer_start = 0.0; + // SRT subtitle state + FILE *srt_file = nullptr; + int srt_index = 0; // subtitle entry counter (1-based) + int srt_segment_sec = 0; // seconds elapsed in current segment + double last_srt_write = 0; // monotonic time of last SRT write + // Ignition tracking for off→on detection bool prev_started = false; bool started_initialized = false; @@ -152,15 +184,24 @@ int main(int argc, char *argv[]) { 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()); + std::string seg_name = make_timestamp(); + LOGW("dashcamd: opening segment %s", seg_name.c_str()); + encoder->encoder_open((seg_name + ".mp4").c_str()); + + // Open companion SRT file + std::string srt_path = trip_dir + "/" + seg_name + ".srt"; + srt_file = fopen(srt_path.c_str(), "w"); + srt_index = 0; + srt_segment_sec = 0; + last_srt_write = 0; + frame_count = 0; segment_start_ts = nanos_since_boot(); state = RECORDING; }; auto close_trip = [&]() { + if (srt_file) { fclose(srt_file); srt_file = nullptr; } if (encoder) { if (state == RECORDING || state == IDLE_TIMEOUT) { encoder->encoder_close(); @@ -265,10 +306,19 @@ int main(int argc, char *argv[]) { // Segment rotation if (frame_count >= FRAMES_PER_SEGMENT) { + if (srt_file) { fclose(srt_file); srt_file = nullptr; } encoder->encoder_close(); - std::string filename = make_timestamp() + ".mp4"; - LOGW("dashcamd: opening segment %s", filename.c_str()); - encoder->encoder_open(filename.c_str()); + + std::string seg_name = make_timestamp(); + LOGW("dashcamd: opening segment %s", seg_name.c_str()); + encoder->encoder_open((seg_name + ".mp4").c_str()); + + std::string srt_path = trip_dir + "/" + seg_name + ".srt"; + srt_file = fopen(srt_path.c_str(), "w"); + srt_index = 0; + srt_segment_sec = 0; + last_srt_write = 0; + frame_count = 0; segment_start_ts = nanos_since_boot(); } @@ -278,9 +328,46 @@ int main(int argc, char *argv[]) { // Feed NV12 frame directly to OMX encoder encoder->encode_frame_nv12(buf->y, y_stride, buf->uv, uv_stride, width, height, ts); frame_count++; + + // Write GPS subtitle at most once per second + if (srt_file && (now - last_srt_write) >= 1.0) { + last_srt_write = now; + srt_index++; + + // Read GPS data + double lat = 0, lon = 0, speed_ms = 0; + bool has_gps = sm.valid("gpsLocation") && sm["gpsLocation"].getGpsLocation().getHasFix(); + if (has_gps) { + auto gps = sm["gpsLocation"].getGpsLocation(); + lat = gps.getLatitude(); + lon = gps.getLongitude(); + speed_ms = gps.getSpeed(); + } + + double speed_mph = speed_ms * 2.23694; + std::string utc = make_utc_timestamp(); + std::string t_start = srt_time(srt_segment_sec); + std::string t_end = srt_time(srt_segment_sec + 1); + srt_segment_sec++; + + if (has_gps) { + const char *deg = "\xC2\xB0"; // UTF-8 degree sign + fprintf(srt_file, "%d\n%s --> %s\n%.0f MPH | %.4f%s%c %.4f%s%c | %s\n\n", + srt_index, t_start.c_str(), t_end.c_str(), + speed_mph, + std::abs(lat), deg, lat >= 0 ? 'N' : 'S', + std::abs(lon), deg, lon >= 0 ? 'E' : 'W', + utc.c_str()); + } else { + fprintf(srt_file, "%d\n%s --> %s\nNo GPS | %s\n\n", + srt_index, t_start.c_str(), t_end.c_str(), utc.c_str()); + } + fflush(srt_file); + } } // Clean exit + if (srt_file) { fclose(srt_file); srt_file = nullptr; } if (encoder) { if (state == RECORDING || state == IDLE_TIMEOUT) { encoder->encoder_close();