mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
376 lines
15 KiB
C++
376 lines
15 KiB
C++
#include <getopt.h>
|
|
|
|
#include <cstdlib>
|
|
#include <algorithm>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <poll.h>
|
|
#include <fcntl.h>
|
|
#include <filesystem>
|
|
#include <regex>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#ifdef __linux__
|
|
#include <sys/prctl.h>
|
|
#endif
|
|
|
|
#include "json11/json11.hpp"
|
|
|
|
#include "common/prefix.h"
|
|
#include "common/timing.h"
|
|
#include "tools/replay/consoleui.h"
|
|
#include "tools/replay/replay.h"
|
|
#include "tools/replay/util.h"
|
|
|
|
const std::string helpText =
|
|
R"(Usage: replay [options] [route]
|
|
Options:
|
|
-a, --allow Whitelist of services to send (comma-separated)
|
|
-b, --block Blacklist of services to send (comma-separated)
|
|
-c, --cache Cache <n> segments in memory. Default is 5
|
|
-s, --start Start from <seconds>
|
|
-x, --playback Playback <speed>
|
|
--demo Use a demo route instead of providing your own
|
|
--auto Auto load the route from the best available source (no video):
|
|
internal, openpilotci, comma_api, car_segments, testing_closet
|
|
-d, --data_dir Local directory with routes
|
|
-p, --prefix Set OPENPILOT_PREFIX
|
|
--cabin Load cabin camera (alias: --dcam)
|
|
--wide-road Load wide road camera (alias: --ecam)
|
|
--no-loop Stop at the end of the route
|
|
--no-cache Turn off local cache
|
|
--qcam Load qcamera
|
|
--no-hw-decoder Disable HW video decoding
|
|
--no-vipc Do not output video
|
|
--all Output all messages including bookmarkButton, uiDebug, userBookmark
|
|
--benchmark Run in benchmark mode (process all events then exit with stats)
|
|
-h, --help Show this help message
|
|
--machine Isolated local replay controlled by newline JSON on stdin
|
|
--start-segment Start machine replay at a recorded segment boundary
|
|
--preload-seek Prepare the next cached segment before machine seeks resume
|
|
)";
|
|
|
|
struct ReplayConfig {
|
|
std::string route;
|
|
std::vector<std::string> allow;
|
|
std::vector<std::string> block;
|
|
std::string data_dir;
|
|
std::string prefix;
|
|
uint32_t flags = REPLAY_FLAG_NONE;
|
|
bool auto_source = false;
|
|
int start_seconds = 0;
|
|
int start_segment = -1;
|
|
int cache_segments = -1;
|
|
float playback_speed = -1;
|
|
bool machine = false;
|
|
};
|
|
|
|
bool parseArgs(int argc, char *argv[], ReplayConfig &config) {
|
|
const struct option cli_options[] = {
|
|
{"allow", required_argument, nullptr, 'a'},
|
|
{"block", required_argument, nullptr, 'b'},
|
|
{"cache", required_argument, nullptr, 'c'},
|
|
{"start", required_argument, nullptr, 's'},
|
|
{"playback", required_argument, nullptr, 'x'},
|
|
{"demo", no_argument, nullptr, 0},
|
|
{"auto", no_argument, nullptr, 0},
|
|
{"data_dir", required_argument, nullptr, 'd'},
|
|
{"prefix", required_argument, nullptr, 'p'},
|
|
{"cabin", no_argument, nullptr, 0},
|
|
{"dcam", no_argument, nullptr, 0}, // deprecated alias
|
|
{"wide-road", no_argument, nullptr, 0},
|
|
{"ecam", no_argument, nullptr, 0}, // deprecated alias
|
|
{"no-loop", no_argument, nullptr, 0},
|
|
{"no-cache", no_argument, nullptr, 0},
|
|
{"qcam", no_argument, nullptr, 0},
|
|
{"no-hw-decoder", no_argument, nullptr, 0},
|
|
{"no-vipc", no_argument, nullptr, 0},
|
|
{"all", no_argument, nullptr, 0},
|
|
{"benchmark", no_argument, nullptr, 0},
|
|
{"machine", no_argument, nullptr, 0},
|
|
{"start-segment", required_argument, nullptr, 0},
|
|
{"preload-seek", no_argument, nullptr, 0},
|
|
{"help", no_argument, nullptr, 'h'},
|
|
{nullptr, 0, nullptr, 0}, // Terminating entry
|
|
};
|
|
|
|
const std::map<std::string, REPLAY_FLAGS> flag_map = {
|
|
{"cabin", REPLAY_FLAG_CABIN_CAMERA},
|
|
{"dcam", REPLAY_FLAG_CABIN_CAMERA}, // deprecated alias
|
|
{"wide-road", REPLAY_FLAG_WIDE_ROAD},
|
|
{"ecam", REPLAY_FLAG_WIDE_ROAD}, // deprecated alias
|
|
{"no-loop", REPLAY_FLAG_NO_LOOP},
|
|
{"no-cache", REPLAY_FLAG_NO_FILE_CACHE},
|
|
{"qcam", REPLAY_FLAG_QCAMERA},
|
|
{"no-hw-decoder", REPLAY_FLAG_NO_HW_DECODER},
|
|
{"no-vipc", REPLAY_FLAG_NO_VIPC},
|
|
{"all", REPLAY_FLAG_ALL_SERVICES},
|
|
{"benchmark", REPLAY_FLAG_BENCHMARK},
|
|
{"preload-seek", REPLAY_FLAG_PRELOAD_SEEK},
|
|
};
|
|
|
|
if (argc == 1) {
|
|
std::cout << helpText;
|
|
return false;
|
|
}
|
|
|
|
int opt, option_index = 0;
|
|
while ((opt = getopt_long(argc, argv, "a:b:c:s:x:d:p:h", cli_options, &option_index)) != -1) {
|
|
switch (opt) {
|
|
case 'a': config.allow = split(optarg, ','); break;
|
|
case 'b': config.block = split(optarg, ','); break;
|
|
case 'c': config.cache_segments = std::atoi(optarg); break;
|
|
case 's': config.start_seconds = std::atoi(optarg); break;
|
|
case 'x': config.playback_speed = std::atof(optarg); break;
|
|
case 'd': config.data_dir = optarg; break;
|
|
case 'p': config.prefix = optarg; break;
|
|
case 0: {
|
|
std::string name = cli_options[option_index].name;
|
|
if (name == "demo") config.route = DEMO_ROUTE;
|
|
else if (name == "auto") config.auto_source = true;
|
|
else if (name == "machine") config.machine = true;
|
|
else if (name == "start-segment") {
|
|
if (!std::regex_match(optarg, std::regex("[0-9]{1,6}"))) return false;
|
|
config.start_segment = std::atoi(optarg);
|
|
}
|
|
else config.flags |= flag_map.at(name);
|
|
break;
|
|
}
|
|
case 'h': std::cout << helpText; return false;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
// Check for a route name (first positional argument)
|
|
if (config.route.empty() && optind < argc) {
|
|
config.route = argv[optind];
|
|
}
|
|
|
|
if (config.route.empty()) {
|
|
std::cerr << "No route provided. Use --help for usage information.\n";
|
|
return false;
|
|
}
|
|
if (config.start_segment >= 0 && !config.machine) return false;
|
|
if ((config.flags & REPLAY_FLAG_PRELOAD_SEEK) && !config.machine) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool privateDirectory(const char *path) {
|
|
if (!path || path[0] != '/') return false;
|
|
struct stat info = {};
|
|
if (lstat(path, &info) || !S_ISDIR(info.st_mode) || info.st_uid != getuid() || (info.st_mode & 07777) != 0700) return false;
|
|
std::error_code error;
|
|
const auto resolved = std::filesystem::canonical(path, error).string();
|
|
if (error) return false;
|
|
return resolved != "/data/params" && resolved.rfind("/data/params/", 0) != 0;
|
|
}
|
|
|
|
bool machineContext(const ReplayConfig &config) {
|
|
const std::string prefix = util::getenv("OPENPILOT_PREFIX", "");
|
|
const std::string clock = util::getenv("SP_REPLAY_CLOCK_PATH", "");
|
|
return util::getenv("SP_REPLAY_CONTEXT", "") == "1" &&
|
|
std::regex_match(prefix, std::regex("replay-[A-Za-z0-9_-]{1,48}")) &&
|
|
privateDirectory(getenv("PARAMS_ROOT")) && privateDirectory(getenv("COMMA_CACHE")) &&
|
|
clock == "/dev/shm/msgq_" + prefix + "/display-clock" &&
|
|
!config.data_dir.empty() && config.data_dir.front() == '/' && config.prefix.empty() &&
|
|
!config.auto_source && config.allow.empty() && config.block.empty() &&
|
|
!(config.flags & (REPLAY_FLAG_ALL_SERVICES | REPLAY_FLAG_BENCHMARK));
|
|
}
|
|
|
|
static std::atomic<bool> machine_exit = false;
|
|
|
|
int runMachine(Replay &replay, int output, int start_segment) {
|
|
auto interrupted = [](int) { machine_exit = true; };
|
|
std::signal(SIGTERM, interrupted);
|
|
std::signal(SIGINT, interrupted);
|
|
std::signal(SIGHUP, interrupted);
|
|
replay.setLoadAbort(&machine_exit);
|
|
std::signal(SIGPIPE, SIG_IGN);
|
|
fcntl(output, F_SETFL, fcntl(output, F_GETFL) | O_NONBLOCK);
|
|
std::string input, last_request, pending_request, command_error;
|
|
uint64_t pending_epoch = 0;
|
|
auto report = [&]() {
|
|
json11::Json::array ranges, segment_ranges;
|
|
for (const auto &range : replay.seekRanges()) {
|
|
ranges.emplace_back(json11::Json::array{range.begin, range.end});
|
|
segment_ranges.emplace_back(json11::Json::array{range.segment, range.begin, range.end});
|
|
}
|
|
json11::Json::array warnings;
|
|
if (replay.legacyClock()) warnings.emplace_back("Legacy recording uses original timestamps; exact camera/HUD clock alignment is unavailable.");
|
|
if (replay.timelineIncomplete()) warnings.emplace_back("Timeline history is incomplete; unavailable evidence remains neutral.");
|
|
const auto value = json11::Json::object{
|
|
{"event", "status"}, {"state", replay.state()}, {"position", replay.currentSeconds()},
|
|
{"start", replay.minSeconds()}, {"end", replay.maxSeconds()}, {"rate", replay.getSpeed()},
|
|
{"paused", replay.isPaused()}, {"epoch", double(replay.epoch())},
|
|
{"routeStartNs", double(replay.routeStartNanos())}, {"wallMinusMonoNs", std::to_string(replay.wallMinusMonoNanos())},
|
|
{"seekRanges", ranges}, {"segmentRanges", segment_ranges}, {"sourceWarnings", warnings}, {"lastRequestId", last_request},
|
|
{"timelineState", replay.timelineState()},
|
|
{"error", command_error.empty() ? replay.error() : command_error}};
|
|
const auto line = json11::Json(value).dump() + "\n";
|
|
return write(output, line.data(), line.size()) == ssize_t(line.size());
|
|
};
|
|
if (!report() || !replay.load()) { report(); return 2; }
|
|
if (start_segment < 0) {
|
|
replay.start();
|
|
} else {
|
|
const auto &ranges = replay.seekRanges();
|
|
auto selected = std::find_if(ranges.begin(), ranges.end(), [start_segment](const auto &range) { return range.segment == start_segment; });
|
|
if (selected == ranges.end()) {
|
|
command_error = "Selected segment has no playable footage";
|
|
report();
|
|
return 2;
|
|
}
|
|
replay.seekTo(selected->begin, false);
|
|
}
|
|
while (!machine_exit) {
|
|
if (!pending_request.empty() && replay.appliedEpoch() >= pending_epoch) {
|
|
last_request = std::move(pending_request);
|
|
pending_request.clear();
|
|
}
|
|
if (!report()) break;
|
|
if (replay.state() == "failed") return 2;
|
|
pollfd source = {.fd = STDIN_FILENO, .events = POLLIN};
|
|
const int ready = poll(&source, 1, 50);
|
|
if (ready < 0) { if (errno == EINTR) continue; break; }
|
|
if (!ready) continue;
|
|
if (source.revents & (POLLERR | POLLNVAL)) break;
|
|
char chunk[4096];
|
|
const auto count = read(STDIN_FILENO, chunk, sizeof(chunk));
|
|
if (count <= 0) break;
|
|
input.append(chunk, count);
|
|
if (input.size() > 4096) { std::cerr << "Machine command exceeds limit\n"; return 2; }
|
|
size_t newline;
|
|
while ((newline = input.find('\n')) != std::string::npos) {
|
|
std::string error;
|
|
auto command = json11::Json::parse(input.substr(0, newline), error);
|
|
input.erase(0, newline + 1);
|
|
const auto op = command["op"].string_value(), request = command["requestId"].string_value();
|
|
if (!error.empty() || !command.is_object() || !std::regex_match(request, std::regex("[a-fA-F0-9]{1,64}"))) return 2;
|
|
const size_t expected = op == "stop" ? 2 : 3;
|
|
if (command.object_items().size() != expected) return 2;
|
|
command_error.clear();
|
|
if (op == "stop") {
|
|
replay.stop();
|
|
replay.pause(true);
|
|
last_request = request;
|
|
report();
|
|
return 0;
|
|
} else if (op == "pause" && command["paused"].is_bool()) {
|
|
replay.pause(command["paused"].bool_value());
|
|
last_request = request;
|
|
} else if (op == "rate" && command["rate"].is_number() &&
|
|
(command["rate"].number_value() == .5 || command["rate"].number_value() == 1 || command["rate"].number_value() == 2)) {
|
|
replay.setSpeed(command["rate"].number_value());
|
|
last_request = request;
|
|
} else if (op == "seek" && command["position"].is_number()) {
|
|
if (replay.canSeek(command["position"].number_value())) {
|
|
replay.seekTo(command["position"].number_value(), false);
|
|
pending_request = request;
|
|
pending_epoch = replay.epoch();
|
|
} else {
|
|
command_error = "invalid_position";
|
|
last_request = request;
|
|
}
|
|
} else return 2;
|
|
}
|
|
}
|
|
replay.stop();
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
#ifdef __APPLE__
|
|
// With all sockets opened, we might hit the default limit of 256 on macOS
|
|
util::set_file_descriptor_limit(1024);
|
|
#endif
|
|
|
|
// The vendored ncurses static library has a wrong compiled-in terminfo path.
|
|
// Point it at the system terminfo database if not already set.
|
|
setenv("TERMINFO_DIRS", "/usr/share/terminfo:/lib/terminfo:/usr/lib/terminfo", 0);
|
|
|
|
ReplayConfig config;
|
|
|
|
if (!parseArgs(argc, argv, config)) {
|
|
return 1;
|
|
}
|
|
|
|
int machine_output = -1;
|
|
if (config.machine) {
|
|
machine_output = dup(STDOUT_FILENO);
|
|
dup2(STDERR_FILENO, STDOUT_FILENO); // Native libraries may print diagnostics to stdout.
|
|
if (!machineContext(config)) {
|
|
std::cerr << "Machine replay requires an owned private local replay context\n";
|
|
return 2;
|
|
}
|
|
#ifdef __linux__
|
|
const auto parent = util::getenv("SP_REPLAY_PARENT_PID", "");
|
|
if (parent != std::to_string(getppid()) || prctl(PR_SET_PDEATHSIG, SIGKILL) || parent != std::to_string(getppid())) {
|
|
std::cerr << "Machine replay parent is unavailable\n";
|
|
return 2;
|
|
}
|
|
#else
|
|
return 2;
|
|
#endif
|
|
config.flags |= REPLAY_FLAG_LOCAL_MACHINE | REPLAY_FLAG_NO_LOOP | REPLAY_FLAG_NO_FILE_CACHE;
|
|
config.allow = {"modelV2", "controlsState", "lateralDelay", "lateralTorqueParameters", "onroadEvents", "extrinsicsCalibration",
|
|
"radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2",
|
|
"narrowRoadCameraState", "wideRoadCameraState", "cabinCameraState", "managerState", "selfdriveState", "starpilotSelfdriveState",
|
|
"starpilotLateralState", "starpilotNavigation", "longitudinalPlan", "gpsLocationExternal", "carOutput", "carControl",
|
|
"slcState", "vehicleParameters", "customReserved6", "spotMonitorState", "clocks", "narrowRoadEncodeIdx", "wideRoadEncodeIdx", "cabinEncodeIdx"};
|
|
try {
|
|
Replay replay(config.route, config.allow, {}, nullptr, config.flags, config.data_dir, false);
|
|
if (config.cache_segments > 0) replay.setSegmentCacheLimit(config.cache_segments);
|
|
return runMachine(replay, machine_output, config.start_segment);
|
|
} catch (const std::exception &error) {
|
|
std::cerr << "Machine replay failed: " << error.what() << '\n';
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<OpenpilotPrefix> op_prefix;
|
|
if (!config.prefix.empty()) {
|
|
op_prefix = std::make_unique<OpenpilotPrefix>(config.prefix);
|
|
}
|
|
|
|
Replay replay(config.route, config.allow, config.block, nullptr, config.flags, config.data_dir, config.auto_source);
|
|
if (config.cache_segments > 0) {
|
|
replay.setSegmentCacheLimit(config.cache_segments);
|
|
}
|
|
if (config.playback_speed > 0) {
|
|
replay.setSpeed(std::clamp(config.playback_speed, ConsoleUI::speed_array.front(), ConsoleUI::speed_array.back()));
|
|
}
|
|
if (!replay.load()) {
|
|
return 1;
|
|
}
|
|
|
|
if (config.flags & REPLAY_FLAG_BENCHMARK) {
|
|
replay.start(config.start_seconds);
|
|
replay.waitForFinished();
|
|
|
|
const auto &stats = replay.getBenchmarkStats();
|
|
uint64_t process_start = stats.process_start_ts;
|
|
|
|
std::cout << "\n===== REPLAY BENCHMARK RESULTS =====\n";
|
|
std::cout << "Route: " << replay.route().name() << "\n\n";
|
|
|
|
std::cout << "TIMELINE:\n";
|
|
std::cout << " t=0 ms process start\n";
|
|
for (const auto &[ts, event] : stats.timeline) {
|
|
double ms = (ts - process_start) / 1e6;
|
|
std::cout << " t=" << std::fixed << std::setprecision(0) << ms << " ms"
|
|
<< std::string(std::max(1, 8 - static_cast<int>(std::to_string(static_cast<int>(ms)).length())), ' ')
|
|
<< event << "\n";
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
ConsoleUI console_ui(&replay);
|
|
replay.start(config.start_seconds);
|
|
return console_ui.exec();
|
|
}
|