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;
};
} // logger
which 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;
}
} // ibus
The 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
});
}
}
} // writer
The 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