How to create a SerialLog function that can be switched off

you have an ESP. You could use a pin or a WIFI interface (for example a HTML page with a button) to activate/deactivate a debug output.
Furthermore, the ESP8266 has at least a TX1 available, so even if Serial is occupied, you could use Serial1 as Debug output.

I don't have an example but I would say yes. This way should work with any class which inherits from Print.

Depending on your use case, enabling debug could also be done with a spare digital. Even the digital output used for the flashing LED... It's been done.

I had to do so for the first code that I wrote for a PIC 16C54. Developed the software using a 16C57 to have some spare pins for LEDs.

That was more a personal, "been there, done that, got the t-shirt" sort of comment. I don't recommend it, but it did do the job at hand. Taught me a lesson in not choosing solutions that are at the hairy edge of "not enough resources".

Interesting!
I had no idea it could be done in this manner.
But compared to my attempt to develop a set of functions, it looks like a more logical approach.
Does the identifier VA ARGS match anything present in the parameters ()?
And the (...) should actually be written that way?

Now I'll give it a shot.
Regards picsapkstudio

Hello,

I needed a simple technique to turn my serial output off and on at runtime. I did not want to compile a separate program for each. I can send my app a message to toggle a boolean variable, hushMode. Setting hushMode to true suppresses serial output.

Below is the macro and an example of how I use it. The Serial statements you want suppressable need to be put in the macro. Note that if you have Serial statements that you always want to print, just don't use the HUSH macro on them. Note the use of spaces, i.e., HUSH( X ) works and HUSH(X) maybe not. Also the HUSH( X ) statement does not have a semi-colon at the end.

Note that this is my first attempt at posting code so I hope I'm doing it correctly.

#define HUSH( X ) if (!hushMode) { X }  // only executes X if NOT in hush mode


// Original statement:  Serial.printf("%s: %d\n", "Label", value);  // value is an integer variable

bool hushMode;  // global variable
HUSH( Serial.printf("%s: %d\n", "Label", value); )  // only prints if hushMode is false

I quote the original post of this solution because I would like to ask a follow-up question.

---- Background ----
I have implemented the above solution in my sketch and it works just fine, so I am getting the same logging as before but now with a single define I can switch it all off by recompiling my firmware with a different define.

My question is this:
My project reads data off of an electricity meter and sends it by MQTT to a broker on which I can retrieve it by subscribing.
Now I would like to know if I could expand the define solution such that instead of sending the log to the Serial device I could send it via MQTT instead?
The MQTT client is defined like this:

WiFiClient espClient;
....
// * Initiate MQTT client
PubSubClient mqtt_client(espClient);

And the mqtt_client is used like this example to send data to the broker:

char buf[32];
//Put some data into buf[]
int rssi = WiFi.RSSI(); //Get signal strength in dB
itoa(rssi, buf, 10);
//Send to MQTT broker
mqtt_client.publish("source/data/wifistrength", buf);

The PubSubClient is defined in the library like this:

class PubSubClient : public Print {
private:
   Client* _client;
.. then a lot of other stuff ...

My main point here is that it seems like PubSubClient is inheriting from Print in the same way as Serial is, so I am asking myself if it could also be used as the target object for the debug logging scheme when there is no way to hook up a serial line?

In the best of worlds I would also like to be able to let the decision of which way to use be handled by a setting somewhere, possibly reading a jumper input such that if the input is high it operates in one way and if it is low in another way.
Right now it is a compile time decision so I cannot change it by any way except recompiling the firmware.
If this could be done I like to select between:

  • No logging
  • Serial logging
  • MQTT logging

The jumper check could be done at boot time following a reset so I am not asking for a way to change this in real time willy-nilly.

Can this be done?
(I am not a C++ software guy, though, so I am not able to make this myself.)

That’s your cue indeed

As it inherits from Print basically all works, just substitute Serial with your PubSubClient instance

But if you want to do it at run time (even in the setup), then you can’t get rid of the code the way I did (because it’s literally removed from the compiled code if you set DEBUG to 0) You would need one of the other method that was mentioned where there is an dynamic decision made based on whatever settings you want to have

IMHO using mqtt is not as "easy" than Serial.print, client.prints, LCD.prints ...

Look at your code snippet:

that's a two step approach. First you fill a buffer with data, than you "publish" that buffer.

So you would need something similar for your debug messages.

  1. you should "print" into a buffer
  2. if your buffer is ready to be transmitted - transmit your buffer.

for 1) I see following options:
a) "print" (each character) into a buffer
b) use the library " PString.h by Mikal Hart", which enables printing in a buffer (and than do a separate manual "publish") ...
c) use my NoiascaBufferPrint.h to print into a buffer and optionally "flush" the buffer to mqtt.

currently I tend that option b) is the most straight forward solution.

I'm not using mqtt. But if you make short MVP - a full compileable simple example with some kind of "that kind information should be debug-printed to mqtt" - there's may be a person who tries to combine some proposals from this thread with your MVP.

@BosseB
over 2h later:

I played around and came up with that

/* Optional Debug.print
   https://forum.arduino.cc/t/how-to-create-a-seriallog-function-that-can-be-switched-off/1057755/10

   disable/enable debug messages during runtime
   use several debug channel in parallel
   store incoming data in a buffer for channels which need one single transmission

   by noiasca
*/


class DebugPrint : public Print {
  protected:
    uint8_t debugLevel = 1 << SERIAL0;            // store the activated debug outputs in a bitmask - you can debug print to several interfaces
    uint32_t previousMillis = 0;             // the last received "write" command
    const uint16_t autoPublish = 500;        // autoflush ("publish") after this period. Set to 0 if you don't need a autoPublish and call flush on your own
    byte dirty = 0;                      // how many bytes are in the buffer
    char buffer[255] {""};                   // internal buffer

  public:
    enum destination {OFF,
                      SERIAL0,
                      LCD,
                      MQTT,
                      SERIAL1,              
                      SERIAL2,               // I'm using an ESP32 with 3 Serials, currently I have Serial2 connected with an USB-TTL interface
                     };
    void off()
    {
      debugLevel = OFF;     // off is realy a switch off
      //Serial.print(F("debugLevel=")); Serial.println(debugLevel, BIN);
    }

    void on()               // activate only on Serial(0)
    {
      debugLevel = 1 << SERIAL0;
      //Serial.print(F("debugLevel=")); Serial.println(debugLevel, BIN);
    }

    void addDebugLevel(destination newDestination)
    {
      debugLevel |= (1 << newDestination);                 // "add" the received destination to the bitmask
      //Serial.print(F("debugLevel=")); Serial.println(debugLevel, BIN);
    }

    void setDebugLevel(byte newLevel)
    {
      debugLevel = newLevel;
      //Serial.print(F("debugLevel=")); Serial.println(debugLevel, BIN);
    }

    size_t write (uint8_t value)
    {
      if (debugLevel & (1 << SERIAL0))
      {
        Serial.write(value);        // passthrough the value to the output channel
      }
      if (debugLevel & (1 << LCD))
      {
        //lcd.write(value);         // not implemented but you get the idea
      }
      if (debugLevel & (1 << MQTT))
      {
        // print to buffer
        previousMillis = millis();
        char in[2] {'\0', '\0'};  // prepare null terminated c-string (for later strcat)
        in[0] = value;            // copy the received value to first byte
        strcat(buffer, in);       // concat to the buffer
        dirty++;
        // avoid overflow of buffer
        if (dirty >= sizeof(buffer) - 1) flush(); // force flush because buffer is full (or handle the buffer overflow differently ...
      }
      if (debugLevel & (1 << SERIAL1))
      {
        Serial1.write(value);     // only activate if you really have a Serial1
      }
      //if (debugLevel & (1 << SERIAL2))
      //{
      //  Serial2.write(value);     // only activate if you really have a Serial2
      //}
      return 1; // you must return the number of "printed" bytes. this write processes 1 byte. therefore return 1 (even if it is not printed)
    }

    void flush()  // output buffer to mqtt
    {
      if (debugLevel & (1 << MQTT))
      {
        // put here the "publish" function of MQTT
        // mqtt_client.publish("source/data/wifistrength", buf);
        //Serial1.print(F("mqtt flush>")); Serial1.println(buffer);  // this line is just for my testing
        strcpy(buffer, "");        // delete internal buffer
        dirty = 0;                 // reset
      }
      else
      {
        //Serial.println(F("I: no publish necessary"));
      }
    }

    // check if there is something to flush
    void update()
    {
      if (dirty && autoPublish && millis() - previousMillis > autoPublish)
      {
        flush();
      }
    }
} debug;


void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  Serial1.begin(115200);
  //Serial2.begin(115200);

  Serial.println(F("Debug Print Test"));

  debug.on();
  debug.println("Debug 1 is on");

  debug.off();
  debug.println("Debug 2 is off");

  debug.addDebugLevel(DebugPrint::SERIAL0);
  debug.println("Debug 3 is on again");

  debug.off();
  debug.println(F("Debug 4 is off"));

  debug.on();
  debug.println(F("Debug 5 is on again"));

  float afloat = 123.45;
  debug.println(afloat);

  char achararray [] = {"bla bla\0"};
  debug.println(achararray);

  String anArduinoStringObject = "bla blub";
  debug.println(anArduinoStringObject);

}

// this is just to change the debug state during runtime.
void serialRead()
{
  byte in = Serial.read();
  switch (in)
  {
    case 'o' : debug.off(); break;
    case 's' : debug.addDebugLevel(DebugPrint::SERIAL0); break;
    case 'l' : debug.addDebugLevel(DebugPrint::LCD); break;
    case 'm' : debug.addDebugLevel(DebugPrint::MQTT); break;
    case '1' : debug.addDebugLevel(DebugPrint::SERIAL1); break;
    //case '2' : debug.addDebugLevel(DebugPrint::SERIAL2); break;
  }
}

// this is just a simulation to do some periodic action
void doSomething()
{
  static uint32_t previousMillis = 0;
  uint32_t currentMillis = millis();
  if (currentMillis - previousMillis > 1000)
  {
    previousMillis = currentMillis;
    int value = random(1000); // simulate some sensor reading
    debug.print(F("value=")); debug.println(value);
    //debug.flush();    //optional if you want to force a flush
  }
}

void loop() {
  doSomething();   // your main program, sensor readings, getting data ...
  serialRead();    // just for test purpose
  debug.update();  // this will call flush() if necessary
}

for example you can activate Output to 2 (3.., 4..) Serials if available:

You can switch on debug messages via Serial interface:
o for off
s for Serial
1 for Serial1
2 for Serial2 (I'm using a ESP32)
l is just an example for a LCD
m would be prepared for MQTT
This is done in the function serialRead(). Therefore I call this function in loop() over and over again. If you have other possibilities to switch on debug messages (buttons, incoming MQTT messages, a HTML page ...) you don't need that function.

The class can have several active channels (=outputs) at the same time. The active channels are stored in the variable debugLevel (in "binary"/as a bitmap).

"Print" interfaces like Serial or LCD would be straight forward, just pass through the write to the activated channel.

For MQTT we add each incoming value to an internal buffer.
The member function flush() sends the buffer to MQTT and empties the buffer.

so this means you would need to do a "flush()" at the end of your debug.prints in case of MQTT.

As I didn't want to add this flush() all over, I came up with a "timeout" function update(): is there something in the buffer and haven't we received new characters for some time, I do a autoflush. Idea is, you might need several prints to get your result (for example you want to print a warning and an actual value). This update() must be called over and over again in loop().

I have simulated this "MQTT call" via a second serial, and it looks like it would work:


The screenshot was taken after the last (direct) print to Serial was already visible, but the timeout for the MQTT was not reached so far. Therefore you see this slight "delay" between the two output channels.

For a production sketch there might be needed more improvements. But this should give you an idea how it could be done. I hope this will make sense for you. Yes I believe you will need to read this several times and play around with the sketch.

Thanks!
I will surely dive into this, but a bit late now. So tomorrow probably.
And the day after I am scheduled to go out and install the device on the real hardware meter. Then is when I no longer can use serial for debugging...

Back at it again, but this time with another question regarding the above definition.
I have tested the code above and it is working Ok.
But is it possible to add to the define something like this:

#define DEBUG 1    // SET TO 0 OUT TO REMOVE TRACES

#if DEBUG
#define D_SerialBegin(...) if (ENABLELOG == LOW) Serial.begin(__VA_ARGS__)
#define D_print(...)       if (ENABLELOG == LOW) Serial.print(__VA_ARGS__)
#define D_write(...)       if (ENABLELOG == LOW) Serial.write(__VA_ARGS__)
#define D_println(...)     if (ENABLELOG == LOW) Serial.println(__VA_ARGS__)
#else
#define D_SerialBegin(bauds)
#define D_print(...)
#define D_write(...)
#define D_println(...)
#endif

where ENABLELOG is a boolean set in setup() depending on the state of a jumper, which controls the state of the Serial port Rx polarity. If the jumper is ON it means that I am not connected to the final system but am debugging so then I want the serial data to be produced, otherwise not. So I would not have to recompile to get the two states.

But this will not be accepted by the compiler....
I do not know how one can create a define for code that should be later compiled and consist of more than one statement.
In this case the if (ENABLELOG) seems to break the functionality...

Is it at all possible?
I am completely illiterate concerning such constructs...

I have no idea. In the time it may take for someone who knows, why not try it?

S'not like you could damage something. With luck it may do the trick, or maybe be only a syntax error or two away that you can find yourself through with the errors and warnings you get when it doesn't compile.

I'd try it myself right now except I'm reading the forum through the tiniest of windows. Later.

I think you might need a pre-processor trick, there's a small handfu, of very useful idiomatic patterns for getting it seems almost anything to happen in a # define statement, so also google is your friend here for that side track. I can't seem to learn or even use much of the power I see, so this looks like a nice experiment for me.

a7

Did you check out the HUSH( X ) macro I posted above? It's an easy way to run (or not run) 1 or more statements depending on the value of a boolean variable "hushMode". And hushMode can be toggled at run time. I use an MQTT message and have used the value of a GPIO pin as well. Or you could use the serial interface. The preprocessor doesn't seem to care what X is. Can be one statement or many, including function calls with variable arguments. And you can probably adapt it (maybe with a switch statement?) to support multiple levels of debugging.

If I need to optimize code size and speed I would compile two separate executable for debug and non-debug mode. For now, HUSH( X ) works for me though.

The Boolean would need to be a global variable, not just defined in setup.

Missed that. #27. Food for thought. THX.

a7