已解决:如何在运行时重新路由 Arduino/ESP 串行输出?

问题描述

我在 Arduino 环境中使用了一个日志库,该库主要依赖于预处理器魔法 - 为了不将任何代码编译成不符合设置日志级别的二进制文件,并且具有可用的文件名、函数名和行号事不宜迟。

输出通常使用 Serial.printf 完成。所以像

这样的声明
LOG_N("%02X/%02X @%d/%d? (heap=%d)\n",sv,fc,ad,ln,ESP.getFreeHeap() - lastHeap);

扩展为

if (MBUlogLvl >= level) Serial.printf("[N] %lu| %-20s [%4d] %s: %02X/%02X @%d/%d? (heap=%d)\n",millis(),file_name(__FILE__),__LINE__,__func__,ESP.getFreeHeap() - lastHeap);

(涉及几个中间预处理器宏扩展)

Serial.printf 在内部是 LOGDEVICE->printf,在其他地方有 Print *LOGDEVICE = &Serial;

虽然只要我能够使用 Serial,它就可以正常工作,但它不适用于 MCU 位于现场某处而无法访问 Serial 输出的情况。我确实有另一个帮助程序类设置 Telnet 服务器,其中日志输出分发给所有连接的客户端。

不过,我无法将两者连接起来。我似乎无法在编译或运行时成功地将 Serial.printf 切换到所需的 TelnetLog.printf

到目前为止我尝试过:

  • #define LOGSTMT TelnetLog.printf 并使用 LOGSTMT 而不是 LOGDEVICE->printf。这里的问题是需要在日志库头文件中进行修改以影响代码的所有部分(该库也用于其他外部提供的库)。我可以修改日志库,但只能修改一次,以免重复破坏其他库的代码
  • using LOGDEVICE = &tlog; - tlog 是类 TelnetLog一个实例,它派生自 Print。虽然这在原则上是有效的,但由于 LOGDEVICE 是指向 Print 的指针,因此未使用 TelnetLog.printf,而是使用 Print.write - 进而 TelnetLog.write函数用于输出,因为 Print.write 是虚拟的。这对于提供多个 TCP 客户端来说是非常低效的,因为每个字符都将单独发送。
  • 使用指向 printfSerial 的相应 TelnetLog函数指针。我根本没有设法编译一些东西。

这是我的示例代码。为了便于阅读,我在这里使用了 AltLog 类而不是 TelnetLogAltLog 只是将输出加倍到串行:

#include <Arduino.h>
#include <typeinfo>

class AltLog : public Print {
public:
  size_t write(uint8_t c) {
    Serial.printf("/1: %c",c);
    Serial.printf("/2: %c",c);
    return 5;
  }

  template <typename... Args>
  size_t printf(const char *format,Args&&... args) {
    size_t len = 0;
    char fmt[strlen(format) + 4];
    strcpy(fmt,"/1: ");
    strcat(fmt,format);
    len = Serial.printf(fmt,std::forward<Args>(args) ...);
    strcpy(fmt,"/2: ");
    strcat(fmt,std::forward<Args>(args) ...);
    return len;
  }
};

AltLog a;

Print *device = &Serial;

void setup() {
  Serial.begin(115200);
  Serial.println("");
  Serial.println("_OK_");
  delay(5000);

  Serial.printf("%s\n",typeid(*device).name());
  device->printf("Serial.%c\n",'S');

  device = &a;

  Serial.printf("%s\n",typeid(*device).name());
  dynamic_cast<AltLog *>(device)->printf("Alternate.%c\n",'A');
  device->printf("Alternate.%c\n",'A');

}

void loop() {
}

确实有效,但它不是解决方案,因为它需要 dynamic_cast 我在日志库中没有。输出如下:


_OK_
14HardwareSerial
Serial.S
6AltLog
/1: Alternate.A
/2: Alternate.A
/1: A/2: A/1: l/2: l/1: t/2: t/1: e/2: e/1: r/2: r/1: n/2: n/1: a/2: a/1: t/2: t/1: e/2: e/1: ./2: ./1: A/2: A/1:
/2:

有什么建议吗?

解决方法

在我的特殊情况下,我找到了一个解决方案(不过,一般问题仍然存在)。

Arduino/ESP32 内核的 Print 类具有另一个虚拟函数 size_t write(const uint8_t *buffer,size_t size)。由于这不是纯虚拟的,我没有注意到。

Print::printf() 正在使用这个 write 变体来输出数据。 write 的默认实现是使用 write 的单字符变体(需要在派生类中实现的纯虚函数)。因此,对 device->printf() 的调用将使用 Print::printf(),依次使用 Print::write() 和最后 AltLog::write()AltLog::printf() 根本没有发挥作用。

实现AltLog::write(buffer,size)函数解决了我的困境,现在可以将完整的缓冲区作为一个发送。

#include <Arduino.h>

class AltLog : public Print {
public:
  size_t write(uint8_t c) {
    Serial.printf("/1: %c",c);
    Serial.printf("/2: %c",c);
    return 5;
  }

  size_t write(const uint8_t *buffer,size_t size) {
    Serial.print("/1: ");
    Serial.write(buffer,size);
    Serial.print("/2: ");
    Serial.write(buffer,size);
    return 4 + size;
  }
};

AltLog a;

Print *device = &Serial;

void setup() {
  Serial.begin(115200);
  Serial.println("");
  Serial.println("_OK_");
  delay(5000);

  device->printf("Serial.%c\n",'S');

  device = &a;
  device->printf("Alternate.%c\n",'A');

}

void loop() {
}