257 lines
12 KiB
C++
257 lines
12 KiB
C++
#include "fgc/Application.h"
|
|
#include "fgc/Config.h"
|
|
#include "fgc/FileLogSink.h"
|
|
#include "fgc/Logger.h"
|
|
#include "fgc/Paths.h"
|
|
#include "fgc/SingleInstance.h"
|
|
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <boost/program_options.hpp>
|
|
|
|
namespace po = boost::program_options;
|
|
using namespace fgc;
|
|
|
|
namespace {
|
|
|
|
std::string want_or(const std::string& s, const char* fallback) {
|
|
return s.empty() ? std::string(fallback) : s;
|
|
}
|
|
|
|
// Written at the top of every log file, and again after each rotation, so an
|
|
// archive found on its own still says which build and tower produced it.
|
|
std::string fileLogHeader(const fgc::AppConfig& cfg, const std::string& cfg_path,
|
|
const std::string& hw_path, int argc, char* argv[]) {
|
|
std::ostringstream h;
|
|
h << "=== fire_gimbal_control ===\n";
|
|
h << "build " << FGC_GIT_REV << "\n";
|
|
h << "tower " << cfg.general.tower_name << "\n";
|
|
h << "pid " << ::getpid() << "\n";
|
|
h << "config " << cfg_path << "\n";
|
|
h << "hardware " << (hw_path.empty() ? "(none)" : hw_path) << "\n";
|
|
h << "argv ";
|
|
for (int i = 0; i < argc; ++i) h << ' ' << argv[i];
|
|
h << "\n===";
|
|
return h.str();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char* argv[]) {
|
|
po::options_description desc("Fire Gimbal Control options");
|
|
desc.add_options()
|
|
("help,h", "show this help")
|
|
("config,c", po::value<std::string>(), "path to config.ini")
|
|
("hardware", po::value<std::string>(),
|
|
"path to hardware.ini (default: $FGC_HARDWARE, /etc/fire_gimbal_control/hardware.ini, "
|
|
"or one beside config.ini)")
|
|
("no-mqtt", po::bool_switch(), "disable MQTT (use a null control channel)")
|
|
("mock-camera", po::bool_switch(), "use a simulated camera (no hardware)")
|
|
("mock-camera-full", po::bool_switch(),
|
|
"simulated camera with the real camera's 5328x3040 12-bit frames, so saving loads "
|
|
"the CPU as on a tower (implies --mock-camera)")
|
|
("mock-serial", po::bool_switch(), "use a simulated motor controller (no hardware)")
|
|
("mock-env", po::bool_switch(),
|
|
"use a simulated ambient temp/humidity sensor (implies enable_env)")
|
|
("tui", po::bool_switch(), "show the full-screen terminal dashboard")
|
|
("no-tui", po::bool_switch(), "force the headless line console (overrides config)")
|
|
("degraded", po::bool_switch(),
|
|
"start even though hardware.ini declares a subsystem this rig does not have "
|
|
"(field triage; says so loudly in the log)")
|
|
("no-lock", po::bool_switch(),
|
|
"skip the single-instance lock (dev only: for a second mock-only instance)")
|
|
("lock-file", po::value<std::string>(), "path to the single-instance lockfile")
|
|
("log-level", po::value<std::string>(),
|
|
"console verbosity: trace|debug|info|warn|error|off. Does NOT silence the log "
|
|
"file, which has its own level")
|
|
("no-log-file", po::bool_switch(), "do not write a persistent log file this run")
|
|
("log-file-level", po::value<std::string>(),
|
|
"log-file verbosity, independent of --log-level (default debug)")
|
|
("test", po::value<std::string>(),
|
|
"run one self-test once the motor link is up, then exit: 0 PASS, 4 FAIL, 5 could "
|
|
"not start. Takes what follows `test` at the console, e.g. --test hardware or "
|
|
"--test \"hardware motor\". Headless; ignores [Startup], so it neither homes nor "
|
|
"leaves manual")
|
|
("trace", po::value<std::string>(),
|
|
"verbatim wire trace, comma list: serial,mqtt,camera,control,all,none");
|
|
|
|
// The removed startup flags, accepted only so that using one says where the
|
|
// setting went instead of boost's bare "unrecognised option". A boot service
|
|
// still carrying them must not look like a typo.
|
|
po::options_description removed;
|
|
removed.add_options()
|
|
("init,i", po::bool_switch(), "")
|
|
("start,s", po::bool_switch(), "");
|
|
po::options_description all;
|
|
all.add(desc).add(removed);
|
|
|
|
po::variables_map vm;
|
|
try {
|
|
po::store(po::parse_command_line(argc, argv, all), vm);
|
|
po::notify(vm);
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Argument error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
if (vm.count("help")) {
|
|
std::cout << desc << "\n";
|
|
return 0;
|
|
}
|
|
if (vm["init"].as<bool>() || vm["start"].as<bool>()) {
|
|
std::cerr << "Argument error: --init and --start were removed. Whether the tower homes "
|
|
"and which mode it starts in are set in config.ini:\n"
|
|
" [Startup]\n"
|
|
" home = true ; was --init\n"
|
|
" mode = auto ; was --start (manual otherwise)\n"
|
|
"A boot service still passing them: re-run scripts/install-tower, which "
|
|
"moves them into config.ini.\n";
|
|
return 1;
|
|
}
|
|
if (vm.count("test")) {
|
|
const std::string t = vm["test"].as<std::string>();
|
|
if (t.find_first_not_of(" \t") == std::string::npos) {
|
|
std::cerr << "Argument error: --test needs a selection, e.g. --test hardware\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
const std::string cli_cfg = vm.count("config") ? vm["config"].as<std::string>() : "";
|
|
auto cfg_path = paths::resolveConfigPath(cli_cfg);
|
|
if (!cfg_path) {
|
|
std::cerr << "No config.ini found. Searched:\n";
|
|
for (const auto& p : paths::configSearchPaths(cli_cfg)) std::cerr << " - " << p << "\n";
|
|
std::cerr << "Run scripts/tower-config seed to create one in "
|
|
<< paths::towerConfigDir() << ", pass --config, or set $FGC_CONFIG.\n";
|
|
return 1;
|
|
}
|
|
|
|
// hardware.ini describes the machine itself and is optional: a tower that has
|
|
// not been migrated yet simply has none, and everything comes from config.ini
|
|
// exactly as before. An explicit --hardware that does not exist IS an error,
|
|
// though - it means someone expected an assembly description to be in force.
|
|
const std::string cli_hw = vm.count("hardware") ? vm["hardware"].as<std::string>() : "";
|
|
auto hw_path = paths::resolveHardwarePath(cli_hw, *cfg_path);
|
|
if (!cli_hw.empty() && !hw_path) {
|
|
std::cerr << "No hardware.ini at --hardware " << cli_hw << "\n";
|
|
return 1;
|
|
}
|
|
|
|
// Resolved before the config is even loaded, because the install block below
|
|
// needs them and it runs before Application::run() touches its own options.
|
|
const std::string opts_log_file_level =
|
|
vm.count("log-file-level") ? vm["log-file-level"].as<std::string>() : std::string();
|
|
const bool opts_no_log_file = vm["no-log-file"].as<bool>();
|
|
|
|
AppConfig cfg;
|
|
try {
|
|
cfg = ConfigLoader::loadFromFile(*cfg_path, hw_path.value_or(""));
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Config error (" << *cfg_path << "): " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
// Start the persistent log HERE - before the first LOG_INFO below, and well
|
|
// before Application::run().
|
|
//
|
|
// Two reasons it cannot wait: run() can refuse to start at
|
|
// reconcileHardware() and return before the UI exists, so a sink installed
|
|
// there would miss the very message explaining why the tower did not come
|
|
// up; and in TUI mode every line otherwise lands in a 500-line on-screen
|
|
// ring buffer that dies with the tmux session.
|
|
//
|
|
// Anything logged earlier than this (bad arguments, no config.ini found)
|
|
// happens before the log's own path is knowable, and stays console-only.
|
|
FileLogSink file_log;
|
|
if (cfg.logging.file && !opts_no_log_file) {
|
|
FileLogPolicy pol;
|
|
pol.dir = cfg.logging.file_dir;
|
|
pol.max_bytes = static_cast<long long>(cfg.logging.file_max_mb) * 1024 * 1024;
|
|
pol.keep_files = cfg.logging.file_keep;
|
|
|
|
LogLevel flvl = LogLevel::Debug; // the file is normally more verbose
|
|
const std::string want =
|
|
!opts_log_file_level.empty() ? opts_log_file_level : cfg.logging.file_level;
|
|
bool bad_level = false;
|
|
if (!want.empty()) {
|
|
LogLevel parsed{};
|
|
if (Logger::setFileLevelFromString(want)) parsed = Logger::fileLevel();
|
|
else bad_level = true;
|
|
if (!bad_level) flvl = parsed;
|
|
}
|
|
Logger::setFileLevel(flvl);
|
|
|
|
if (file_log.open(pol, fileLogHeader(cfg, *cfg_path, hw_path.value_or(""), argc, argv))) {
|
|
Logger::setFileSink([&file_log](LogLevel l, LogCat c, const std::string& line) {
|
|
file_log.write(l, c, line);
|
|
});
|
|
}
|
|
// Reported from OUT HERE, never from inside the sink: the sink runs
|
|
// under Logger's non-recursive mutex and logging from it deadlocks.
|
|
if (file_log.failed()) {
|
|
Logger::setFileLevel(LogLevel::Off);
|
|
LOG_WARN << "no log file: " << file_log.lastError();
|
|
} else {
|
|
LOG_INFO << "logging to " << file_log.path() << " (level "
|
|
<< want_or(want, "debug") << ", " << cfg.logging.file_keep << " x "
|
|
<< cfg.logging.file_max_mb << " MB)";
|
|
}
|
|
if (bad_level) LOG_WARN << "unknown Logging.file_level '" << want << "', using debug";
|
|
}
|
|
|
|
LOG_INFO << "Loaded config: " << *cfg_path;
|
|
if (paths::configInOldPlace(cli_cfg, *cfg_path))
|
|
LOG_WARN << "this tower's config.ini is still in the project folder, where a deploy "
|
|
"can reach it: run scripts/tower-config move to put it in "
|
|
<< paths::towerConfigDir();
|
|
|
|
// Only one instance may run: a second would fight the first over the motor's
|
|
// serial port, the camera, and the exposure store. The lock is held for the
|
|
// whole run and released when this scope ends (or the process dies).
|
|
SingleInstance lock;
|
|
if (!vm["no-lock"].as<bool>()) {
|
|
const std::string lock_path = vm.count("lock-file")
|
|
? paths::expandUser(vm["lock-file"].as<std::string>())
|
|
: SingleInstance::defaultLockPath();
|
|
lock = SingleInstance::acquire(lock_path);
|
|
if (!lock) {
|
|
std::cerr << "Cannot start: " << lock.error() << "\n";
|
|
return 2; // distinct from 1 so scripts/fgc can tell this apart
|
|
}
|
|
LOG_INFO << "Single-instance lock held: " << lock.path();
|
|
}
|
|
|
|
RuntimeOptions opts;
|
|
if (vm["no-mqtt"].as<bool>()) opts.use_mqtt = false;
|
|
if (vm["mock-camera"].as<bool>()) opts.mock_camera = true;
|
|
if (vm["mock-camera-full"].as<bool>()) {
|
|
opts.mock_camera = true;
|
|
opts.mock_camera_full = true;
|
|
}
|
|
if (vm["mock-serial"].as<bool>()) opts.mock_serial = true;
|
|
if (vm["mock-env"].as<bool>()) opts.mock_env = true;
|
|
opts.degraded = vm["degraded"].as<bool>();
|
|
if (vm["tui"].as<bool>()) opts.use_tui = true;
|
|
if (vm["no-tui"].as<bool>()) opts.use_tui = false; // --no-tui wins
|
|
if (vm.count("log-level")) opts.log_level = vm["log-level"].as<std::string>();
|
|
if (vm.count("trace")) opts.trace_categories = vm["trace"].as<std::string>();
|
|
opts.config_path = *cfg_path; // so the setup commands can save back to it
|
|
if (file_log.isOpen()) opts.log_file = [&file_log] { return file_log.path(); };
|
|
if (vm.count("test")) {
|
|
opts.run_test = vm["test"].as<std::string>();
|
|
opts.use_tui = false; // a script wants lines on stdout, not a dashboard
|
|
}
|
|
|
|
Application app(std::move(cfg), std::move(opts));
|
|
try {
|
|
return app.run();
|
|
} catch (const std::exception& e) {
|
|
// Startup can still reject the configuration once it touches the
|
|
// filesystem (a missing camera-mode directory, say). Report that the way
|
|
// a bad config.ini is reported, not as an unhandled terminate.
|
|
std::cerr << "Startup failed: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|