//************************************************************ // this is a simple example that uses the painlessMesh library // // 1. sends a silly message to every node on the mesh at a random time between 1 // and 5 seconds // 2. prints anything it receives to Serial.print // 3. has OTA support and can be updated remotely // //************************************************************ //needs these: //https://github.com/me-no-dev/AsyncTCP //https://github.com/me-no-dev/ESPAsyncTCP //https://github.com/me-no-dev/ESPAsyncWebServer //https://randomnerdtutorials.com/esp-mesh-esp32-esp8266-painlessmesh/ //Outpost Mesh OTA RECEIVER //This will let itself be updated over the air by ota sender //role type defines what ota update firmware applies to this device: // simplesensor - sends numeric data //simplerelay - accepts simple on/off actions // v.0.0.1 - initial // v.0.0.2 - version, ota receiver and ota sender, better serial info // v.0.0.3 - mesh.onReceive moved to setup, msg sent as : delimited strings (command:waterpump:switch:on , response:waterpump:switch_status:on) // v.0.0.4 - no more random chatter, basic switch actions with timer, main actions are now set in doactions() // v.0.0.5 - short commands: whoami, orders //todo monospace responses for table view. Use command:all:get:status #include "namedMesh.h" #define NODE_VERSION "ota-receiver-0.0.4" //role and version number #define MESH_PREFIX "SensorSwarm" //Outpost Mesh, SwarmSense #define MESH_PASSWORD "1L.24edckin3487bH^g36g3ud91" #define MESH_PORT 5555 String nodeName = "waterbox"; // Name needs to be unique. Waterlevel, Waterpump, Frontlights, String nodeRole = "simpleswitch"; //this name should be found in the ota bin file firmware__.bin (ex: firmware_ESP8266_simplerelay.bin) //analogread = sends numeric data //digitalread = sends numeric data //simpleswitch = accepts on/off commands //board specific const int relayPin = D1; unsigned long interval = 5000; // pause for two seconds int relayState = LOW; unsigned long previousMillis = 0; //serial control const byte numChars = 80; char receivedSerialChars[numChars]; // an array to store the received data boolean newSerialData = false; //command it receives regardless if important String cmdType; String cmdTarget; String cmdKey; String cmdValue; //command it decides it should be active String actCmdType; String actCmdTarget; String actCmdKey; String actCmdValue; Scheduler userScheduler; // to control your personal task namedMesh mesh; // User stub void sendReport(); //declair first //set a task to run sendReport that runs the task again Task taskSendMessage(TASK_SECOND * 1, TASK_FOREVER, &sendReport); void sendReport() { //@@todo: read sensor and save status variable, then broadcast this to entire mesh once every x secs (longer to allow intercalations) //command reports are triggered in the callbac func //set the task again taskSendMessage.setInterval(random(TASK_SECOND * 1, TASK_SECOND * 5)); } //say hello when node connects void sayHello(){ //broadcast to all String msg = ""; msg.reserve(80); msg += "hello:"; msg += nodeName; msg += ":"; msg += nodeRole; msg += "__"; msg += NODE_VERSION; msg += ":"; msg += mesh.getNodeId(); //mesh.getNodeId(); mesh.sendBroadcast(msg); } //todo //command is 'read_pressure', value is 'null' //or for action devices command is 'switch' value is 'on' void reportValueToAll(String command, String value){ //broadcast to all String myvalue = String(mesh.getNodeId()); //"myval123"; String msg = ""; msg.reserve(80); //@@todo: :value: is the type of sensor value, for example 'pressure', and myvalue is '30psi' msg += "response:"; msg += nodeName; msg += ":"; msg += command; msg += ":"; msg += myvalue; //mesh.getNodeId(); mesh.sendBroadcast(msg); } //--- void setActiveCommand(String cmd, String targ, String k, String v){ actCmdType = cmd; actCmdTarget = targ; actCmdKey = k; actCmdValue = v; } // Needed for painless library // loaded in setup so it can support both sent to name and to id void receivedCallback(String &from, String &msg) { //uint32_t from bool shouldBeActive = true; //first load in temp command, later if nothing interferes , move to active parseCommandString(msg); //msg_char_arr //make sure its command if(cmdType == "command" || cmdType == "to"){ //if(strcmp(cmdType, String("command").c_str()) == 0 || strcmp(cmdType, String("to").c_str()) == 0 ){ //make sure its for me if(cmdTarget == nodeName || cmdTarget == "all"){ //reportValueToAll(cmdKey,cmdValue); //delivery confirmation String resp_from = from; String resp_msg = "message received"; mesh.sendSingle(resp_from, resp_msg); //catch all non active functions if(cmdKey == "get" || cmdKey == "read"){ shouldBeActive = false; //do not make active, it will be handled in doactions before active command } }else{ Serial.println("message not for me"); shouldBeActive = false; } } //if no other use was found for this command, assue its the one thats supposed to be active/run if(shouldBeActive){ setActiveCommand(cmdType, cmdTarget, cmdKey, cmdValue); } Serial.print("[From:"); Serial.print(from); Serial.print("] "); Serial.println(msg); } void newConnectionCallback(uint32_t nodeId) { //Serial.print("[Node id:"); //Serial.print(mesh.getNodeId()); //Serial.print("[Node:"); //Serial.print(nodeName); Serial.printf("Detected new connection, nodeId = %u", nodeId); Serial.println(); } void changedConnectionCallback() { //Serial.print("[Node id:"); //Serial.print(mesh.getNodeId()); //Serial.print("[Node:"); //Serial.print(nodeName); Serial.printf("Changed connections"); Serial.println(); } void nodeTimeAdjustedCallback(int32_t offset) { //Serial.print("[Node id:"); //Serial.print(mesh.getNodeId()); //Serial.print("[Node:"); //Serial.print(nodeName); Serial.printf("Adjusted time %u. Offset = %d", mesh.getNodeTime(), offset); Serial.println(); } //receive serial command followed by enter (new line) void readSerialCommands() { static byte ndx = 0; char endMarker = '\n'; char rc; while (Serial.available() > 0 && newSerialData == false) { rc = Serial.read(); if (rc != endMarker) { receivedSerialChars[ndx] = rc; ndx++; if (ndx >= numChars) { ndx = numChars - 1; } } else { receivedSerialChars[ndx] = '\0'; // terminate the string ndx = 0; newSerialData = true; } } } // split command string into its parts //first cmd is loaded , then if a valid command is detected that should replace the current one, cmd is trasnfered to actCmd. This allows other intermediary commands to be run via parseCommandString, not just the active one void parseCommandString( String receivedcmd ) { cmdType = getValue(receivedcmd,':',0); cmdTarget = getValue(receivedcmd,':',1); cmdKey = getValue(receivedcmd,':',2); cmdValue = getValue(receivedcmd,':',3); if(cmdType.isEmpty()){ Serial.println("Warn: cmdType (1st element of a:b:c:d command), is missing"); } if(cmdTarget.isEmpty()){ Serial.println("Warn: cmdTarget (2nd element of a:b:c:d command), is missing"); } //if(cmdKey.isEmpty()){ // Serial.println("Warn: cmdKey (3rd element of a:b:c:d command), is missing"); //} } String getValue(String data, char separator, int index) { int found = 0; int strIndex[] = { 0, -1 }; int maxIndex = data.length() - 1; for (int i = 0; i <= maxIndex && found <= index; i++) { if (data.charAt(i) == separator || i == maxIndex) { found++; strIndex[0] = strIndex[1] + 1; strIndex[1] = (i == maxIndex) ? i+1 : i; } } return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; } void setup() { Serial.begin(115200); //mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on mesh.setDebugMsgTypes(ERROR | STARTUP); // | DEBUG// set before init() so that you can see startup messages mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT); mesh.setName(nodeName); // This needs to be an unique name! //mesh.onReceive([](uint32_t from, String &msg) { // // int str_len = msg.length() + 1; // char msg_char_arr[str_len]; // msg.toCharArray(msg_char_arr, str_len); // // Serial.println(); // Serial.print("[Node id:"); // Serial.print(mesh.getNodeId()); // Serial.print(" name:"); // Serial.print(nodeName); // Serial.printf("] (todo1) Receive callback: Received from %u msg=%s\n", from, msg.c_str()); // // parseCommandString(msg_char_arr); // String to = cmdTarget; // String msg_command = receivedSerialChars; // mesh.sendSingle(to, msg_command); // //}); //second definition? //mesh.onReceive([](String &from, String &msg) { // Serial.printf(" (todo2) Received message by name from: %s, %s\n", from.c_str(), msg.c_str()); //}); mesh.onReceive(&receivedCallback); mesh.onNewConnection(&newConnectionCallback); mesh.onChangedConnections(&changedConnectionCallback); mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback); // if you want your node to accept OTA firmware, simply include this line // with whatever role you want your hardware to be. For instance, a // mesh network may have a thermometer, rain detector, and bridge. Each of // those may require different firmware, so different roles are preferrable. // // MAKE SURE YOUR UPLOADED OTA FIRMWARE INCLUDES OTA SUPPORT OR YOU WILL LOSE // THE ABILITY TO UPLOAD MORE FIRMWARE OVER OTA. YOU ALSO WANT TO MAKE SURE // THE ROLES ARE CORRECT mesh.initOTAReceive(nodeRole); userScheduler.addTask(taskSendMessage); taskSendMessage.enable(); //board specific pinMode(relayPin, OUTPUT); sayHello(); //broadcasts hello to entire mesh } //commands are read in temp commands first (cmd), then moved to actCmd if they are worth it. //serial commands will just run local actions, and any important action will be sent to run here via the mesh (this means no locally available functions, to avoid redundant code) void actOnSerialCommand(){ bool shortcmd = false; //flag to detect //detect serial command if (newSerialData == true) { //Serial.print("Serial input: "); //Serial.println(receivedSerialChars); //snitch String msg = ""; msg.reserve(80); msg += " Received serial input: "; msg += receivedSerialChars; //mesh.getNodeId(); mesh.sendBroadcast(msg); //----- String serialmsg = receivedSerialChars; if(serialmsg == "whoami"){ Serial.print(" Node name:"); Serial.print(nodeName); Serial.print(" version:"); Serial.print(NODE_VERSION); Serial.print(" firmware role:"); Serial.print(nodeRole); Serial.println(); shortcmd = true; } if(serialmsg == "orders"){ Serial.print(" cmd:"); Serial.print(cmdType); Serial.print(" target:"); Serial.print(cmdTarget); Serial.print(" key:"); Serial.print(cmdKey); Serial.print(" val:"); Serial.print(cmdValue); Serial.println(); shortcmd = true; } //no short command detected, look for more complex if(!shortcmd){ //loads temp command with instructions parseCommandString(receivedSerialChars); //cmdType, cmdTarget, cmdKey, cmdValue //"\0" //if command is "command" or "to", will send to target node //@@todo: to: should be replaced with self: and allow internal actions, because node can not broadcast to itself //Serial.print(" will check for cmdtype ("); //Serial.print(cmdType); //Serial.print(") and cmdtarget is ("); //Serial.print(cmdTarget); //Serial.println(")"); if(cmdType == "command" || cmdType == "to" ){ //if(strcmp(cmdType, String("command").c_str()) == 0 || strcmp(cmdType, String("to").c_str()) == 0 ){ Serial.print("Sending to: "); Serial.print(cmdTarget); Serial.print(" Msg: "); Serial.print(receivedSerialChars); Serial.println(); if(cmdTarget=="all"){ String msg = receivedSerialChars; msg.reserve(80); mesh.sendBroadcast(msg); }else{ String to = cmdTarget; String msg_command = receivedSerialChars; mesh.sendSingle(to, msg_command); } }else{ //anything that is not detected as short command or does not have a valid command suntax, falls here Serial.print("Undefined serial command: ("); Serial.print(cmdType); Serial.print(") To: ("); Serial.print(cmdTarget); Serial.println(")"); Serial.print("Trying to parse:"); Serial.println(receivedSerialChars); } } newSerialData = false; //reset } } //erase current actCmd fields so it no longer consdiers it has to do any active (continous) command void releaseActiveCommand(){ actCmdType = ""; actCmdTarget = ""; actCmdKey = ""; actCmdValue = ""; } //release any command you just read from serial or received via broadcast void releaseTempCommand(){ cmdType = ""; cmdTarget = ""; cmdKey = ""; cmdValue = ""; } //[command|to]:[node_name|all]:[on|off]:(secs) //[get|read]:[node_name|all]:[status] void doactions(){ unsigned long currentMillis = millis(); //String cmdType; //String cmdTarget; //String cmdKey; //String cmdValue; //process temporary commands, this will not change current active commands, and can not be something that runs continuously. Use for status reads if(cmdTarget == nodeName || cmdTarget == "all"){ if(cmdType == "get" || cmdType == "read"){ if(actCmdKey == "status"){ String msg = ""; msg.reserve(80); msg += "response:"; msg += nodeName; msg += ":status:"; mesh.sendBroadcast(msg); if(nodeRole == "simpleswitch"){ if(relayState){ msg += "on"; }else{ msg += "off"; } mesh.sendBroadcast(msg); releaseTempCommand(); //end this, but still go on with active comm } //--- if(nodeRole == "digitalread"){ //todo msg += "todo"; mesh.sendBroadcast(msg); releaseTempCommand(); //end this, but still go on with active comm } if(nodeRole == "analogread"){ //todo msg += "todo"; mesh.sendBroadcast(msg); releaseTempCommand(); //end this, but still go on with active comm } } } } if(actCmdTarget == nodeName || actCmdTarget == "all"){ if(actCmdType == "command" || actCmdType == "to"){ if(actCmdKey == "on"){ //if off, start the timer and switch it on if(relayState == LOW){ //switch on relayState = HIGH; digitalWrite(relayPin, relayState); if(actCmdValue.toInt()>0){ //do not end, wait for interval previousMillis = currentMillis; //init timer interval = actCmdValue.toInt()*1000; //seconds to milis mesh.sendBroadcast("Switched ON for "+actCmdValue+" seconds"); }else{ mesh.sendBroadcast("Switched ON indefinitely"); releaseActiveCommand(); //end } }else{ //if its already on if (currentMillis - previousMillis >= interval) { // if enough millis have elapsed //end action relayState = LOW; digitalWrite(relayPin, relayState); mesh.sendBroadcast("Switched OFF after "+actCmdValue+" seconds"); releaseActiveCommand(); //end }else{ //do nothing, its still on and during the interval } } } //-------- if(actCmdKey == "off"){ //if on switch permanently off if(relayState == HIGH){ //switch off relayState = LOW; digitalWrite(relayPin, relayState); mesh.sendBroadcast("Switched OFF"); releaseActiveCommand(); //end }else{ //already off } } //------- }else{ //no action command } }else{ //not me } } void loop() { readSerialCommands(); //receive serial command actOnSerialCommand(); //do something about received serial commands doactions(); //meshtastic mesh.update();// it will run the user scheduler as well }