Inter-device Logging
What’s neat is that the system allows long-term storage of Pico log messages through IBUS on the PI. The system does this using the PicoToPiAndPiToPicoMessaging system.
Logging on the Pico
The Pico implements an Android-style logger interface that looks like this:
namespace pico::logger {
class BaseLogger {
public:
virtual void d(std::string tag, std::string message);
virtual void i(std::string tag, std::string message);
virtual void w(std::string tag, std::string message);
virtual void e(std::string tag, std::string message);
virtual void wtf(std::string tag, std::string message);
enum class Level { DEBUG, INFO, WARN, ERROR, WTF};
virtual void print(Level level, std::string tag, std::string message) = 0;
};
} // loggerwhich allows instances of BaseLogger to be injected into any class for debug logs.
Initialization
Initialization of the logger is done in the LoggerFactory, which depends on the Bus Topology
The LoggerFactory exposes a method buildAndGetLogger() which reads the bus topology, and calls
the factory methods logsToStdio() or logsToUart()
Stdio logs
This logger just does a Pico SDK printf of every log message that comes in.
void LoggerFactory::logsToStdio() {
//We want to print out all the logs to stdout, because stdout is setup to go over UART1,
//which goes to the picoprobe, which can be read from the laptop.
this->returnedBaseLogger = std::make_shared<logger::StdioPrintFLogger>();
}It was pretty handy early in the project when the IBUS logic wasn’t finished yet, and I needed a way to test the pico while it was being developed.
void StdioPrintFLogger::print(
pico::logger::BaseLogger::Level level,
std::string tag,
std::string message
) {
std::string levelString;
switch (level) {
case Level::DEBUG : { levelString = "DEBUG" ; break; }
case Level::INFO : { levelString = "INFO" ; break; }
case Level::WARN : { levelString = "WARN" ; break; }
case Level::ERROR : { levelString = "ERROR" ; break; }
case Level::WTF : { levelString = "WTF" ; break; }
default: { levelString = "" ; break; }
}
printf("[%s] [%s]:[%s] \n", levelString.c_str(), tag.c_str(), message.c_str());
}This can be seen in action here:
UART Logs
The logsToUart() method in the LoggerFactory makes an IbusOutLogger, which uses an IbusLoggerOutput writer.
namespace pico::logger::ibus {
void IBusOutLogger::print(Level level, std::string tag, std::string message) {
std::string levelString;
switch (level) {
case Level::DEBUG : { levelString = "D" ; break; }
case Level::INFO : { levelString = "I" ; break; }
case Level::WARN : { levelString = "W" ; break; }
case Level::ERROR : { levelString = "E" ; break; }
case Level::WTF : { levelString = "?" ; break; }
default: { levelString = "" ; break; }
}
loggerOutput->print(fmt::format("{} [{}] {}", levelString, tag, message));
}
IBusOutLogger::IBusOutLogger(std::shared_ptr<pico::ibus::output::writer::IbusLoggerOutput> loggerOutput) {
this->loggerOutput = loggerOutput;
}
} // ibusThe IbusOutLogger implements BaseLogger, mildly compresses the levelString for brevity, and sends the message to the loggerOutput writer.
namespace pico::ibus::output::writer {
IbusLoggerOutput::IbusLoggerOutput(
std::function<std::shared_ptr<dma::IDmaManager>()> dmaManagerAccessor
) {
this->dmaManagerAccessor = dmaManagerAccessor;
}
std::shared_ptr<dma::IDmaManager> IbusLoggerOutput::getDmaManager() {
return dmaManagerAccessor();
}
void IbusLoggerOutput::print(std::string message) {
if (dmaManagerAccessor() == nullptr) {
//We cannot log anything here until the dma manager has been set up.
return;
}
if (splitLogMessagesAtTruncationPoint) {
//TODO Split into multiple messages and pump them all out.
//TODO
} else {
//Just take the first part of the message and print one out.
std::string shortened = message.substr(0, std::min((int)message.size(), truncateLogMessagesToCharacters));
schedulePicoToPiMessageForWrite(messages::PicoToPiMessage {
.messageType = messages::PicoToPiMessage::MessageType::LogStatement,
.loggerMessage = shortened
});
}
}
} // writerThe use of the dmaManagerAccessor() is to prevent a dependency cycle. When the device is configured to use a UART logger, it will have log statements that happen before and during the DmaManager setup. As well, almost every dependency in the graph needs a logger, which needs a DmaManager, which needs a logger…. So, the pattern chosen is like the Dagger2 Provider<DmaManager> technique, allowing a late initialization on first use.
namespace pico::di {
void NoVideoFactory::initializeAllSmartPointers() {
//We have a knot. The logger factory needs dma manager to build a dependency,
//but dma manager needs a logger.
this->busTopologyManager = std::make_shared<pico::ibus::topology::BusTopologyManager>();
// When this is called, it'll return null, because the class variable isn't initialized yet.
std::function<std::shared_ptr<pico::ibus::dma::IDmaManager>()> dmaManagerAccessor = [this](){
return this->dmaManager;
};
this->loggerFactory = std::make_shared<logger::factory::LoggerFactory>(dmaManagerAccessor, busTopologyManager);
this->logger = loggerFactory->buildAndGetLogger();
this->observerRegistry = std::make_shared<ibus::observerRegistry::ObserverRegistry>(this->logger);
// Set the class variable for the dmaManager. Every invocation of the accessor can not get the DmaManager.
this->dmaManager = std::make_shared<ibus::dma::SingleCoreDmaManager>(logger, observerRegistry, busTopologyManager);
this->watchdogManager = std::make_shared<watchdog::WatchdogManager>(logger);
this->powerSwitchManager = std::make_shared<hardware::pi4powerswitch::GpioPi4PowerSwitchManager>(this->logger);
// ... more initializations....Pico Log Viewer
I built a screen in the e39-rpi HMI to view all the incoming IBUS messages, as well as the parsed PicoToPi (and outgoing PiToPico) messages.
Pi Logging Infrastructure
The e39-rpi HMI has a very similar logger class to the one on the Pico.
LogMessageParser
A LogMessageParser on the Pi listens for incoming Pico log messages, and forwards them to the pi logger, as well as the commsDebugChannel, which is a Channel that populates the PicoCommsDebugWindow.
@PlatformServiceInfo(
name = "LogMessageParser",
description = "Forwards Pico log messages to the Pi logger"
)
@PicoToPiParserGroup
class LogMessageParser @Inject constructor(
@Named(ApplicationModule.IBUS_MESSAGE_INGRESS) val incomingMessages: MutableSharedFlow<IBusMessage>,
@Named(ApplicationModule.IBUS_COMMS_DEBUG_CHANNEL) private val commsDebugChannel: MutableSharedFlow<IbusCommsDebugMessage>,
private val logger: Logger,
@Named(ConfiguredCarModule.SERVICE_COROUTINE_SCOPE) private val coroutineScope: CoroutineScope,
@Named(ConfiguredCarModule.SERVICE_COROUTINE_DISPATCHER) parsingDispatcher: CoroutineDispatcher
) : LongRunningService(coroutineScope, parsingDispatcher) {
override suspend fun doWork() {
incomingMessages
.filter { it.sourceDevice == IBusDevice.PICO && it.destinationDevice == IBusDevice.PI }
.collect { raw ->
val message = try {
PicoToPi.parser().parseFrom(raw.data.toByteArray())
} catch (e: Throwable) {
logger.e("LogMessageParser", "Could not parse protobuf", e)
return@collect
}
if (message.messageType == PicoToPi.MessageType.LogStatement) {
onLogMessage(raw, message)
}
}
}
suspend fun onLogMessage(raw: IBusMessage, parsed: PicoToPi) {
commsDebugChannel.emit(
IbusCommsDebugMessage.IncomingMessage.PicoToPiMessage(
Instant.now(),
raw,
parsed
)
)
logger.i("PICO", parsed.loggerStatement)
}
}Pi Logging Framework
The Pi has a very simple home-brew logging interface that is injected everywhere
interface Logger {
fun v(tag : String, msg : String)
fun d(tag : String, msg : String)
fun i(tag : String, msg : String)
fun w(tag : String, msg : String)
fun e(tag : String, msg : String)
fun e(tag : String, msg : String, e : Throwable)
}
@ApplicationScope
class StdOutLogger @Inject constructor() : Logger {
override fun v(tag: String, msg: String) {
println("VERBOSE : $tag / $msg")
}
override fun d(tag: String, msg: String) {
println("DEBUG : $tag / $msg")
}
override fun i(tag: String, msg: String) {
println("INFO : $tag / $msg")
}
override fun w(tag: String, msg: String) {
println("WARN : $tag / $msg")
}
override fun e(tag: String, msg: String) {
println("ERROR : $tag / $msg")
}
override fun e(tag: String, msg: String, e: Throwable) {
e(tag, "$msg exception: {${e.toString()}} ${e.message} ${e.printStackTrace()}")
}
}When the Pi is run on the device, it’s run by a startup script that pipes the output of stdout to a file. This allows reading the logs later by ssh-ing into the device. Some day I can setup forwarding from this interface to JournalD, Log4j, or whatever.
Return to Top