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.