mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-17 00:52:45 -04:00
* indicator: RP2040 peripherals for the main firmware The SenseCAP Indicator RP2040 co-processor serves as a generic peripheral bridge over a serial protobuf link (interdevice.proto): - FakeI2C implements TwoWire and tunnels write and read transactions, so the standard sensor drivers and the I2C scan work unmodified on the bridged second bus (WIRE1) - FakeUART forwards GPS NMEA to the regular GPS driver - SD card access with chunked file transfers, paged directory listings and card statistics; device-ui loads map tiles and map styles from the card behind the RP2040 - link at 2M baud with 4KB chunks, message structs kept off task stacks Log messages carrying their own bracket tag render it like a thread name. Replaces the earlier IndicatorSensor/COBS approach. * indicator: address review Correlate responses with request ids, serialize the shared TX buffer, reject oversized frames, fix RX buffer overflow and NMEA truncation, full-length file paths. * indicator: assign the GPS FakeUART at runtime Static initialization order across translation units is undefined, so createGps() assigns and null-checks the bridged serial instead. Bound the NMEA length defensively. * indicator: bump device-ui pin to 27e6c0c * indicator: ping/pong link probe, non-blocking runOnce, FakeI2C locking The RP2040 sends nothing unsolicited without a GPS module attached, so wait_ready now probes with the new ping message instead of listening passively. runOnce skips its pump while a requester holds link_lock, keeping the main loop from blocking for a full request timeout. FakeI2C serializes transactions between the UI task and the main loop with an owner-tracked lock held from beginTransmission to transaction end. * indicator: link resync, config-honoring GPS, bridged-bus routing, stats validity Frame resync scans to the next magic instead of flushing the RX buffer, and the pump handles all buffered frames per pass. The RX drain reads in bulk and the protobuf encoder gets the correct buffer bound. GPS honors the gps_mode setting on the Indicator instead of always running. RTC, I2C keyboard and motion sensor drivers resolve WIRE1 through ScanI2CTwoWire::fetchI2CBus so bridged buses reach the right transport. FakeUART implements flush/availableForWrite/const-write from the Stream contract and fences its cross-core ring buffer. SdCardInfo.stats_valid is passed through to device-ui, and the remote FS backend gains the remove operation used for cleanup of failed tile saves. * indicator: retry lost link round trips, I2CResult UNSPECIFIED Remote FS operations retry once on a transport timeout. Correlation ids drop late responses of the first attempt; a retried append whose first attempt landed is recognized by the offset conflict carrying the resulting file size. Definitive failures are not retried, missing-tile probes stay a single round trip. Regenerated bindings add the I2CResult.Status UNSPECIFIED zero value so an empty result cannot decode as success. * indicator: nack responses, rename bridge classes to I2CProxy/UARTProxy A request the co-processor cannot decode or handle is nacked, so the requester fails fast instead of burning its timeout. All requests stage the shared tx_message under link_lock. FakeI2C and FakeUART are renamed to I2CProxy and UARTProxy after the pattern they implement, with their instances following suit. Drops dead code (unused NO_NEWS_PAUSE, unreachable not-running branches, doubled include guards) and the GPS pin log line that is meaningless on the tunneled port. * indicator: refuse a co-processor that speaks another protocol version The ping/pong handshake now carries InterdeviceVersion. A pong reporting a version other than ours means the RP2040 runs firmware that does not match this build, so the bridge stays shut down for the session and the mismatch is logged with both versions. Requests fail fast instead of being misinterpreted by the other side. * indicator: regen protos, interdevice protocol version 2 * indicator: per-task I2C contexts, gated handshake, retryable link failures The bridged I2C bus is shared between the main loop and the UI task, and TwoWire has no transaction bracket a lock can span: drivers drain the read buffer with available()/read() long after requestFrom() returned. Each calling task therefore gets its own staging and read buffers instead of a lock that could be left held (or that could not protect the read buffer anyway). The transaction is staged inside the link, under its lock. No request is sent before the co-processor has completed the version handshake, and runOnce keeps probing until it does, so a co-processor that boots slowly or reboots on its watchdog no longer leaves the bridge dead for the session. Requests in flight are counted, not flagged: two threads can be in a request and the first one out must not clear the other's state. File operations are retried on a lost frame and on a co-processor busy with card maintenance, but not on a refusal (nack) or a definitive failure, and they release the SPI lock while they wait so a slow link does not starve the radio. * indicator: fail safe on a peer mismatch, wait out card maintenance FileStatus moved to a fresh tag: reusing the tag of the removed success flag made every failure status decode as success on a peer that predates it. A card being mounted (busy) is retried rather than reported as an empty slot, and a co-processor busy with card maintenance is waited out: mounting takes seconds and the free space scan of a large card walks its whole FAT, which is not a reason to report a missing tile. The bridged I2C bus releases the SPI lock as well, so the keyboard scan on the UI task cannot starve the radio either. Slot claims in the I2C proxy are atomic, NMEA is not sent to a peer we refuse to talk to, and the handshake is completed by the unsolicited ping the co-processor sends when it has booted, which also reports a reboot. * indicator: regen protos, FileStatus back on the original tags * indicator: regen protos, ping/pong carry the InterdeviceVersion enum * indicator: point the protobufs submodule at the merged interdevice protos * indicator: pin device-ui to the branch with the remote SD support * indicator: honor the txOnly flag of flush, report dropped GPS writes flush() through a Stream pointer discarded the receive buffer: the flag is txOnly, and HardwareSerial::flush() keeps what has been received. write() reported bytes as written even when the link refused to send them. The link probe uses Throttle for its rate limit. * indicator: decide the log tag on the formatted message, hex request ids The thread tag was suppressed based on the printf template, which disagrees with the rendered message it is compared against: a format starting with a conversion could produce two tags, and one without a trailing bracket-space lost the tag entirely. vprintf now receives the thread name and picks. Also shifts only the bytes actually buffered after a frame, throttles with Throttle and logs request ids as hex. * indicator: SD mount, eject and format commands over the link * indicator: bound how long a busy card state blocks the UI task * indicator: a busy co-processor must not block the UI task for ever The busy retry re-armed its own budget on every busy answer, so a co-processor that stayed busy kept the caller in the loop with no way out. Transport retries and the wait for a busy card are now separate budgets that only count down. * indicator: start each request from an aligned receive buffer A byte run lost mid-response (a UART overflow during a 4KB tile chunk, when the display starves the RX interrupt) misaligns the assembly buffer. The buffer was never reset, so the poison outlived the request and cascaded into the following chunks of the same tile: one glitch dropped a whole multi-chunk tile, while single-chunk tiles resynced in the idle gap and survived. Each request now flushes the buffer first, bounding a glitch to the one chunk it hit. Adds resync/decode/timeout counters, logged rarely, to see the rate. * indicator: enlarge the LVGL heap for low-zoom map tiles The heap was 3MB and the image cache reserves 1.5MB of it, so a low-zoom map tile could not find a large enough contiguous block to decode and rendered white. 5MB of the 8MB PSRAM fixes it with room to spare. * indicator: advance the device-ui and protobufs pins to the merged commits Point the protobufs submodule at the merged SD command protos (protobufs #986) so it matches the checked in interdevice sources, and bump the device-ui archive to the current indicator branch tip that carries the SD button and format UI. * Update device-ui library dependency URL * remove cutom sdkconfig * remove duplicated synchronisation (after PR11278 is in place) * set commit reference to updated RemoteSDService class * Add board_level configuration for release * fix cppcheck errors --------- Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai> Co-authored-by: mverch67 <manuel.verch@gmx.de>
409 lines
13 KiB
C++
409 lines
13 KiB
C++
#include "RedirectablePrint.h"
|
|
#include "NodeDB.h"
|
|
#include "concurrency/OSThread.h"
|
|
#include "configuration.h"
|
|
#include "gps/RTC.h"
|
|
#include "main.h"
|
|
#include "memGet.h"
|
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
|
#include <assert.h>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <sys/time.h>
|
|
#include <time.h>
|
|
|
|
#ifdef ARCH_PORTDUINO
|
|
#include "platform/portduino/PortduinoGlue.h"
|
|
#endif
|
|
|
|
#if HAS_NETWORKING
|
|
extern meshtastic::Syslog syslog;
|
|
#endif
|
|
void RedirectablePrint::rpInit()
|
|
{
|
|
#ifdef HAS_FREE_RTOS
|
|
inDebugPrint = xSemaphoreCreateMutexStatic(&this->_MutexStorageSpace);
|
|
#endif
|
|
}
|
|
|
|
void RedirectablePrint::setDestination(Print *_dest)
|
|
{
|
|
assert(_dest);
|
|
dest = _dest;
|
|
}
|
|
|
|
size_t RedirectablePrint::write(uint8_t c)
|
|
{
|
|
// Always send the characters to our segger JTAG debugger
|
|
#ifdef USE_SEGGER
|
|
SEGGER_RTT_PutChar(SEGGER_STDOUT_CH, c);
|
|
#endif
|
|
// Account for legacy config transition
|
|
bool serialEnabled = config.has_security ? config.security.serial_enabled : config.device.serial_enabled;
|
|
if (!config.has_lora || serialEnabled)
|
|
dest->write(c);
|
|
|
|
return 1; // We always claim one was written, rather than trusting what the
|
|
// serial port said (which could be zero)
|
|
}
|
|
|
|
size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg, const char *threadName)
|
|
{
|
|
va_list copy;
|
|
#if ARCH_PORTDUINO
|
|
static char printBuf[512];
|
|
#else
|
|
static char printBuf[160];
|
|
#endif
|
|
|
|
#ifdef ARCH_PORTDUINO
|
|
bool color = !portduino_config.ascii_logs;
|
|
#else
|
|
bool color = true;
|
|
#endif
|
|
|
|
va_copy(copy, arg);
|
|
size_t len = vsnprintf(printBuf, sizeof(printBuf), format, copy);
|
|
va_end(copy);
|
|
|
|
// If the resulting string is longer than sizeof(printBuf)-1 characters, the remaining characters are still counted for the
|
|
// return value
|
|
|
|
if (len > sizeof(printBuf) - 1) {
|
|
len = sizeof(printBuf) - 1;
|
|
printBuf[sizeof(printBuf) - 2] = '\n';
|
|
}
|
|
for (size_t f = 0; f < len; f++) {
|
|
if (!std::isprint(static_cast<unsigned char>(printBuf[f])) && printBuf[f] != '\n')
|
|
printBuf[f] = '#';
|
|
}
|
|
// A message with its own "[Tag] " (the device-ui task has no OSThread)
|
|
// uses it instead of the thread name, printed uncolored like a real one.
|
|
size_t tagLen = 0;
|
|
if (printBuf[0] == '[') {
|
|
const char *end = (const char *)memchr(printBuf, ']', len < 24 ? len : 24);
|
|
if (end && (size_t)(end - printBuf) + 1 < len && end[1] == ' ')
|
|
tagLen = end - printBuf + 2;
|
|
}
|
|
if (tagLen)
|
|
Print::write(printBuf, tagLen);
|
|
else if (threadName) {
|
|
Print::write("[", 1);
|
|
Print::write(threadName, strlen(threadName));
|
|
Print::write("] ", 2);
|
|
}
|
|
if (color && logLevel != nullptr) {
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0)
|
|
Print::write("\u001b[34m", 5);
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0)
|
|
Print::write("\u001b[32m", 5);
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0)
|
|
Print::write("\u001b[33m", 5);
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0)
|
|
Print::write("\u001b[31m", 5);
|
|
}
|
|
len = tagLen + Print::write(printBuf + tagLen, len - tagLen);
|
|
if (color && logLevel != nullptr) {
|
|
Print::write("\u001b[0m", 4);
|
|
}
|
|
return len;
|
|
}
|
|
|
|
void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, va_list arg)
|
|
{
|
|
size_t r = 0;
|
|
|
|
#ifdef ARCH_PORTDUINO
|
|
bool color = !portduino_config.ascii_logs;
|
|
#else
|
|
bool color = true;
|
|
#endif
|
|
|
|
// include the header
|
|
if (color) {
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0)
|
|
Print::write("\u001b[34m", 5);
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0)
|
|
Print::write("\u001b[32m", 5);
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0)
|
|
Print::write("\u001b[33m", 5);
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0)
|
|
Print::write("\u001b[31m", 5);
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0)
|
|
Print::write("\u001b[35m", 5);
|
|
}
|
|
|
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile
|
|
if (rtc_sec > 0) {
|
|
long hms = rtc_sec % SEC_PER_DAY;
|
|
// hms += tz.tz_dsttime * SEC_PER_HOUR;
|
|
// hms -= tz.tz_minuteswest * SEC_PER_MIN;
|
|
// mod `hms` to ensure in positive range of [0...SEC_PER_DAY)
|
|
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
|
|
|
// Tear apart hms into h:m:s
|
|
int hour = hms / SEC_PER_HOUR;
|
|
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
|
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
|
|
|
#ifdef ARCH_PORTDUINO
|
|
::printf("%s ", logLevel);
|
|
if (color) {
|
|
::printf("\u001b[0m");
|
|
}
|
|
::printf("| %02d:%02d:%02d %u.%03u ", hour, min, sec, millis() / 1000, millis() % 1000);
|
|
#else
|
|
printf("%s ", logLevel);
|
|
if (color) {
|
|
printf("\u001b[0m");
|
|
}
|
|
printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000);
|
|
#endif
|
|
} else {
|
|
#ifdef ARCH_PORTDUINO
|
|
::printf("%s ", logLevel);
|
|
if (color) {
|
|
::printf("\u001b[0m");
|
|
}
|
|
::printf("| ??:??:?? %u.%03u ", millis() / 1000, millis() % 1000);
|
|
#else
|
|
printf("%s ", logLevel);
|
|
if (color) {
|
|
printf("\u001b[0m");
|
|
}
|
|
printf("| ??:??:?? %u ", millis() / 1000);
|
|
#endif
|
|
}
|
|
auto thread = concurrency::OSThread::currentThread;
|
|
// the tag is printed by vprintf, which knows whether the formatted
|
|
// message already carries one of its own
|
|
|
|
#ifdef DEBUG_HEAP
|
|
// Add heap free space bytes prefix before every log message
|
|
#ifdef ARCH_PORTDUINO
|
|
::printf("[heap %u] ", memGet.getFreeHeap());
|
|
#else
|
|
printf("[heap %u] ", memGet.getFreeHeap());
|
|
#endif
|
|
#endif // DEBUG_HEAP
|
|
|
|
r += vprintf(logLevel, format, arg, thread ? thread->ThreadName.c_str() : nullptr);
|
|
}
|
|
|
|
void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, va_list arg)
|
|
{
|
|
#if HAS_NETWORKING && !defined(ARCH_PORTDUINO)
|
|
// if syslog is in use, collect the log messages and send them to syslog
|
|
if (syslog.isEnabled()) {
|
|
int ll = 0;
|
|
switch (logLevel[0]) {
|
|
case 'D':
|
|
ll = SYSLOG_DEBUG;
|
|
break;
|
|
case 'I':
|
|
ll = SYSLOG_INFO;
|
|
break;
|
|
case 'W':
|
|
ll = SYSLOG_WARN;
|
|
break;
|
|
case 'E':
|
|
ll = SYSLOG_ERR;
|
|
break;
|
|
case 'C':
|
|
ll = SYSLOG_CRIT;
|
|
break;
|
|
default:
|
|
ll = 0;
|
|
}
|
|
auto thread = concurrency::OSThread::currentThread;
|
|
if (thread) {
|
|
syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg);
|
|
} else {
|
|
syslog.vlogf(ll, format, arg);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_list arg)
|
|
{
|
|
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
|
if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) {
|
|
bool isBleConnected = false;
|
|
#ifdef ARCH_ESP32
|
|
isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected();
|
|
#elif defined(ARCH_NRF52)
|
|
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
|
|
#elif defined(ARCH_NRF54L15)
|
|
isBleConnected = nrf54l15Bluetooth != nullptr && nrf54l15Bluetooth->isConnected();
|
|
#endif
|
|
if (isBleConnected) {
|
|
auto thread = concurrency::OSThread::currentThread;
|
|
meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero;
|
|
logRecord.level = getLogLevel(logLevel);
|
|
vsnprintf(logRecord.message, sizeof(logRecord.message), format, arg);
|
|
if (thread)
|
|
strlcpy(logRecord.source, thread->ThreadName.c_str(), sizeof(logRecord.source));
|
|
logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true);
|
|
|
|
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
|
|
size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
|
|
#ifdef ARCH_ESP32
|
|
nimbleBluetooth->sendLog(buffer.get(), size);
|
|
#elif defined(ARCH_NRF52)
|
|
nrf52Bluetooth->sendLog(buffer.get(), size);
|
|
#elif defined(ARCH_NRF54L15)
|
|
nrf54l15Bluetooth->sendLog(buffer.get(), size);
|
|
#endif
|
|
}
|
|
}
|
|
#else
|
|
(void)logLevel;
|
|
(void)format;
|
|
(void)arg;
|
|
#endif
|
|
}
|
|
|
|
meshtastic_LogRecord_Level RedirectablePrint::getLogLevel(const char *logLevel)
|
|
{
|
|
meshtastic_LogRecord_Level ll = meshtastic_LogRecord_Level_UNSET; // default to unset
|
|
switch (logLevel[0]) {
|
|
case 'D':
|
|
ll = meshtastic_LogRecord_Level_DEBUG;
|
|
break;
|
|
case 'I':
|
|
ll = meshtastic_LogRecord_Level_INFO;
|
|
break;
|
|
case 'W':
|
|
ll = meshtastic_LogRecord_Level_WARNING;
|
|
break;
|
|
case 'E':
|
|
ll = meshtastic_LogRecord_Level_ERROR;
|
|
break;
|
|
case 'C':
|
|
ll = meshtastic_LogRecord_Level_CRITICAL;
|
|
break;
|
|
}
|
|
return ll;
|
|
}
|
|
|
|
void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
|
{
|
|
|
|
// append \n to format
|
|
size_t len = strlen(format);
|
|
auto newFormat = std::unique_ptr<char[]>(new char[len + 2]);
|
|
strcpy(newFormat.get(), format);
|
|
newFormat[len] = '\n';
|
|
newFormat[len + 1] = '\0';
|
|
|
|
#if ARCH_PORTDUINO
|
|
// level trace is special, two possible ways to handle it.
|
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
|
if (portduino_config.traceFilename != "") {
|
|
va_list arg;
|
|
va_start(arg, format);
|
|
try {
|
|
traceFile << va_arg(arg, char *) << std::endl;
|
|
} catch (const std::ios_base::failure &e) {
|
|
}
|
|
va_end(arg);
|
|
}
|
|
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
|
return;
|
|
}
|
|
}
|
|
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
|
return;
|
|
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
|
|
return;
|
|
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
|
|
return;
|
|
}
|
|
#endif
|
|
if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
|
return;
|
|
}
|
|
|
|
#ifdef HAS_FREE_RTOS
|
|
if (inDebugPrint != nullptr && xSemaphoreTake(inDebugPrint, portMAX_DELAY) == pdTRUE) {
|
|
#else
|
|
if (!inDebugPrint) {
|
|
inDebugPrint = true;
|
|
#endif
|
|
|
|
va_list arg;
|
|
va_list arg_copy;
|
|
|
|
va_start(arg, format);
|
|
|
|
va_copy(arg_copy, arg);
|
|
log_to_serial(logLevel, newFormat.get(), arg_copy);
|
|
va_end(arg_copy);
|
|
|
|
va_copy(arg_copy, arg);
|
|
log_to_syslog(logLevel, newFormat.get(), arg_copy);
|
|
va_end(arg_copy);
|
|
|
|
log_to_ble(logLevel, newFormat.get(), arg);
|
|
|
|
va_end(arg);
|
|
#ifdef HAS_FREE_RTOS
|
|
xSemaphoreGive(inDebugPrint);
|
|
#else
|
|
inDebugPrint = false;
|
|
#endif
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void RedirectablePrint::hexDump(const char *logLevel, const unsigned char *buf, uint16_t len)
|
|
{
|
|
const char alphabet[17] = "0123456789abcdef";
|
|
log(logLevel, " +------------------------------------------------+ +----------------+");
|
|
log(logLevel, " |.0 .1 .2 .3 .4 .5 .6 .7 .8 .9 .a .b .c .d .e .f | | ASCII |");
|
|
for (uint16_t i = 0; i < len; i += 16) {
|
|
if (i % 128 == 0)
|
|
log(logLevel, " +------------------------------------------------+ +----------------+");
|
|
char s[] = " | | | |\n";
|
|
uint8_t ix = 5, iy = 56;
|
|
for (uint8_t j = 0; j < 16; j++) {
|
|
if (i + j < len) {
|
|
uint8_t c = buf[i + j];
|
|
s[ix++] = alphabet[(c >> 4) & 0x0F];
|
|
s[ix++] = alphabet[c & 0x0F];
|
|
ix++;
|
|
if (c > 31 && c < 128)
|
|
s[iy++] = c;
|
|
else
|
|
s[iy++] = '.';
|
|
}
|
|
}
|
|
uint8_t index = i / 16;
|
|
sprintf(s, "%03x", index);
|
|
s[3] = '.';
|
|
log(logLevel, s);
|
|
}
|
|
log(logLevel, " +------------------------------------------------+ +----------------+");
|
|
}
|
|
|
|
std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...)
|
|
{
|
|
int n = ((int)fmt_str.size()) * 2; /* Reserve two times as much as the length of the fmt_str */
|
|
std::unique_ptr<char[]> formatted;
|
|
va_list ap;
|
|
while (1) {
|
|
formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */
|
|
va_start(ap, fmt_str);
|
|
int final_n = vsnprintf(&formatted[0], n, fmt_str.c_str(), ap);
|
|
va_end(ap);
|
|
if (final_n < 0 || final_n >= n)
|
|
n += abs(final_n - n + 1);
|
|
else
|
|
break;
|
|
}
|
|
return std::string(formatted.get());
|
|
}
|