($100 paid request) Serial/SafeString/Digital Read/Write bug is ruining my life

I'll try and spare you the detail of the embarrassing weeks it took me to even figure out that this bug was on the Arduino and not within ROS/python/host computer but now that I have somewhat nailed it down in the code I still do not know what exactly is causing the issue and I just can't come up with much else to try. For this reason I am willing to pay $100 to whoever can provide me with a robust solution first the arduino code is attached, it is ~200 lines but the first half is mostly just boilerplate definitions for the serial messages.

I have also attached the python code which produces the bug, you will need ROS2 to run as is but would be pretty easy to edit it to python only, just need to replace the timers. The bug typically occurs when the 'pressure_setting' (12,14) threshold is passed and a number of actuators are turned from on to off.

**important: using SafeString and BufferedOutput for serial communications **

**BUG DESCRIPTION ** My sketch uses a simple serial message format of <key>:<data> for the host computer to request sensor data or set an actuator and initially everything works as desired but as the host computer begins make many requests in close sequence (almost always turning several actuators on or off basically at the same time) the serial connection becomes unresponsive / the Arduino crashes I have tried many things on the host computer being extremely careful that the serial is always called in a synchronous manner and trying to add in delays between the serial calls to prevent the issue but it only creates a bottleneck and delays the issue at best. I have also tried making some changes and delays in the way the Arduino processes a request to turn something on or off and even had it respond with the normal serial rather than bufferedOutput but this did nothing.

Now, what does work is if I have the Arduino just respond with the same serial message it received (host computer thinks the on or off request is successful) instead of actually hitting the code below and making the switch. Serially speaking this is identical to what is happening when the bug occurs so I feel it is fairly safe to say the bug lives somewhere here within the interaction between the serial and the switching and not in the serial communication alone.

// set relay true = on false = off 
void setRelay(int pin, bool setting, String reply_key){
  digitalWrite(pin, !setting);
  transmitSerial(reply_key, String(!digitalRead(pin)));
  // transmitSerial(reply_key, String(!setting));
  // Serial.println(reply_key + ':' + String(!digitalRead(pin)));
}

this can be observed by uncommenting line 148 and commenting 149-163 to make serial messages be returned instead of actually processed.

I could guess all day at what it may be here but I give up. Won't let me attach the files because I'm a new user so here it is...

arduino

#include "SafeStringReader.h"
#include "BufferedOutput.h"

// serial messages / keys and pin definitions
// air valves
#define CANOPY_IN_AV "cainv"
#define CANOPY_IN_PIN 44 // NC
#define CANOPY_EX_AV "caexv"
#define CANOPY_EX_PIN 47 // OC
#define ROOT_IN_AV "rtinv"
#define ROOT_IN_PIN 43 // NR
#define ROOT_EX_AV "rtexv"
#define ROOT_EX_PIN 46 // OR
#define AMBIENT_IN_AV "aminv"
#define AMBIENT_IN_PIN 45 // NA
#define AMBIENT_EX_AV "amexv"
#define AMBIENT_EX_PIN 48 // OA
// water solenoids
#define PASS_RADIATOR_SOL "pards" // R1
#define PASS_RAD_PIN 39
#define ROOT_RADIATOR_SOL "rtrds" // R2
#define ROOT_RAD_PIN 40
#define CANOPY_SPRAY_SOL "casps" // IC
#define CANOPY_SPRAY_PIN 41

#define CORE_IN_SOL "coins" // IR
#define CORE_IN_PIN 38
#define RETURN_IN_SOL "reins" // RE
#define RETURN_IN_PIN 37
#define ROOT_DRAIN_IN_SOL "rtdrins" // RZ
#define ROOT_DRAIN_IN_PIN 36
#define MIST_1_SOL "mi1s" // M1
#define MIST_1_PIN 35
#define MIST_2_SOL "mi2s" // M2
#define MIST_2_PIN 34
#define DRAIN_SOL "drains" // DR
#define DRAIN_PIN 33

// water pump
#define PUMP "pump"
#define PUMP_PIN 42

// lock
#define LOCK "lock"
#define LOCK_PIN 27
#define EMPTY 26
bool locked = false;

// fan
#define FAN "fan"
#define IN1 5
#define IN2 4
#define PWM 6

// SENSORS
#define HUMIDITY "humid"
#define IN_TEMP "intemp"
#define ROOT_TEMP "rttemp"
#define EX_TEMP "extemp"
#define EX_TEMP_PIN A6
#define PRESSURE "press"
#define PRESSURE_PIN A14
const int pressure_offset = 0.469;
#define RETURN_SOLUTION "resol"
#define RETURN_SOLUTION_PIN 29
#define DOOR_CLOSED "door"
#define LEFT_DOOR_CLOSE_PIN 16
#define RIGHT_DOOR_CLOSE_PIN 15
#define TOUCH "touch"


// create an safeReader instance of SafeStringReader class
// delimited by comma, CarrageReturn or NewLine
// the createSafeStringReader( ) macro creates both the SafeStringReader (safeReader) and the necessary SafeString that holds input chars until a delimiter is found
// args are (ReaderInstanceName, expectedMaxCmdLength, delimiters)
createSafeStringReader(safeReader, 63, ",\r\n");


// Buffered output instance
createBufferedOutput(output, 63, BLOCK_IF_FULL);

bool running = true;

float pressure = 300;

void setup() {
  Serial.begin(115200);

  pinMode(CANOPY_IN_PIN, OUTPUT);
  pinMode(CANOPY_EX_PIN, OUTPUT);
  pinMode(ROOT_IN_PIN, OUTPUT);
  pinMode(ROOT_EX_PIN, OUTPUT);
  pinMode(AMBIENT_IN_PIN, OUTPUT);
  pinMode(AMBIENT_EX_PIN, OUTPUT);
  pinMode(PASS_RAD_PIN, OUTPUT);
  pinMode(ROOT_RAD_PIN, OUTPUT);
  pinMode(CANOPY_SPRAY_PIN, OUTPUT);
  pinMode(CORE_IN_PIN, OUTPUT);
  pinMode(RETURN_IN_PIN, OUTPUT);
  pinMode(ROOT_DRAIN_IN_PIN, OUTPUT);
  pinMode(MIST_1_PIN, OUTPUT);
  pinMode(MIST_2_PIN, OUTPUT);
  pinMode(DRAIN_PIN, OUTPUT);
  pinMode(PUMP_PIN, OUTPUT);
  pinMode(LOCK_PIN, OUTPUT);
  pinMode(EMPTY, OUTPUT);
  allRelaysOff();

  
  // enable error messages and SafeString.debug() output to be sent to Serial
  // SafeString::setOutput(Serial);

  safeReader.connect(Serial); // where SafeStringReader will read from
  // safeReader.echoOn(); // echo back all input, by default echo is off

  output.connect(Serial); // Connect output buffer to serial

}

void loop() {
  output.nextByteOut(); // Send next byte from serial buffer

  processSerial();
}


void processSerial(){
  if(safeReader.read()){
    SafeString::Output.println(safeReader);
    size_t colonIdx = 0;
    colonIdx = safeReader.indexOf(':');
    if (colonIdx < safeReader.length()){ // Command needs data parsing
      // safeString parsing variables
      createSafeString(command, 9); 
      createSafeString(data, 11);

      long dataLong;
      float dataFloat;

      // Parse
      safeReader.substring(command, 0, colonIdx); // extract command before colon
      safeReader.substring(data, colonIdx+1); // extract data after colon

      if (data.toLong(dataLong)){ // type long parse (fails with decimal)
        SafeString::Output.print(F("Long data detected: "));
        SafeString::Output.println(dataLong);
        // output.println(safeReader);
        if (command == CORE_IN_SOL){ // core intake solution solenod
          setRelay(CORE_IN_PIN, bool(dataLong), CORE_IN_SOL);
        } else if (command == RETURN_IN_SOL){ // return intake solenoid
          setRelay(RETURN_IN_PIN, bool(dataLong), RETURN_IN_SOL);
        } else if (command == ROOT_DRAIN_IN_SOL){ // root drain intake solenoid
          setRelay(ROOT_DRAIN_IN_PIN, bool(dataLong), ROOT_DRAIN_IN_SOL);
        } else if (command == MIST_1_SOL){ // mist 1 solenoid
          setRelay(MIST_1_PIN, bool(dataLong), MIST_1_SOL);
        } else if (command == MIST_2_SOL){ // mist 2 solenoid
          setRelay(MIST_2_PIN, bool(dataLong), MIST_2_SOL);
        } else if (command == DRAIN_SOL){ // drain solenoid
          setRelay(DRAIN_PIN, bool(dataLong), DRAIN_SOL);
        } else if (command == PUMP){ // water pump
          setRelay(PUMP_PIN, bool(dataLong), PUMP);
        }
      }

    } else { // else no data command
        if(safeReader == "press"){
          transmitSerial("press", String(pressure));
          pressure += 10;
        }
      }
  }
}

// Send outgoing message with colon
void transmitSerial(String key, String data) {
  output.print(key + ':');
  output.println(data);
}


// set relay true = on false = off 
void setRelay(int pin, bool setting, String reply_key){
  digitalWrite(pin, !setting);
  transmitSerial(reply_key, String(!digitalRead(pin)));
  // transmitSerial(reply_key, String(!setting));
  // Serial.println(reply_key + ':' + String(!digitalRead(pin)));
}

// all relays off
void allRelaysOff(){
  digitalWrite(CANOPY_IN_PIN, HIGH);
  digitalWrite(CANOPY_EX_PIN, HIGH);
  digitalWrite(ROOT_IN_PIN, HIGH);
  digitalWrite(ROOT_EX_PIN, HIGH);
  digitalWrite(AMBIENT_IN_PIN, HIGH);
  digitalWrite(AMBIENT_EX_PIN, HIGH);
  digitalWrite(PASS_RAD_PIN, HIGH);
  digitalWrite(ROOT_RAD_PIN, HIGH);
  digitalWrite(CANOPY_SPRAY_PIN, HIGH);
  digitalWrite(CORE_IN_PIN, HIGH);
  digitalWrite(RETURN_IN_PIN, HIGH);
  digitalWrite(ROOT_DRAIN_IN_PIN, HIGH);
  digitalWrite(MIST_1_PIN, HIGH);
  digitalWrite(MIST_2_PIN, HIGH);
  digitalWrite(DRAIN_PIN, HIGH);
  digitalWrite(PUMP_PIN, HIGH);
  digitalWrite(LOCK_PIN, HIGH);
  digitalWrite(EMPTY, HIGH);
}

python serial messenger class

#!/usr/bin/env python3

import serial

import serial.tools.list_ports

from serial import SerialException

import threading

import time

class SerialMessenger():

    def __init__(self, serial_number, timeout):

        self.serial_number = serial_number

        self.serial_timeout = timeout

        self.get_serial_port(self.serial_number)

        self.connect_to_serial(self.serial_port)

        # lock for serial resource

        self.lock = threading.Lock()

        

    # return matching serial port for given serial number

    def get_serial_port(self, serial_number):

        match_port = None

        all_ports = serial.tools.list_ports.comports()

        print('finding SN: {} in {} ports'.format(serial_number, len(all_ports)))

        for port in all_ports: 

            if port.serial_number == serial_number:

                print('MATCH! port : {} SN: {}'.format(port.device, port.serial_number))

                match_port = port.device

        if match_port is None:

            print('no matching port for SN: {}'.format(serial_number))

        

        self.serial_port = match_port

        return match_port

    # make serial connection

    def connect_to_serial(self, port):

        if(port is not None):

            print("connecting at port {}".format(self.serial_port))

            try:

                # open and clear serial port

                self.serial = serial.Serial(port,  115200, timeout = self.serial_timeout)

                self.serial.reset_input_buffer()

                self.serial.reset_output_buffer()

                print("serial connected!")                

            except SerialException as err:

                print(str(err))

    # only send data to arduino

    def send_to_serial(self, message):

        try:

            if(self.serial.writable()):

                #print('sending: {}'.format(message))

                message += "\n"

                self.serial.write( message.encode())

            else:

                print('not writable')

        except SerialException as err:

            print(str(err))

    # recieve and return a line from the arduino

    def recieve_from_serial(self):

        if(self.serial.readable()):

            try: # attempt to read a line with timeout

                message = self.serial.readline().decode().strip()

                if message:

                    # if line exists, parse and return

                    parsedMessage = message.split(':') # split message into list using colon delimiter

                    return parsedMessage

                else:

                    print("read line operation timed out...")

                    return None

            except SerialException as err:

                print(str(err))

        else:

            print('not readable')

    # send and recieve a message from the arduino

    def send_and_receive(self, message ):

        aquired = self.lock.acquire(timeout=3)

        try:

            self.send_to_serial(message)

            response = self.recieve_from_serial()

            if response is not None:

                print("recieved: " + str(response))

            else:

                print('message: {} failed, reconnecting...'.format(message))

                # reconnect

                self.serial.close()

                time.sleep(2)

                # get serial port and connect

                self.get_serial_port(self.serial_number)

                self.connect_to_serial(self.serial_port)

                time.sleep(4)

            return response

        finally:

            self.lock.release()

python bug producer with ROS

#!/usr/bin/env python3
import time
from rclpy import node
from .serial_messenger_class import SerialMessenger
import rclpy
from rclpy.node import Node

class AeroponicDriverNode(Node):
    def __init__(self):
        super().__init__('aeroponic_driver')
        # declare parameters
        self.declare_parameter('pressure', 600.0)

        self.pressure_setting = self.get_parameter('pressure').value

        self.chamber_serial = SerialMessenger("55735323435351803022", 1)

        time.sleep(.5)
        self.pressure_sense_timer = self.create_timer(.5, self.get_pressure_callback)
        # self.root_return_timer = self.create_timer(1, self.get_root_return_callback)

    def get_pressure_callback(self):
        self.pressure = float(self.chamber_serial.send_and_receive('press')[1])
        self.get_logger().info('pressure: {} kpa'.format(self.pressure))
        self.pressurize()

    def get_root_return_callback(self):
        self.root_return = bool(int(self.chamber_serial.send_and_receive('resol')[1]))
        self.get_logger().info('root return: {} wet'.format(self.root_return))

    def set_core_intake(self, setting):
        setting = int(setting)
        self.core_intake = bool(int(self.chamber_serial.send_and_receive('coins:{}'.format(setting))[1]))
        self.get_logger().info('core intake: {}'.format(self.core_intake))

    def set_pump(self, setting):
        setting = int(setting)
        self.core_intake = bool(int(self.chamber_serial.send_and_receive('pump:{}'.format(setting))[1]))
        self.get_logger().info('pump on: {}'.format(self.core_intake))

    def set_mist_1(self, setting):
        setting = int(setting)
        self.core_intake = bool(int(self.chamber_serial.send_and_receive('mi1s:{}'.format(setting))[1]))
        self.get_logger().info('mist 1: {}'.format(self.core_intake))    

    def pressurize(self):
        if self.pressure < self.pressure_setting:

            self.set_core_intake(True)

            self.set_pump(True)

            self.set_mist_1(False)

        else:

            self.set_pump(False)

            self.set_core_intake(False)

            self.set_mist_1(True)



def main(args=None):
    rclpy.init(args=args)
    node = AeroponicDriverNode()
    rclpy.spin(node)

as for payment you can either take me for my word or contact me and will pay 50% on proof of solution and the remainder on delivery of solution.

Your post was MOVED to its current location as it is more suitable.

1 Like

Honestly it's not worth anyone's time to debug three peices of code for $100, let alone setting up the ROS2 environment and testing your nodes to find the bug. You might want to consider increasing your budget for someone to help you with this.

That's normal if you use the String type on an Arduino with small RAM due to memory fragmentation.

I never use 'safestring' maybe you should contact the author @drmpf if i am not mistaken.

Would this happen just from casting to string?

Edit: I am using a 2650 and casting 1s and 0s

Yeah that's next on my list

Ok I'll remove the ROS and consider raising the price thanks

Most probably yes. You can find out yourself.

I will work on that thanks

Could it be a power problem with switching multiple relays more or less simultaneously?

I did not look in depth at your code but changing

void transmitSerial(String key, String data)

to

void transmitSerial(String &key, String &data)

will circumvent one of the pitfalls of the String class. In your code, it will place a copy of key and of data on the stack, the modified version will place references to key and data on the stack (taking less space).

If you use the SafeString library, I suggest that you use it consistently and stay away from the String class.

I think your insight is probably correct and the fact I have any usage of Strings left is probably the cause. Unfortunately the & pass did not work alone as now it has warnings on my calls to it

initial value of reference to non-const must be an lvalue

anyhow, I am going to improve my safestring usage and hopefully that helps

You have a few more where you pass the object and not a reference.

As you're using actuators, I'm curious how you are controlling them? Relays? MOSFets? Have you considered that this might be power or spike related?

Drop me a line, with your code, if you have problems using my SafeString library.

You are buffering the output, but perhaps the problem is flooding the input.
Take a look at BufferedInput section of Arduino Serial I/O for the Real World

Edit --
You could also add a loopTimer to check how much time the processing loop takes.
I note you have BLOCK_IF_FULL for the buffered output so the loop will slow down if the output buffer fills up,
A quick compile gives
Global variables use 894 bytes (10%) of dynamic memory, leaving 7298 bytes for local variables. Maximum is 8192 bytes.
So you have plenty of space to increase the size of the output buffer and see if that changes the loop time
EEdit-
You could also capture your python output in a file and isolate a series of commands that cause the problem and then use SafeStringStream to inject them as test data into your Arduino code.
If you drop me that file of commands, I can try it here.

For debugging, start up Serial1 and connect it to the IDE Serial Monitor via TTL-USB cable or another Arduino board.
Wrap Serial1 in a BufferedOutput, called say debugOut, with DROP_IF_FULL setting to prevent it blocking.
and set SafeString::setOutput(debugOut)
also use loopTimer.check(debugOut)

thanks a lot for your direction. I'm highly doubtful a buffer is overflowing as at this point I am very sure that another serial message does not even get sent until the one before it has a response. I think my failure was in not making my safestring usage consistent enough, I still technically have string usage in the form of parameters and casts and I am hoping if I remove those with the help of this the fragmentation will not occur.

This version removes all the String usage. But suggest you still need to add more debugging and a looptimer.

// https://forum.arduino.cc/t/100-paid-request-serial-safestring-digital-read-write-bug-is-ruining-my-life/903396
#include "SafeStringReader.h"
#include "BufferedOutput.h"
// serial messages / keys and pin definitions
// air valves
#define CANOPY_IN_AV "cainv"
#define CANOPY_IN_PIN 44 // NC
#define CANOPY_EX_AV "caexv"
#define CANOPY_EX_PIN 47 // OC
#define ROOT_IN_AV "rtinv"
#define ROOT_IN_PIN 43 // NR
#define ROOT_EX_AV "rtexv"
#define ROOT_EX_PIN 46 // OR
#define AMBIENT_IN_AV "aminv"
#define AMBIENT_IN_PIN 45 // NA
#define AMBIENT_EX_AV "amexv"
#define AMBIENT_EX_PIN 48 // OA
// water solenoids
#define PASS_RADIATOR_SOL "pards" // R1
#define PASS_RAD_PIN 39
#define ROOT_RADIATOR_SOL "rtrds" // R2
#define ROOT_RAD_PIN 40
#define CANOPY_SPRAY_SOL "casps" // IC
#define CANOPY_SPRAY_PIN 41
#define CORE_IN_SOL "coins" // IR
#define CORE_IN_PIN 38
#define RETURN_IN_SOL "reins" // RE
#define RETURN_IN_PIN 37
#define ROOT_DRAIN_IN_SOL "rtdrins" // RZ
#define ROOT_DRAIN_IN_PIN 36
#define MIST_1_SOL "mi1s" // M1
#define MIST_1_PIN 35
#define MIST_2_SOL "mi2s" // M2
#define MIST_2_PIN 34
#define DRAIN_SOL "drains" // DR
#define DRAIN_PIN 33
// water pump
#define PUMP "pump"
#define PUMP_PIN 42
// lock
#define LOCK "lock"
#define LOCK_PIN 27
#define EMPTY 26
bool locked = false;
// fan
#define FAN "fan"
#define IN1 5
#define IN2 4
#define PWM 6
// SENSORS
#define HUMIDITY "humid"
#define IN_TEMP "intemp"
#define ROOT_TEMP "rttemp"
#define EX_TEMP "extemp"
#define EX_TEMP_PIN A6
#define PRESSURE "press"
#define PRESSURE_PIN A14
const int pressure_offset = 0.469;
#define RETURN_SOLUTION "resol"
#define RETURN_SOLUTION_PIN 29
#define DOOR_CLOSED "door"
#define LEFT_DOOR_CLOSE_PIN 16
#define RIGHT_DOOR_CLOSE_PIN 15
#define TOUCH "touch"
// create an safeReader instance of SafeStringReader class
// delimited by comma, CarrageReturn or NewLine
// the createSafeStringReader( ) macro creates both the SafeStringReader (safeReader) and the necessary SafeString that holds input chars until a delimiter is found
// args are (ReaderInstanceName, expectedMaxCmdLength, delimiters)
createSafeStringReader(safeReader, 63, ",\r\n");
// Buffered output instance
createBufferedOutput(output, 63, BLOCK_IF_FULL);
bool running = true;
float pressure = 300;
void setup() {
  Serial.begin(115200);
  pinMode(CANOPY_IN_PIN, OUTPUT);
  pinMode(CANOPY_EX_PIN, OUTPUT);
  pinMode(ROOT_IN_PIN, OUTPUT);
  pinMode(ROOT_EX_PIN, OUTPUT);
  pinMode(AMBIENT_IN_PIN, OUTPUT);
  pinMode(AMBIENT_EX_PIN, OUTPUT);
  pinMode(PASS_RAD_PIN, OUTPUT);
  pinMode(ROOT_RAD_PIN, OUTPUT);
  pinMode(CANOPY_SPRAY_PIN, OUTPUT);
  pinMode(CORE_IN_PIN, OUTPUT);
  pinMode(RETURN_IN_PIN, OUTPUT);
  pinMode(ROOT_DRAIN_IN_PIN, OUTPUT);
  pinMode(MIST_1_PIN, OUTPUT);
  pinMode(MIST_2_PIN, OUTPUT);
  pinMode(DRAIN_PIN, OUTPUT);
  pinMode(PUMP_PIN, OUTPUT);
  pinMode(LOCK_PIN, OUTPUT);
  pinMode(EMPTY, OUTPUT);
  allRelaysOff();
  // enable error messages and SafeString.debug() output to be sent to Serial
  // SafeString::setOutput(Serial);
  safeReader.connect(Serial); // where SafeStringReader will read from
  // safeReader.echoOn(); // echo back all input, by default echo is off
  output.connect(Serial); // Connect output buffer to serial
}

void loop() {
  output.nextByteOut(); // Send next byte from serial buffer
  processSerial();
}

void processSerial(){
  if(safeReader.read()){
    SafeString::Output.println(safeReader);
    size_t colonIdx = 0;
    colonIdx = safeReader.indexOf(':');
    if (colonIdx < safeReader.length()){ // Command needs data parsing
      // safeString parsing variables
      createSafeString(command, 9); 
      createSafeString(data, 11);
      long dataLong;
      float dataFloat;
      // Parse
      safeReader.substring(command, 0, colonIdx); // extract command before colon
      safeReader.substring(data, colonIdx+1); // extract data after colon
      if (data.toLong(dataLong)){ // type long parse (fails with decimal)
        SafeString::Output.print(F("Long data detected: "));
        SafeString::Output.println(dataLong);
        // output.println(safeReader);
        if (command == CORE_IN_SOL){ // core intake solution solenod
          setRelay(CORE_IN_PIN, dataLong, CORE_IN_SOL);
        } else if (command == RETURN_IN_SOL){ // return intake solenoid
          setRelay(RETURN_IN_PIN, dataLong, RETURN_IN_SOL);
        } else if (command == ROOT_DRAIN_IN_SOL){ // root drain intake solenoid
          setRelay(ROOT_DRAIN_IN_PIN, dataLong, ROOT_DRAIN_IN_SOL);
        } else if (command == MIST_1_SOL){ // mist 1 solenoid
          setRelay(MIST_1_PIN, dataLong, MIST_1_SOL);
        } else if (command == MIST_2_SOL){ // mist 2 solenoid
          setRelay(MIST_2_PIN, dataLong, MIST_2_SOL);
        } else if (command == DRAIN_SOL){ // drain solenoid
          setRelay(DRAIN_PIN, dataLong, DRAIN_SOL);
        } else if (command == PUMP){ // water pump
          setRelay(PUMP_PIN, dataLong, PUMP);
        }
      }

    } else { // else no data command
        if(safeReader == "press"){
          cSF(sfData, 20);
          sfData = pressure;
          transmitSerial("press", sfData.c_str());
          pressure += 10;
        }
      }
  }
}

// Send outgoing message with colon
// could pass SafeString & here but seems an overkill
void transmitSerial(const char*key, const char*data) {
  output.print(key); output.print(':');
  output.println(data);
}

// set relay true = on false = off 
void setRelay(int pin, bool setting, const char*reply_key){
  digitalWrite(pin, !setting);
  cSF(sfData, 10);
  sfData = !digitalRead(pin);
  transmitSerial(reply_key, sfData.c_str()); //String(!digitalRead(pin)));
  // transmitSerial(reply_key, String(!setting));
  // Serial.println(reply_key + ':' + String(!digitalRead(pin)));
}

// all relays off
void allRelaysOff(){
  digitalWrite(CANOPY_IN_PIN, HIGH);
  digitalWrite(CANOPY_EX_PIN, HIGH);
  digitalWrite(ROOT_IN_PIN, HIGH);
  digitalWrite(ROOT_EX_PIN, HIGH);
  digitalWrite(AMBIENT_IN_PIN, HIGH);
  digitalWrite(AMBIENT_EX_PIN, HIGH);
  digitalWrite(PASS_RAD_PIN, HIGH);
  digitalWrite(ROOT_RAD_PIN, HIGH);
  digitalWrite(CANOPY_SPRAY_PIN, HIGH);
  digitalWrite(CORE_IN_PIN, HIGH);
  digitalWrite(RETURN_IN_PIN, HIGH);
  digitalWrite(ROOT_DRAIN_IN_PIN, HIGH);
  digitalWrite(MIST_1_PIN, HIGH);
  digitalWrite(MIST_2_PIN, HIGH);
  digitalWrite(DRAIN_PIN, HIGH);
  digitalWrite(PUMP_PIN, HIGH);
  digitalWrite(LOCK_PIN, HIGH);
  digitalWrite(EMPTY, HIGH);
}

thanks! I was just setting up with safestrings as parameters but this does seem a little easier. I will give it a shot and report back in the next couple hours.

unfortunately issue persists. I am going to try and get an exact safeStringStream which causes the failure and try connecting the debug serial to a secondary connection as you mentioned

is it possible any strings would be making it back in by way of my definitions or during comparison?