#pragma once #include #include #include #include namespace fgc { enum class LogLevel { Trace = 0, Debug, Info, Warn, Error, Off }; // Wire-tracing categories. Orthogonal to LogLevel: a category emits verbatim // TX/RX lines and is toggled independently of the linear level, so you can // watch e.g. all serial traffic without turning everything else to Trace. // Values are a bitmask (multiple categories enabled at once). enum class LogCat : unsigned { None = 0, Serial = 1u << 0, Mqtt = 1u << 1, Camera = 1u << 2, Control = 1u << 3, All = Serial | Mqtt | Camera | Control, }; // Minimal leveled, thread-safe logger. Each log line is assembled in a // per-statement buffer and written atomically under a shared mutex, so lines // from different threads never interleave. Level filtering is global. // // Usage: LOG_INFO << "starting, rate=" << rate; // Lines at Warn/Error go to stderr; everything else to stdout. class Logger { public: // The CONSOLE threshold. Kept as the plain name because every existing // caller - `--log-level`, [Logging] level, the `debug` console command - // means the console. See setFileLevel() for the file's own. static void setLevel(LogLevel level); static LogLevel level(); static bool enabled(LogLevel level); // Parse "trace"|"debug"|"info"|"warn"|"error"|"off" (case-insensitive). // Returns false (and leaves the level unchanged) on an unknown string. static bool setLevelFromString(const std::string& s); // Wire-trace categories (independent of the linear level above). static void setCategories(unsigned mask); static unsigned categories(); static void enableCategory(LogCat cat); static void disableCategory(LogCat cat); static bool categoryEnabled(LogCat cat); // Parse a comma list: "serial,mqtt" | "all" | "none" (case-insensitive, // whitespace-trimmed). Recognized tokens always apply; an unknown token // sets *ok=false (when ok != nullptr) but does not discard the rest. static unsigned parseCategories(const std::string& csv, bool* ok = nullptr); // Map a single category name to its enum value. Unknown => LogCat::None and // *ok=false (when ok != nullptr). static LogCat catFromString(const std::string& name, bool* ok = nullptr); // Output sink for finished log lines. The string is the fully formatted line // (timestamp + "[LABEL]" + message, no trailing newline); `level` lets a sink // colour or route by severity. Installing a sink REPLACES the default // stdout/stderr writer entirely - used by the TUI to divert logs into an // on-screen pane so they never corrupt the rendered screen. Pass an empty // function to restore the default writer. Thread-safe. using LogSink = std::function; static void setSink(LogSink sink); // --- persistent file logging ------------------------------------------ // // A SECOND, independent sink slot. setSink() above belongs to the TUI, which // installs its pane writer on start and clears it on stop; a file sink put // through that one slot would be silently replaced, and the shutdown lines - // logged after ui->stop() - are exactly the ones worth keeping. // // It takes the CATEGORY as well as the level. A wire-trace line arrives with // level == Trace and is otherwise indistinguishable from a plain LOG_TRACE, // and the file has to tell them apart to honour "traces only when asked for". // // WARNING: the sink runs while this logger's mutex is held, and that mutex is // NOT recursive. A sink that calls LOG_* deadlocks the process - including // the control loop, possibly mid-move. Sinks must report failure by latching // a flag the caller polls, never by logging. using FileSink = std::function; static void setFileSink(FileSink sink); // --- tap ---------------------------------------------------------------- // // A THIRD independent slot: sees every line that reaches the console (the // same lines, the same filter), whether the console is the dashboard's pane // or stdout. It exists so a command that arrived over MQTT can be answered // with what it logged, without taking over either sink above. // // The same WARNING as the file sink applies: it runs under this logger's // non-recursive mutex, so it must only record - never log, never block, // never publish. Pass an empty function to remove it. using TapSink = std::function; static void setTap(TapSink tap); // The file's own threshold, independent of the console's, so the dashboard // can stay at info while the file keeps debug. // // The LOG_* macros gate on the MORE verbose of the two, and each sink then // filters to its own. Off (the default) means the file wants nothing, which // leaves every existing path byte-for-byte as it was. static void setFileLevel(LogLevel level); static LogLevel fileLevel(); static bool setFileLevelFromString(const std::string& s); // min(level(), fileLevel()) - what the macros actually gate on. static LogLevel effectiveLevel(); }; // RAII helper that buffers one log line and flushes it on commit(). // Designed to be driven by the LOG_* macros' for-loop guard. class LogStream { public: explicit LogStream(LogLevel level); LogStream(LogLevel level, LogCat cat); // category (wire-trace) line bool pending() const { return enabled_ && !done_; } void commit(); std::ostream& stream() { return buffer_; } private: LogLevel level_; LogCat cat_ = LogCat::None; // None => gated by level only bool enabled_; bool done_ = false; std::ostringstream buffer_; }; } // namespace fgc // Brace-safe: the whole macro is a single for-statement, so it composes // correctly inside if/else without dangling-else hazards, and the message // expression is never evaluated when the level is disabled. #define FGC_LOG_AT(level) \ for (::fgc::LogStream _fgc_ls(level); _fgc_ls.pending(); _fgc_ls.commit()) \ _fgc_ls.stream() #define LOG_TRACE FGC_LOG_AT(::fgc::LogLevel::Trace) #define LOG_DEBUG FGC_LOG_AT(::fgc::LogLevel::Debug) #define LOG_INFO FGC_LOG_AT(::fgc::LogLevel::Info) #define LOG_WARN FGC_LOG_AT(::fgc::LogLevel::Warn) #define LOG_ERROR FGC_LOG_AT(::fgc::LogLevel::Error) // Verbatim wire-trace line for one category. Prints when the category is // enabled (independent of the linear level, except level Off silences all). // The message expression is not evaluated when the category is off. #define LOG_TRACE_CAT(cat) \ for (::fgc::LogStream _fgc_ls(::fgc::LogLevel::Trace, (cat)); _fgc_ls.pending(); \ _fgc_ls.commit()) \ _fgc_ls.stream()