This is a classic example of why you should not use the String
class.
I'm glad you asked for help, because there are several fairly simple techniques you can use to completely eliminate String
usage. You should use char arrays instead (aka C strings). Your sketch will be smaller, faster and more reliable.
1) The first thing is to just use the packetBuffer for all the parsing. There's no reason to copy it to other buffers (or a String). However, to make this char array into a C string, you must NUL-terminate it. That means you need to put a zero byte at the end. All the C string manipulation functions watch for that last zero byte. It tells them when they have reached the last character in the array. Simply declare the packet to be one byte larger, and set the last byte to a NUL character after you read it:
char packetBuffer[ UDP_TX_PACKET_MAX_SIZE+1 ]; // leave room for 0 byte (NUL terminator for C string)
void udpEvent (){
packetSize = Udp.parsePacket(); //Read the packetSize
if (packetSize>0){ //Check to see if a request is present
Udp.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE); //Reading the data request on the Udp
packetBuffer[packetSize] = '\0'; // NUL-terminate the packet
Serial.print( F("Received packet! '") );
Serial.print(packetBuffer);
Serial.println( '\'' );
You now have a C string (the packetBuffer) instead of a String object (copied from the packetBuffer).
2) Watching for specific commands is one of the main functions of udpEvent. You can use the strstr function to compare two C strings:
if (strstr( packetBuffer, "Relay1 off" ) != NULL) {
// matched!
You do this for many different strings, so let's make a function that makes it easier to read:
bool isCommand( const char *command )
{
return strstr( packetBuffer, command ) != NULL;
}
Now all your comparisons will read very nicely:
if ( isCommand("Relay1 on") && (Relay1_isAuto == 0)) {
Relay1_State = 1;
EEPROM.write(6,1);
turnRelay(1, 1);
insert_sql (Relays, 2);
}
else if ( isCommand("Relay1 off") && (Relay1_isAuto == 0)) {
Relay1_State = 0;
EEPROM.write(6,0);
turnRelay(1, 0);
insert_sql (Relays, 2);
}
// same for relays 2..8
else if ( isCommand("Relay1 isAuto 1") ) {
Relay1_isAuto = 1;
EEPROM.write(22,1);
insert_sql (Relays, 2);
}
else if ( isCommand("Relay1 isAuto 0") ) {
Relay1_isAuto = 0;
EEPROM.write(22,0);
insert_sql (Relays, 2);
}
// same for relays 2..8
else if ( isCommand("restoredefaults") ) {
Serial.println("RestoreDefaults");
RestoreDefaults();
}
... and if none of these comparisons match, you can print an error message:
} else {
Serial.println( F("Invalid command") );
}
}
memset(packetBuffer, 0, sizeof(packetBuffer) );
} // udpEvent
3) udpEvent
also parses a series of values from some commands. For example, the setdate
command has 6 numbers for the various date/time fields. You can use the strtok function to step through the comma-separated fields:
const size_t MAX_FIELDS = 7;
int field[ MAX_FIELDS ]; // parsed ints
char *token = strtok( packetBuffer, ", " ); // skip past command
for (int i = 0; i < MAX_FIELDS; i++) {
if (token) {
field[i] = atoi( token );
token = strtok( NULL, "," ); // step to the next comma separator
} else {
field[i] = 0;
}
}
You pass the string to parse in the first call to strtok
, along with the field delimiters, and it returns the first field. In your case, the first "field" is actually the command, like "setdate".
Next, the atoi function tries to parse an integer from that field. Iit will return a 0 for "setdate", but the next fields should give non-zero values for the various date & time fields. Those are stored in the elements of the integer array, [nobbc]field[i][/nobbc]
.
Notice that the second call to strtok is passed a NULL for the first argument. That tells it to continue stepping through the previous string (packetBuffer). When there are no more fields, strtok returns NULL. The for loop will simply set that field to 0 when token is NULL.
When the for loop exits, all the field values have been set.
There are several commands that take these arguments, so let's make a common routine that can be called for each of these commands:
void parseFields( int field[], const size_t MAX_FIELDS )
{
char *token = strtok( packetBuffer, ", " ); // skip past command
for (int i = 0; i < MAX_FIELDS; i++) {
if (token) {
field[i] = atoi( token );
token = strtok( NULL, "," ); // step to the next comma separator
} else {
field[i] = 0;
}
}
}
Processing the "setdate" command looks like this:
else if ( isCommand("setdate")) {
Serial.println("setdate");
const size_t MAX_FIELDS = 7;
int field[ MAX_FIELDS ]; // parsed ints
parseFields( field, MAX_FIELDS );
You can use those array elements to set global variables:
int parsed_month = field[1];
int parsed_day = field[2];
int parsed_year = field[3];
int parsed_hour = field[4];
int parsed_minute = field[5];
int parsed_second = field[6];
EEPROM.write(0,parsed_hour);
EEPROM.write(1,parsed_minute);
EEPROM.write(2,parsed_second);
EEPROM.write(3,parsed_day);
EEPROM.write(4,parsed_month);
EEPROM.write(5,parsed_year);
RTC.adjust(DateTime(parsed_year, parsed_month, parsed_day, parsed_hour, parsed_minute, parsed_second));
4) For testing, it is much easier to enter commands in the Serial Monitor window. We can add some code that simulates all the external code, like the Ethernet Udp class, RTC class, insert_sql routine, etc. Here is what I used for testing:
class FakeUdp
{
public:
size_t parsePacket() { return packetSize; }
void read( char *, size_t ) {};
};
FakeUdp Udp;
class DateTime
{
public:
DateTime(int,int,int,int,int,int) {}
};
class FakeRTC
{
public:
void adjust( const DateTime & ) {}
};
FakeRTC RTC;
class FakeEEPROM
{
public:
void write( int address, int value )
{
Serial.print( F("EEPROM[") );
Serial.print( address );
Serial.print( F("] = ") );
Serial.println( value );
}
};
FakeEEPROM EEPROM;
void RestoreDefaults() {}
void turnRelay( int r, int state )
{
Serial.print(F("turnRelay "));
Serial.print(r);
Serial.print( ' ' );
if (state == 0)
Serial.println(F("off"));
else
Serial.println(F("on"));
}
enum unknownVariable { Relays };
void insert_sql( int, int ) {}
int Relay1_State, Relay1_isAuto;
int Light_ON_hour, Light_ON_min, Light_OFF_hour, Light_OFF_min;
int NextRunLOG, NextRunLCD, NextRunTX;
I have attached the complete test program that should allow you to use the Serial Monitor window for command input and debug output. Notice that EEPROM writes don't actually go into the EEPROM; they are displayed instead. For example, entering "setdate 6,2,18,11,35,23" in the Serial Monitor window (press Send) causes this debug output:
Received packet! 'setdate 6,2,18,11,35,23'
setdate
EEPROM[0] = 11
EEPROM[1] = 35
EEPROM[2] = 23
EEPROM[3] = 2
EEPROM[4] = 6
EEPROM[5] = 18
You should be able to test all the commands very quickly. When they all work, merge the helper routines (isCommand and parseFields) and udpEvent back into your real sketch.
If there's some portion that you're not sure about how to convert, please feel free to ask for ideas. The C string library is very extensive, and it can be overwhelming to first-time users.
Cheers,
/dev
P.S. Relays 1-8 really should be implemented as an array, but you should start with these changes first.
ve3sjk.ino (5.46 KB)