Programming Propane: Espnow And Fire Shooting Walkway Lights
About the project
To feel more awesome and badass, I upgraded my suburban walkway lights. Instead of typical bulbs, they're propane canisters with solenoid valves, high-voltage arc generators, and esp32 microcontrollers using ESPNow. This local network lets me command them to shoot fire on cue in my yard.
Project info
Difficulty: Moderate
Platforms: Espressif
Estimated time: 1 day
License: GNU General Public License, version 3 or later (GPL3+)
Items used in this project
Hardware components
View all
Story
First Electromaker Post!!!
The Concept: Upon returning to my idyllic suburbanite life after a long day's work, I wanted to feel more awesome. More badass. The solution? Fire and (electric guitar) power chords.
I had an idea inspired by my buddy Tomasz, who shares all of his projects here.
Specifically, he made for me the best gift ever:
The Concept: Upon returning to my idyllic suburbanite life after a long day's work, I wanted to feel more awesome. More badass. The solution? Fire and (electric guitar) power chords.
I had an idea inspired by my buddy Tomasz, who shares all of his projects here.
Specifically, he made for me the best gift ever:
Step 1: I figured out a way to use 12V to a) allow propane to rapidly escape a 1 pound propane tank and b) ignite said propane with high voltage.
I used a 1/4" solenoid valve and an adapter:
(Below: Concept working)
Step 2: I had to learn how to control fairly high current (max 4A) at 12V with an esp32.
(Below: a prototyping board, a couple relays, a 12V to 5V converter, and the arc generator)
That worked out alright. It was then time to program an esp32 to be the ESPNow master controller, and to program the board above as the ESPNow "voluntary helper" (formerly "slave" per the ESPNow library). Random Nerd Tutorials was a gigantic resource in this project, and they are the creators of many, many helpful exp32 projects.
It worked:
It's worth mentioning here- I was using a push-button board I made for the Master controller esp32, but it only had 3 buttons (as well as my first attempt at hardwarre debouncing). I also screwed around with the capacitive touch pins. Ultimately, I wanted to send enough different signals that I bought a Blackberyy BBQ20 board that had been modded to run on i2c (and many other protocols). It worked great, and allowed me to quickly set up man different firing options (e.g. the timings on how long the valve remained open, how long the igniter fired) hot-keyed to individual burners. It worked good.
Step 3: Since I wanted 3 pairs of two of these (to resemble walkway lights), there was no way I was going to make another 5 proto-boards. Additionally, the protoboards were far bigger than necessary. I made my first 12V relay board with standoffs to insert the esp32s. I'm sure there are things that could hae been done better, but I was happy with the small form factor.
Below: The schematic I made using EasyEDA (this is included under prtoject assets) and the board view
Here's a quick walk-through of the board:
Step 4: It was time to use my embryonic Fusion 360 skills to design housing for the controller board and the spark generator. Please note! Capacitors would likely be helpful for voltage spikes (and were included in the original design) but I had no way to discharge them so I clipped them out.
The design came out something like this:
Step 5: Working out bugs- I am gonna put a lot here, a I ran into a lot of problems and I lay them out here in case somebody were to encounter something similar:
To be clear, it took forever for me to get from one working flame pooger to six that worked together. I encountered a LOT of problems, most of which were my own fault.
a. Air-fuel mixture: Propane (and i think all hydrocarbon fuels for that matter) do not combust in an oxidation-combustion reaction unless they have, well, an oxidizer. Typically, this oxidizer is O2 molecular oxygen that comprises 21% of breathable air. Propane torches typically have a simple carberator, which use the venturi effect to mix expelled propane with atmospheric air. I did my best to design the flame poofers so air mixed with the propane enough that the high voltage igniter would trigger the combustion reaction.
Here’s the problem: the pressure at which the propane is expelled lowers each time I consecutively fire them. Why? LIQUID propane is in the tank under pressure, so gravity pulls liquid propane to the bottom, and GAS propane fills up the rest of the space. Liquid propane turns to gas at -40C, and the pressure of the tank forces some of the gas into a liquid state.
Now, every time the cannister fires, the pressurized gas is released into the atmosphere, which in turn lowers the pressure in the cannister as it tries to equilibriate with atmospheric pressure. When the solenoid valve closes and no more gas can be expelled, some of the liquid propane phase-transitions to gas. This requires energy, in the form of leaching heat from the metal cannister. As a result, the cannister gets colder each time it fires. This colder temperature decreases the energy in the gas, so subsequent poofs contain lower energy gas, which in turn means less fuel is expelled, changing the air/fuel mixture to a leaner (less gas) mix. Luckily, the poofers ignite better with the leaner mix.
It’s worth stating here, the air fuel mix of the poofers is ALWAYS on the rich side, even at it’s leanest. This is aesthetically good, but it also means the flames contain less energy, and are generally pretty safe. I could swipe my hand through the mix, losing some hair, but not seriously injuring my self. It also means it is hard for these to ignite anything I DON’T want ignited (like the wooden beams in my shop).
b. The arc generators: I originally housed the arc generators right next to the esp32 chips. Bad idea. When the high voltage arc is created, it stops the esp32 code running, but any open solenoids are still powered. This is typically evident by a burning pillar of propane that will not cease until I kill the power. Slightly unnerving, slightly awesome.
Below: The chip crashing and the valve open with terrifying fire erupting:
Step 6: The Code
I am not a software designer. I program when I need to and when it is fun. I have no doubt this code is horrible. That said, I control fire in my front yard, so it has done what I've asked of it : )
Below: code for the ESP Now MASTER chip
- /*
- This is poorly written code shared by Dan from Gears, Code, and Fire
- https://youtube.com/@gearscodeandfire
- Gigantic credit to Rui Santos of randomnerdtutorials.com
- You are advised against reproducing this for the original putpose (shooting fire)
- That said, this is simply code for the master chip using the ESPNow library.
- Pressing buttons on the recommended keyboard (https://www.tindie.com/products/arturo182/bb-q20-keyboard-with-trackpad-usbi2cpmod/)
- controls varying combos of 6 ESPNow slave esp32 chips running two 12V relays and an LED
- Originally posted 9/23/2023 to https://www.electromaker.io/
- v5: instead of broadcasting to all, i iterate over it and it works fine now
- */
- #include <Arduino.h>
- #include <esp_now.h>
- #include <WiFi.h>
- #include <algorithm>
- #include <BBQ10Keyboard.h>
- #include <iostream>
- #include <string>
- #define CHANNEL 0
- #define PRINTSCANRESULTS 0
- #define NUMSLAVES 7 //
- esp_now_peer_info_t slaves[NUMSLAVES] = {};
- BBQ10Keyboard keyboard;
- int SlaveCnt = 0;
- //led onboard
- const int ledPin = 2; // the number of the LED pin
- // Set up tactile buttons // I stopped using this approach at some point
- struct Button {
- const int pin;
- bool currentState;
- bool prevState;
- };
- //I am not using these in this code (they are here for redundancy), but they reflect the mac addresses of the six slave esp32s
- String burnerMac[6] = {"94:3C:C6:33:99:24",
- "08:B6:1F:34:A5:50",
- "08:B6:1F:3B:53:1C",
- "C8:F0:9E:74:E1:A0",
- "08:B6:1F:3D:22:70",
- "C8:F0:9E:50:7D:40"};
- /* This was discovery info logged form ESPNow about my 6 slave chips
- 1: Slave:C8:F0:9E:74:E1:A0 [C8:F0:9E:74:E1:A1] (-48) burner[3]
- 2: Slave:08:B6:1F:34:A5:50 [08:B6:1F:34:A5:51] (-51) burner[1]
- 5: Slave:C8:F0:9E:50:7D:40 [C8:F0:9E:50:7D:41] (-55) burner[5]
- 6: Slave:08:B6:1F:3B:53:1C [08:B6:1F:3B:53:1D] (-56) burner[2]
- 7: Slave:94:3C:C6:33:99:24 [94:3C:C6:33:99:25] (-61) burner[0]
- 8: Slave:08:B6:1F:3D:22:70 [08:B6:1F:3D:22:71] (-65) burner[4]
- */
- // I stopped using below as well, but leave it incase somebody wants to go with push-buttons
- const int numButtons = 6;
- Button buttons[numButtons] = {
- {4, false, false},
- {16, false, false},
- {17, false, false},
- {5, false, false},
- {18, false, false},
- {19, false, false}
- };
- //timers for ScanSlave and ManageSlave
- unsigned long scanTimer = 600000;
- unsigned long lastTimer = 0;
- //below is the basic transmission payload that I determine
- typedef struct cmd_struct {
- uint8_t burnerNumbers[6];
- bool fire[6];
- uint8_t igniterStartDelays[6]; //IMPORTANT: uint8_t is 1 byte, int is 4 bytes. Max value is 255, so I will have to multiply it by 10 on the client side
- //this is because the bytes are translated into time delays on the slave chip
- uint8_t igniterEndDelays[6];
- uint8_t solenoidEndDelays[6];
- } cmd_struct;
- typedef struct payload_struct {
- uint8_t burnerNumbers[6];
- bool fire[6];
- uint8_t igniterStartDelays[6]; //IMPORTANT: uint8_t is 1 byte, int is 4 bytes. Max value is 255, so I will have to multiply it by 10 on the client side
- // as above
- uint8_t igniterEndDelays[6];
- uint8_t solenoidEndDelays[6];
- } payload_struct;
- payload_struct* payload;
- // Init ESP Now with fallback
- void InitESPNow() {
- WiFi.disconnect();
- if (esp_now_init() == ESP_OK) {
- Serial.println("ESPNow Init Success");
- }
- else {
- Serial.println("ESPNow Init Failed");
- // Retry InitESPNow, add a counte and then restart?
- // InitESPNow();
- // or Simply Restart
- ESP.restart();
- }
- }
- // Scan for slaves in AP mode
- void ScanForSlave() {
- int8_t scanResults = WiFi.scanNetworks(); //returns an int8_t of compatible devices (e.g. in AP mode)
- //reset slaves
- memset(slaves, 0, sizeof(slaves));
- SlaveCnt = 0;
- Serial.println("");
- if (scanResults == 0) {
- Serial.println("No WiFi devices in AP Mode found");
- } else {
- Serial.print("Found "); Serial.print(scanResults); Serial.println(" devices ");
- for (int i = 0; i < scanResults; ++i) {
- // Print SSID and RSSI for each device found
- String SSID = WiFi.SSID(i);
- int32_t RSSI = WiFi.RSSI(i);
- String BSSIDstr = WiFi.BSSIDstr(i);
- if (PRINTSCANRESULTS) { //initialized to 0
- Serial.print(i + 1); Serial.print(": "); Serial.print(SSID); Serial.print(" ["); Serial.print(BSSIDstr); Serial.print("]"); Serial.print(" ("); Serial.print(RSSI); Serial.print(")"); Serial.println("");
- }
- delay(10);
- // Check if the current device starts with `Slave`
- if (SSID.indexOf("Slave") == 0) {
- // SSID of interest
- Serial.print(i + 1); Serial.print(": "); Serial.print(SSID); Serial.print(" ["); Serial.print(BSSIDstr); Serial.print("]"); Serial.print(" ("); Serial.print(RSSI); Serial.print(")"); Serial.println("");
- // Get BSSID => Mac Address of the Slave
- int mac[6];
- if ( 6 == sscanf(BSSIDstr.c_str(), "%x:%x:%x:%x:%x:%x", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5] ) ) {
- for (int ii = 0; ii < 6; ++ii ) {
- slaves[SlaveCnt].peer_addr[ii] = (uint8_t) mac[ii];
- } // it appears the variable slaves[i].peer_addr = an array of 6 hexidecimal numbers
- }
- slaves[SlaveCnt].channel = CHANNEL; // pick a channel
- slaves[SlaveCnt].encrypt = 0; // no encryption
- SlaveCnt++;
- }
- }
- }
- if (SlaveCnt > 0) {
- Serial.print(SlaveCnt); Serial.println(" Slave(s) found, processing..");
- } else {
- Serial.println("No Slave Found, trying again.");
- }
- // clean up ram
- WiFi.scanDelete();
- }
- // Check if the slave is already paired with the master.
- // If not, pair the slave with master
- void manageSlave() {
- if (SlaveCnt > 0) {
- for (int i = 0; i < SlaveCnt; i++) {
- Serial.print("Processing: ");
- for (int ii = 0; ii < 6; ++ii ) { //purely for outputting mac addresses of slaves; Slave[i] is int, Slave[i].peeraddr is mac in an array[6]
- Serial.print((uint8_t) slaves[i].peer_addr[ii], HEX);
- if (ii != 5) Serial.print(":");
- }
- Serial.print(" Status: ");
- // check if the peer exists
- bool exists = esp_now_is_peer_exist(slaves[i].peer_addr);
- if (exists) {
- // Slave already paired.
- Serial.println("Already Paired");
- } else {
- // Slave not paired, attempt pair
- esp_err_t addStatus = esp_now_add_peer(&slaves[i]);
- if (addStatus == ESP_OK) {
- // Pair success
- Serial.println("Pair success");
- //THIS IS WHERE I SHOULD WRITE IT TO EEPROM, and on the SLAVE SIDE I SHOULD DO THE SAME
- } else if (addStatus == ESP_ERR_ESPNOW_NOT_INIT) {
- // How did we get so far!!
- Serial.println("ESPNOW Not Init");
- } else if (addStatus == ESP_ERR_ESPNOW_ARG) {
- Serial.println("Add Peer - Invalid Argument");
- } else if (addStatus == ESP_ERR_ESPNOW_FULL) {
- Serial.println("Peer list full");
- } else if (addStatus == ESP_ERR_ESPNOW_NO_MEM) {
- Serial.println("Out of memory");
- } else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
- Serial.println("Peer Exists");
- } else {
- Serial.println("Not sure what happened");
- }
- //delay(100);
- }
- }
- } else {
- // No slave found to process
- Serial.println("No Slave found to process");
- }
- }
- void sendData(const uint8_t* burnerNumbers, const bool* fire, const uint8_t* igniterStartDelays, const uint8_t* igniterEndDelays,const uint8_t* solenoidEndDelays) {
- cmd_struct cmd;
- std::copy(burnerNumbers, burnerNumbers+6, cmd.burnerNumbers);
- std::copy(fire, fire+6, cmd.fire);
- std::copy(igniterStartDelays, igniterStartDelays+6, cmd.igniterStartDelays);
- std::copy(igniterEndDelays, igniterEndDelays+6, cmd.igniterEndDelays);
- std::copy(solenoidEndDelays, solenoidEndDelays+6, cmd.solenoidEndDelays);
- //esp_err_t result = esp_now_send(peer_addr, &test, sizeof(test));
- for(int i=0; i<SlaveCnt; i++){ //QUESTION: is slaves[i] same as cmd.burnerNumbers[i]
- esp_err_t result = esp_now_send(slaves[i].peer_addr, (uint8_t *) &cmd, sizeof(cmd_struct)); //peer_addr replaced with 0 for all
- Serial.print("Send Status: ");
- if (result == ESP_OK) {
- Serial.println("Success");
- } else if (result == ESP_ERR_ESPNOW_NOT_INIT) {
- Serial.println("ESPNOW not Init.");
- } else if (result == ESP_ERR_ESPNOW_ARG) {
- Serial.println("Invalid Argument");
- } else if (result == ESP_ERR_ESPNOW_INTERNAL) {
- Serial.println("Internal Error");
- } else if (result == ESP_ERR_ESPNOW_NO_MEM) {
- Serial.println("ESP_ERR_ESPNOW_NO_MEM");
- } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
- Serial.println("Peer not found.");
- } else {
- Serial.println("Not sure what happened");
- }
- }
- }
- // callback when data is sent from Master to Slave
- void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
- /*char macStr[18];
- snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
- mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
- Serial.print("Last Packet Sent to: "); Serial.println(macStr);
- Serial.print("Last Packet Send Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
- */
- }
- bool wasClicked(int x){
- if(!buttons[x].currentState && buttons[x].prevState){
- return true;
- }
- else
- return false;
- }
- // callback when data is recv from Master
- void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
- char macStr[18];
- payload = (payload_struct*) data;
- snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
- mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
- // DEBUG
- Serial.print("Last Packet Recv from: "); Serial.println(macStr);
- }
- void setup() {
- Serial.begin(115200);
- delay(1000);
- WiFi.mode(WIFI_STA);
- Serial.println("ESPNow ***MASTER***");
- Serial.print("STA MAC: "); Serial.println(WiFi.macAddress());
- Wire.begin();
- keyboard.begin();
- keyboard.setBacklight(0.5f);
- InitESPNow();
- esp_now_register_send_cb(OnDataSent);
- esp_now_register_recv_cb(OnDataRecv);
- // DECLARE IO PINS
- pinMode(ledPin, OUTPUT);
- for(int i=0; i<6; i++)
- pinMode(buttons[i].pin, INPUT_PULLDOWN);
- //Initial scan
- ScanForSlave(); // I need to not do this repeatedly and add a timer
- if (SlaveCnt > 0) // check if slave channel is defined
- manageSlave(); // THIS TAKES ABOUT 6 SECONDS AND SHOULD NOT BE CALLED IN THE LOOOP UNLESS NECESSARY
- Serial.println("Setup complete");
- }
- void loop() {
- const int keyCount = keyboard.keyCount();
- uint8_t burnerNumbers[6] = {0, 1, 2, 3, 4, 5};
- bool fire[6] = {0, 0, 0, 0, 0, 0};
- uint8_t igniterStartDelays[6] = {0, 0, 0, 0, 0, 0}; //IMPORTANT: uint8_t is 1 byte, int is 4 bytes. Max value is 255, so I will have to multiply it by 10 on the client side
- uint8_t igniterEndDelays[6] = {1, 1, 1, 1, 1, 1};
- uint8_t solenoidEndDelays[6] = {2, 2, 2, 2, 2, 2,};
- for(int i=0; i<6; i++){
- buttons[i].currentState = digitalRead(buttons[i].pin);
- if (buttons[i].currentState == LOW) digitalWrite(ledPin, LOW);
- else digitalWrite(ledPin, HIGH);
- }
- if(millis()-lastTimer > scanTimer){
- lastTimer = millis();
- ScanForSlave(); // I need to not do this repeatedly and add a timer
- if (SlaveCnt > 0) // check if slave channel is defined
- manageSlave(); // THIS TAKES ABOUT 6 SECONDS AND SHOULD NOT BE CALLED IN THE LOOOP UNLESS NECESSARY
- }
- //check who got clicked
- for(uint8_t i=0; i<6; i++){
- if(wasClicked(i)){
- fire[i] = 1;
- igniterStartDelays[i] = 50; // MAKE SURE TO MULTIPLY THESE x10 in the SLAVE CODE
- igniterEndDelays[i] = 4;
- solenoidEndDelays[i] = 5;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- Serial.printf("Button %d was clickedn", i);
- }
- }
- for(int i=0; i<6; i++)
- buttons[i].prevState = buttons[i].currentState;
- if (keyCount != 0){
- uint8_t burnerNumbers[6] = {0, 1, 2, 3, 4, 5};
- bool fire[6] = {0, 0, 0, 0, 0, 0};
- uint8_t igniterStartDelays[6] = {0, 0, 0, 0, 0, 0}; //IMPORTANT: uint8_t is 1 byte, int is 4 bytes. Max value is 255, so I will have to multiply it by 10 on the client side
- uint8_t igniterEndDelays[6] = {1, 1, 1, 1, 1, 1};
- uint8_t solenoidEndDelays[6] = {2, 2, 2, 2, 2, 2,};
- const BBQ10Keyboard::KeyEvent key = keyboard.keyEvent();
- if (key.state == BBQ10Keyboard::StatePress){
- Serial.print(key.key); Serial.println(" pressed");
- switch(key.key){
- case 'w': //burner 0, small poof
- fire[0] = 1;
- igniterStartDelays[0] = 0;
- igniterEndDelays[0] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[0] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 's': //burner 0, bigger poof
- fire[0] = 1;
- igniterStartDelays[0] = 10; //eg 200
- igniterEndDelays[0] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[0] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'z': //burner 0, biggest poof
- fire[0] = 1;
- igniterStartDelays[0] = 0; //eg 200
- igniterEndDelays[0] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[0] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- /////////////////////////////////////////////////////////////////////////////
- case 'e': //burner 1, small poof
- fire[1] = 1;
- igniterStartDelays[1] = 0;
- igniterEndDelays[1] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[1] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'd': //burner 1, bigger poof
- fire[1] = 1;
- igniterStartDelays[1] = 10; //eg 200
- igniterEndDelays[1] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[1] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'x': //burner 1, biggest poof
- fire[1] = 1;
- igniterStartDelays[1] = 0; //eg 200
- igniterEndDelays[1] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[1] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- /////////////////////////////////////////////////////////////////////////////////////
- case 'r': //burner 2, small poof
- fire[2] = 1;
- igniterStartDelays[2] = 0;
- igniterEndDelays[2] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[2] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'f': //burner 2, bigger poof
- fire[2] = 1;
- igniterStartDelays[2] = 10; //eg 200
- igniterEndDelays[2] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[2] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'c': //burner 2, biggest poof
- fire[2] = 1;
- igniterStartDelays[2] = 0; //eg 200
- igniterEndDelays[2] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[2] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- Serial.println("w pressed");
- break;
- /////////////////////////////////////////////////////////////////////////////////////
- case 't': //burner 3, small poof
- fire[3] = 1;
- igniterStartDelays[3] = 0;
- igniterEndDelays[3] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[3] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'g': //burner 3, bigger poof
- fire[3] = 1;
- igniterStartDelays[3] = 10; //eg 200
- igniterEndDelays[3] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[3] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'v': //burner 3, biggest poof
- fire[3] = 1;
- igniterStartDelays[3] = 0; //eg 200
- igniterEndDelays[3] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[3] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- //////////////////////////////////////////////////////////////////////////
- case 'y': //burner 4, small poof
- fire[4] = 1;
- igniterStartDelays[4] = 0;
- igniterEndDelays[4] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[4] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'h': //burner 4, bigger poof
- fire[4] = 1;
- igniterStartDelays[4] = 10; //eg 200
- igniterEndDelays[4] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[4] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'b': //burner 4, biggest poof
- fire[4] = 1;
- igniterStartDelays[4] = 0; //eg 200
- igniterEndDelays[4] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[4] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- ///////////////////////////////////////////////////////////////////////////
- case 'u': //burner 5, small poof
- fire[5] = 1;
- igniterStartDelays[5] = 0;
- igniterEndDelays[5] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[5] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'j': //burner 5, bigger poof
- fire[5] = 1;
- igniterStartDelays[5] = 10; //eg 200
- igniterEndDelays[5] = 10; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[5] = 10;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'n': //burner 5, biggest poof
- fire[5] = 1;
- igniterStartDelays[5] = 0; //eg 200
- igniterEndDelays[5] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[5] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- //////////////////////// DUAL BURNERS !!!!!////////////////////////////
- case 'i': //burner 1 and 2 safe poof
- fire[0] = 1;
- igniterStartDelays[0] = 0; //eg 200
- igniterEndDelays[0] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[0] = 0;
- fire[1] = 1;
- igniterStartDelays[1] = 0; //eg 200
- igniterEndDelays[1] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[1] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'k': //burner 2 and 3 safe poof
- fire[2] = 1;
- igniterStartDelays[2] = 0; //eg 200
- igniterEndDelays[2] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[2] = 0;
- fire[3] = 1;
- igniterStartDelays[3] = 0; //eg 200
- igniterEndDelays[3] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[3] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- case 'm': //burner 2 and 3 safe poof
- fire[4] = 1;
- igniterStartDelays[4] = 0; //eg 200
- igniterEndDelays[4] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[4] = 0;
- fire[5] = 1;
- igniterStartDelays[5] = 0; //eg 200
- igniterEndDelays[5] = 20; //remember these ge 10x when the client finally receives them
- solenoidEndDelays[5] = 0;
- sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
- break;
- }
- }
- }
- }
Below: Code that can be placed on each ESP Now esp32 "voluntary helper" chip. Please note, if you want to reproduce this, you will first need to know the MAC addresses of all of the chips joining the network
- /*
- homegreeter slave v6
- This is poorly written code shared by Dan from Gears, Code, and Fire
- https://youtube.com/@gearscodeandfire
- Gigantic credit to Rui Santos of randomnerdtutorials.com
- You are advised against reproducing this for the original putpose (shooting fire)
- That said, this is simply code for the master chip using the ESPNow library.
- Pressing buttons on the recommended keyboard (https://www.tindie.com/products/arturo182/bb-q20-keyboard-with-trackpad-usbi2cpmod/)
- controls varying combos of 6 ESPNow slave esp32 chips running two 12V relays and an LED
- Originally posted 9/23/2023 to https://www.electromaker.io/
- v6: tweaked some logging
- */
- #include <Arduino.h>
- #include <esp_now.h>
- #include <WiFi.h>
- #include <iostream>
- #include <string>
- #define CHANNEL 0
- #define LEDPIN 2
- #define SOLENOIDPIN 4
- #define IGNITERPIN 5
- #define FIREPIN 33
- //when below is true, the main loop will execute the firing logic
- bool runFire = false;
- //globals for how long to run the fire routine
- int igniterStartDelay;
- int igniterEndDelay;
- int solenoidEndDelay;
- //flags for the loop
- bool igniterIsOn = false;
- bool solenoidIsOn = false;
- bool fireStarted = false;
- unsigned long startTime;
- //below is the basic transmission payload that I determine
- typedef struct payload_struct {
- uint8_t burnerNumbers[6];
- bool fire[6];
- uint8_t igniterStartDelays[6]; //IMPORTANT: uint8_t is 1 byte, int is 4 bytes. Max value is 255, so I will have to multiply it by 10 on the client side
- uint8_t igniterEndDelays[6];
- uint8_t solenoidEndDelays[6];
- } payload_struct;
- payload_struct* payload;
- //array of mac address Strings associated with burners
- String burnerMac[6] = {"94:3C:C6:33:99:24",
- "08:B6:1F:34:A5:50",
- "08:B6:1F:3B:53:1C",
- "C8:F0:9E:74:E1:A0",
- "08:B6:1F:3D:22:70",
- "C8:F0:9E:50:7D:40"};
- // Init ESP Now with fallback
- void InitESPNow() {
- WiFi.disconnect();
- if (esp_now_init() == ESP_OK) {
- Serial.println("ESPNow Init Success");
- }
- else {
- Serial.println("ESPNow Init Failed");
- // Retry InitESPNow, add a counte and then restart?
- // InitESPNow();
- // or Simply Restart
- ESP.restart();
- }
- }
- // config AP SSID
- void configDeviceAP() {
- String Prefix = "Slave:";
- //String Mac = WiFi.macAddress(); // i made this global for easy access
- String Mac = WiFi.macAddress();
- String SSID = Prefix + Mac;
- String Password = "123456789";
- bool result = WiFi.softAP(SSID.c_str(), Password.c_str(), CHANNEL, 0);
- if (!result) {
- Serial.println("AP Config failed.");
- } else {
- Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
- }
- }
- // callback when data is recv from Master
- void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
- char macStr[18];
- payload = (payload_struct*) data;
- snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
- mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
- // DEBUG
- //Serial.print("Last Packet Recv from: "); Serial.println(macStr);
- //solenoidStart always = 0
- //NEW CODE FOR NEW PAYLOAD 5/3/2023
- for(int i=0; i<6; i++){
- Serial.print("payload->fire["); Serial.print(i); Serial.print("]: "); Serial.println(payload->fire[i]);
- Serial.print("WiFi.macAddress()==burnerMac["); Serial.print(i); Serial.print("]: "); Serial.println(WiFi.macAddress()==burnerMac[i]);
- if(payload->fire[i] == true && WiFi.macAddress()==burnerMac[i]){
- //burner i is instructed to fire, check if burnerMac[i] is this device's mac
- //put timers in global variables igniterStartDelay, igniterEndDelay, solenoidEndDelay
- igniterStartDelay = payload->igniterStartDelays[i]*10;
- igniterEndDelay = payload->igniterEndDelays[i]*10;
- solenoidEndDelay = payload->solenoidEndDelays[i]*10;
- Serial.print ("Last Packet Recv:n");
- Serial.print("Bool Fire: "); Serial.println(payload->fire[i]);
- Serial.print("Int burnerNumber: "); Serial.println(payload->burnerNumbers[i]);
- runFire = true;
- }
- }
- Serial.println("");
- }
- void setup() {
- pinMode(IGNITERPIN, OUTPUT);
- digitalWrite(IGNITERPIN, LOW);
- pinMode(LEDPIN, OUTPUT);
- pinMode(SOLENOIDPIN, OUTPUT);
- pinMode(FIREPIN, OUTPUT);
- digitalWrite(SOLENOIDPIN, LOW);
- Serial.begin(115200);
- delay(1000);
- Serial.println("ESPNow ***SLAVE***");
- //Set device in AP mode to begin with
- WiFi.mode(WIFI_AP);
- // configure device AP mode
- configDeviceAP();
- // This is the mac address of the Slave in AP Mode
- Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
- Serial.print("Actual MAC: "); Serial.println(WiFi.macAddress());
- // Init ESPNow with a fallback logic
- InitESPNow();
- esp_now_register_recv_cb(OnDataRecv);
- Serial.println("Setup complete");
- }
- // the loop is very intentionally blocking code- I don't want to miss any logic to turn the thing off once it's on!
- void loop() {
- if(runFire){
- //noInterrupts();
- Serial.println("Fire Started");
- digitalWrite(FIREPIN, HIGH); //D33
- digitalWrite(LEDPIN, HIGH); //D2
- digitalWrite(SOLENOIDPIN, HIGH); //D4
- Serial.println("Solenoid On");
- delay(igniterStartDelay);
- digitalWrite(IGNITERPIN, HIGH); //D5
- Serial.println("Igniter On");
- delay(igniterEndDelay);
- digitalWrite(IGNITERPIN, LOW);
- Serial.println("Igniter Off");
- delay(solenoidEndDelay);
- digitalWrite(SOLENOIDPIN, LOW);
- Serial.println("Solenoid Off");
- runFire = false; //reset all the flags to off
- digitalWrite(FIREPIN, LOW); //D33
- digitalWrite(LEDPIN, LOW); //D2
- Serial.println("Fire Done");
- }
- }
And lastly, the video, which breaks things down into a fair amount of detail.
Leave your feedback...