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

MELIFE esp32-s 30-pin dev board MELIFE esp32-s 30-pin dev board x 2
uxcell a12062900ux0221 DC 3V Coil 5 Pins SPST Power Relay uxcell a12062900ux0221 DC 3V Coil 5 Pins SPST Power Relay x 2
MP1584EN buck converter MP1584EN buck converter x 1
NPN 2N2222A transistors NPN 2N2222A transistors x 1
1N4007 flyback diode 1N4007 flyback diode x 2
3.3k resistors 3.3k resistors x 2
220 resistors 220 resistors x 1
screw-down terminals screw-down terminals x 3
BB Q20 Keyboard with trackpad, USB/I2C/PMOD BB Q20 Keyboard with trackpad, USB/I2C/PMOD x 1
1/4 Inch NPT 12V/24V/110V/220V Brass Electric Solenoid Valve 2W025-08 1/4 Inch NPT 12V/24V/110V/220V Brass Electric Solenoid Valve 2W025-08 x 1
High Voltage Generator DC 6-12V to 1000kV Boost Step-Up Inverter Arc Pulse Generator High Voltage Generator DC 6-12V to 1000kV Boost Step-Up Inverter Arc Pulse Generator x 1
Custom PCB Custom PCB Design files included below x 1
Tungsten TIG welding electrode Tungsten TIG welding electrode Or other suitable conductive metal x 1

View all

Software apps and online services

Visual Studio Code Visual Studio Code

Hand tools and fabrication machines

FDM 3d printer FDM 3d printer x 1
Resin 3d printer Resin 3d printer x 1

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:


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

  1. /*
  2. This is poorly written code shared by Dan from Gears, Code, and Fire
  3. https://youtube.com/@gearscodeandfire
  4.  
  5. Gigantic credit to Rui Santos of randomnerdtutorials.com
  6.  
  7. You are advised against reproducing this for the original putpose (shooting fire)
  8. That said, this is simply code for the master chip using the ESPNow library.
  9. Pressing buttons on the recommended keyboard (https://www.tindie.com/products/arturo182/bb-q20-keyboard-with-trackpad-usbi2cpmod/)
  10. controls varying combos of 6 ESPNow slave esp32 chips running two 12V relays and an LED
  11.  
  12. Originally posted 9/23/2023 to https://www.electromaker.io/
  13.  
  14. v5: instead of broadcasting to all, i iterate over it and it works fine now
  15. */
  16. #include <Arduino.h>
  17. #include <esp_now.h>
  18. #include <WiFi.h>
  19. #include <algorithm>
  20. #include <BBQ10Keyboard.h>
  21. #include <iostream>
  22. #include <string>
  23.  
  24.  
  25. #define CHANNEL 0
  26. #define PRINTSCANRESULTS 0
  27. #define NUMSLAVES 7 //
  28.  
  29. esp_now_peer_info_t slaves[NUMSLAVES] = {};
  30. BBQ10Keyboard keyboard;
  31. int SlaveCnt = 0;
  32.  
  33. //led onboard
  34. const int ledPin = 2; // the number of the LED pin
  35.  
  36. // Set up tactile buttons // I stopped using this approach at some point
  37. struct Button {
  38. const int pin;
  39. bool currentState;
  40. bool prevState;
  41. };
  42.  
  43. //I am not using these in this code (they are here for redundancy), but they reflect the mac addresses of the six slave esp32s
  44. String burnerMac[6] = {"94:3C:C6:33:99:24",
  45. "08:B6:1F:34:A5:50",
  46. "08:B6:1F:3B:53:1C",
  47. "C8:F0:9E:74:E1:A0",
  48. "08:B6:1F:3D:22:70",
  49. "C8:F0:9E:50:7D:40"};
  50.  
  51. /* This was discovery info logged form ESPNow about my 6 slave chips
  52. 1: Slave:C8:F0:9E:74:E1:A0 [C8:F0:9E:74:E1:A1] (-48) burner[3]
  53. 2: Slave:08:B6:1F:34:A5:50 [08:B6:1F:34:A5:51] (-51) burner[1]
  54. 5: Slave:C8:F0:9E:50:7D:40 [C8:F0:9E:50:7D:41] (-55) burner[5]
  55. 6: Slave:08:B6:1F:3B:53:1C [08:B6:1F:3B:53:1D] (-56) burner[2]
  56. 7: Slave:94:3C:C6:33:99:24 [94:3C:C6:33:99:25] (-61) burner[0]
  57. 8: Slave:08:B6:1F:3D:22:70 [08:B6:1F:3D:22:71] (-65) burner[4]
  58. */
  59.  
  60.  
  61. // I stopped using below as well, but leave it incase somebody wants to go with push-buttons
  62. const int numButtons = 6;
  63. Button buttons[numButtons] = {
  64. {4, false, false},
  65. {16, false, false},
  66. {17, false, false},
  67. {5, false, false},
  68. {18, false, false},
  69. {19, false, false}
  70. };
  71.  
  72. //timers for ScanSlave and ManageSlave
  73. unsigned long scanTimer = 600000;
  74. unsigned long lastTimer = 0;
  75.  
  76. //below is the basic transmission payload that I determine
  77. typedef struct cmd_struct {
  78. uint8_t burnerNumbers[6];
  79. bool fire[6];
  80. 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
  81. //this is because the bytes are translated into time delays on the slave chip
  82. uint8_t igniterEndDelays[6];
  83. uint8_t solenoidEndDelays[6];
  84. } cmd_struct;
  85.  
  86. typedef struct payload_struct {
  87. uint8_t burnerNumbers[6];
  88. bool fire[6];
  89. 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
  90. // as above
  91. uint8_t igniterEndDelays[6];
  92. uint8_t solenoidEndDelays[6];
  93. } payload_struct;
  94.  
  95. payload_struct* payload;
  96.  
  97. // Init ESP Now with fallback
  98. void InitESPNow() {
  99. WiFi.disconnect();
  100. if (esp_now_init() == ESP_OK) {
  101. Serial.println("ESPNow Init Success");
  102. }
  103. else {
  104. Serial.println("ESPNow Init Failed");
  105. // Retry InitESPNow, add a counte and then restart?
  106. // InitESPNow();
  107. // or Simply Restart
  108. ESP.restart();
  109. }
  110. }
  111.  
  112. // Scan for slaves in AP mode
  113. void ScanForSlave() {
  114. int8_t scanResults = WiFi.scanNetworks(); //returns an int8_t of compatible devices (e.g. in AP mode)
  115. //reset slaves
  116. memset(slaves, 0, sizeof(slaves));
  117. SlaveCnt = 0;
  118. Serial.println("");
  119. if (scanResults == 0) {
  120. Serial.println("No WiFi devices in AP Mode found");
  121. } else {
  122. Serial.print("Found "); Serial.print(scanResults); Serial.println(" devices ");
  123. for (int i = 0; i < scanResults; ++i) {
  124. // Print SSID and RSSI for each device found
  125. String SSID = WiFi.SSID(i);
  126. int32_t RSSI = WiFi.RSSI(i);
  127. String BSSIDstr = WiFi.BSSIDstr(i);
  128.  
  129. if (PRINTSCANRESULTS) { //initialized to 0
  130. 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("");
  131. }
  132. delay(10);
  133. // Check if the current device starts with `Slave`
  134. if (SSID.indexOf("Slave") == 0) {
  135. // SSID of interest
  136. 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("");
  137. // Get BSSID => Mac Address of the Slave
  138. int mac[6];
  139.  
  140. if ( 6 == sscanf(BSSIDstr.c_str(), "%x:%x:%x:%x:%x:%x", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5] ) ) {
  141. for (int ii = 0; ii < 6; ++ii ) {
  142. slaves[SlaveCnt].peer_addr[ii] = (uint8_t) mac[ii];
  143. } // it appears the variable slaves[i].peer_addr = an array of 6 hexidecimal numbers
  144. }
  145. slaves[SlaveCnt].channel = CHANNEL; // pick a channel
  146. slaves[SlaveCnt].encrypt = 0; // no encryption
  147. SlaveCnt++;
  148. }
  149. }
  150. }
  151.  
  152. if (SlaveCnt > 0) {
  153. Serial.print(SlaveCnt); Serial.println(" Slave(s) found, processing..");
  154. } else {
  155. Serial.println("No Slave Found, trying again.");
  156. }
  157.  
  158. // clean up ram
  159. WiFi.scanDelete();
  160. }
  161.  
  162. // Check if the slave is already paired with the master.
  163. // If not, pair the slave with master
  164. void manageSlave() {
  165. if (SlaveCnt > 0) {
  166. for (int i = 0; i < SlaveCnt; i++) {
  167. Serial.print("Processing: ");
  168. 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]
  169. Serial.print((uint8_t) slaves[i].peer_addr[ii], HEX);
  170. if (ii != 5) Serial.print(":");
  171. }
  172. Serial.print(" Status: ");
  173. // check if the peer exists
  174. bool exists = esp_now_is_peer_exist(slaves[i].peer_addr);
  175. if (exists) {
  176. // Slave already paired.
  177. Serial.println("Already Paired");
  178. } else {
  179. // Slave not paired, attempt pair
  180. esp_err_t addStatus = esp_now_add_peer(&slaves[i]);
  181. if (addStatus == ESP_OK) {
  182. // Pair success
  183. Serial.println("Pair success");
  184. //THIS IS WHERE I SHOULD WRITE IT TO EEPROM, and on the SLAVE SIDE I SHOULD DO THE SAME
  185. } else if (addStatus == ESP_ERR_ESPNOW_NOT_INIT) {
  186. // How did we get so far!!
  187. Serial.println("ESPNOW Not Init");
  188. } else if (addStatus == ESP_ERR_ESPNOW_ARG) {
  189. Serial.println("Add Peer - Invalid Argument");
  190. } else if (addStatus == ESP_ERR_ESPNOW_FULL) {
  191. Serial.println("Peer list full");
  192. } else if (addStatus == ESP_ERR_ESPNOW_NO_MEM) {
  193. Serial.println("Out of memory");
  194. } else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
  195. Serial.println("Peer Exists");
  196. } else {
  197. Serial.println("Not sure what happened");
  198. }
  199. //delay(100);
  200. }
  201. }
  202. } else {
  203. // No slave found to process
  204. Serial.println("No Slave found to process");
  205. }
  206. }
  207.  
  208. void sendData(const uint8_t* burnerNumbers, const bool* fire, const uint8_t* igniterStartDelays, const uint8_t* igniterEndDelays,const uint8_t* solenoidEndDelays) {
  209. cmd_struct cmd;
  210. std::copy(burnerNumbers, burnerNumbers+6, cmd.burnerNumbers);
  211. std::copy(fire, fire+6, cmd.fire);
  212. std::copy(igniterStartDelays, igniterStartDelays+6, cmd.igniterStartDelays);
  213. std::copy(igniterEndDelays, igniterEndDelays+6, cmd.igniterEndDelays);
  214. std::copy(solenoidEndDelays, solenoidEndDelays+6, cmd.solenoidEndDelays);
  215.  
  216. //esp_err_t result = esp_now_send(peer_addr, &test, sizeof(test));
  217. for(int i=0; i<SlaveCnt; i++){ //QUESTION: is slaves[i] same as cmd.burnerNumbers[i]
  218. esp_err_t result = esp_now_send(slaves[i].peer_addr, (uint8_t *) &cmd, sizeof(cmd_struct)); //peer_addr replaced with 0 for all
  219. Serial.print("Send Status: ");
  220. if (result == ESP_OK) {
  221. Serial.println("Success");
  222. } else if (result == ESP_ERR_ESPNOW_NOT_INIT) {
  223. Serial.println("ESPNOW not Init.");
  224. } else if (result == ESP_ERR_ESPNOW_ARG) {
  225. Serial.println("Invalid Argument");
  226. } else if (result == ESP_ERR_ESPNOW_INTERNAL) {
  227. Serial.println("Internal Error");
  228. } else if (result == ESP_ERR_ESPNOW_NO_MEM) {
  229. Serial.println("ESP_ERR_ESPNOW_NO_MEM");
  230. } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
  231. Serial.println("Peer not found.");
  232. } else {
  233. Serial.println("Not sure what happened");
  234. }
  235. }
  236. }
  237.  
  238. // callback when data is sent from Master to Slave
  239. void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  240. /*char macStr[18];
  241. snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
  242. mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  243. Serial.print("Last Packet Sent to: "); Serial.println(macStr);
  244. Serial.print("Last Packet Send Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
  245. */
  246. }
  247.  
  248.  
  249. bool wasClicked(int x){
  250. if(!buttons[x].currentState && buttons[x].prevState){
  251. return true;
  252. }
  253. else
  254. return false;
  255. }
  256.  
  257. // callback when data is recv from Master
  258. void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  259. char macStr[18];
  260. payload = (payload_struct*) data;
  261.  
  262. snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
  263. mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  264. // DEBUG
  265. Serial.print("Last Packet Recv from: "); Serial.println(macStr);
  266. }
  267.  
  268. void setup() {
  269. Serial.begin(115200);
  270. delay(1000);
  271. WiFi.mode(WIFI_STA);
  272. Serial.println("ESPNow ***MASTER***");
  273. Serial.print("STA MAC: "); Serial.println(WiFi.macAddress());
  274. Wire.begin();
  275. keyboard.begin();
  276. keyboard.setBacklight(0.5f);
  277. InitESPNow();
  278. esp_now_register_send_cb(OnDataSent);
  279. esp_now_register_recv_cb(OnDataRecv);
  280.  
  281. // DECLARE IO PINS
  282. pinMode(ledPin, OUTPUT);
  283. for(int i=0; i<6; i++)
  284. pinMode(buttons[i].pin, INPUT_PULLDOWN);
  285. //Initial scan
  286. ScanForSlave(); // I need to not do this repeatedly and add a timer
  287. if (SlaveCnt > 0) // check if slave channel is defined
  288. manageSlave(); // THIS TAKES ABOUT 6 SECONDS AND SHOULD NOT BE CALLED IN THE LOOOP UNLESS NECESSARY
  289. Serial.println("Setup complete");
  290. }
  291.  
  292. void loop() {
  293. const int keyCount = keyboard.keyCount();
  294. uint8_t burnerNumbers[6] = {0, 1, 2, 3, 4, 5};
  295. bool fire[6] = {0, 0, 0, 0, 0, 0};
  296. 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
  297. uint8_t igniterEndDelays[6] = {1, 1, 1, 1, 1, 1};
  298. uint8_t solenoidEndDelays[6] = {2, 2, 2, 2, 2, 2,};
  299.  
  300. for(int i=0; i<6; i++){
  301. buttons[i].currentState = digitalRead(buttons[i].pin);
  302. if (buttons[i].currentState == LOW) digitalWrite(ledPin, LOW);
  303. else digitalWrite(ledPin, HIGH);
  304. }
  305. if(millis()-lastTimer > scanTimer){
  306. lastTimer = millis();
  307. ScanForSlave(); // I need to not do this repeatedly and add a timer
  308. if (SlaveCnt > 0) // check if slave channel is defined
  309. manageSlave(); // THIS TAKES ABOUT 6 SECONDS AND SHOULD NOT BE CALLED IN THE LOOOP UNLESS NECESSARY
  310. }
  311. //check who got clicked
  312. for(uint8_t i=0; i<6; i++){
  313. if(wasClicked(i)){
  314. fire[i] = 1;
  315. igniterStartDelays[i] = 50; // MAKE SURE TO MULTIPLY THESE x10 in the SLAVE CODE
  316. igniterEndDelays[i] = 4;
  317. solenoidEndDelays[i] = 5;
  318. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  319. Serial.printf("Button %d was clickedn", i);
  320. }
  321. }
  322.  
  323. for(int i=0; i<6; i++)
  324. buttons[i].prevState = buttons[i].currentState;
  325. if (keyCount != 0){
  326. uint8_t burnerNumbers[6] = {0, 1, 2, 3, 4, 5};
  327. bool fire[6] = {0, 0, 0, 0, 0, 0};
  328. 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
  329. uint8_t igniterEndDelays[6] = {1, 1, 1, 1, 1, 1};
  330. uint8_t solenoidEndDelays[6] = {2, 2, 2, 2, 2, 2,};
  331.  
  332. const BBQ10Keyboard::KeyEvent key = keyboard.keyEvent();
  333.  
  334. if (key.state == BBQ10Keyboard::StatePress){
  335. Serial.print(key.key); Serial.println(" pressed");
  336. switch(key.key){
  337. case 'w': //burner 0, small poof
  338. fire[0] = 1;
  339. igniterStartDelays[0] = 0;
  340. igniterEndDelays[0] = 10; //remember these ge 10x when the client finally receives them
  341. solenoidEndDelays[0] = 10;
  342. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  343. break;
  344. case 's': //burner 0, bigger poof
  345. fire[0] = 1;
  346. igniterStartDelays[0] = 10; //eg 200
  347. igniterEndDelays[0] = 10; //remember these ge 10x when the client finally receives them
  348. solenoidEndDelays[0] = 10;
  349. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  350. break;
  351. case 'z': //burner 0, biggest poof
  352. fire[0] = 1;
  353. igniterStartDelays[0] = 0; //eg 200
  354. igniterEndDelays[0] = 20; //remember these ge 10x when the client finally receives them
  355. solenoidEndDelays[0] = 0;
  356. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  357. break;
  358. /////////////////////////////////////////////////////////////////////////////
  359. case 'e': //burner 1, small poof
  360. fire[1] = 1;
  361. igniterStartDelays[1] = 0;
  362. igniterEndDelays[1] = 10; //remember these ge 10x when the client finally receives them
  363. solenoidEndDelays[1] = 10;
  364. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  365. break;
  366. case 'd': //burner 1, bigger poof
  367. fire[1] = 1;
  368. igniterStartDelays[1] = 10; //eg 200
  369. igniterEndDelays[1] = 10; //remember these ge 10x when the client finally receives them
  370. solenoidEndDelays[1] = 10;
  371. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  372. break;
  373. case 'x': //burner 1, biggest poof
  374. fire[1] = 1;
  375. igniterStartDelays[1] = 0; //eg 200
  376. igniterEndDelays[1] = 20; //remember these ge 10x when the client finally receives them
  377. solenoidEndDelays[1] = 0;
  378. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  379. break;
  380. /////////////////////////////////////////////////////////////////////////////////////
  381. case 'r': //burner 2, small poof
  382. fire[2] = 1;
  383. igniterStartDelays[2] = 0;
  384. igniterEndDelays[2] = 10; //remember these ge 10x when the client finally receives them
  385. solenoidEndDelays[2] = 10;
  386. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  387. break;
  388. case 'f': //burner 2, bigger poof
  389. fire[2] = 1;
  390. igniterStartDelays[2] = 10; //eg 200
  391. igniterEndDelays[2] = 10; //remember these ge 10x when the client finally receives them
  392. solenoidEndDelays[2] = 10;
  393. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  394. break;
  395. case 'c': //burner 2, biggest poof
  396. fire[2] = 1;
  397. igniterStartDelays[2] = 0; //eg 200
  398. igniterEndDelays[2] = 20; //remember these ge 10x when the client finally receives them
  399. solenoidEndDelays[2] = 0;
  400. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  401. Serial.println("w pressed");
  402. break;
  403. /////////////////////////////////////////////////////////////////////////////////////
  404. case 't': //burner 3, small poof
  405. fire[3] = 1;
  406. igniterStartDelays[3] = 0;
  407. igniterEndDelays[3] = 10; //remember these ge 10x when the client finally receives them
  408. solenoidEndDelays[3] = 10;
  409. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  410. break;
  411. case 'g': //burner 3, bigger poof
  412. fire[3] = 1;
  413. igniterStartDelays[3] = 10; //eg 200
  414. igniterEndDelays[3] = 10; //remember these ge 10x when the client finally receives them
  415. solenoidEndDelays[3] = 10;
  416. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  417. break;
  418. case 'v': //burner 3, biggest poof
  419. fire[3] = 1;
  420. igniterStartDelays[3] = 0; //eg 200
  421. igniterEndDelays[3] = 20; //remember these ge 10x when the client finally receives them
  422. solenoidEndDelays[3] = 0;
  423. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  424. break;
  425. //////////////////////////////////////////////////////////////////////////
  426. case 'y': //burner 4, small poof
  427. fire[4] = 1;
  428. igniterStartDelays[4] = 0;
  429. igniterEndDelays[4] = 10; //remember these ge 10x when the client finally receives them
  430. solenoidEndDelays[4] = 10;
  431. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  432. break;
  433. case 'h': //burner 4, bigger poof
  434. fire[4] = 1;
  435. igniterStartDelays[4] = 10; //eg 200
  436. igniterEndDelays[4] = 10; //remember these ge 10x when the client finally receives them
  437. solenoidEndDelays[4] = 10;
  438. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  439. break;
  440. case 'b': //burner 4, biggest poof
  441. fire[4] = 1;
  442. igniterStartDelays[4] = 0; //eg 200
  443. igniterEndDelays[4] = 20; //remember these ge 10x when the client finally receives them
  444. solenoidEndDelays[4] = 0;
  445. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  446. break;
  447. ///////////////////////////////////////////////////////////////////////////
  448. case 'u': //burner 5, small poof
  449. fire[5] = 1;
  450. igniterStartDelays[5] = 0;
  451. igniterEndDelays[5] = 10; //remember these ge 10x when the client finally receives them
  452. solenoidEndDelays[5] = 10;
  453. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  454. break;
  455. case 'j': //burner 5, bigger poof
  456. fire[5] = 1;
  457. igniterStartDelays[5] = 10; //eg 200
  458. igniterEndDelays[5] = 10; //remember these ge 10x when the client finally receives them
  459. solenoidEndDelays[5] = 10;
  460. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  461. break;
  462. case 'n': //burner 5, biggest poof
  463. fire[5] = 1;
  464. igniterStartDelays[5] = 0; //eg 200
  465. igniterEndDelays[5] = 20; //remember these ge 10x when the client finally receives them
  466. solenoidEndDelays[5] = 0;
  467. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  468. break;
  469. //////////////////////// DUAL BURNERS !!!!!////////////////////////////
  470. case 'i': //burner 1 and 2 safe poof
  471. fire[0] = 1;
  472. igniterStartDelays[0] = 0; //eg 200
  473. igniterEndDelays[0] = 20; //remember these ge 10x when the client finally receives them
  474. solenoidEndDelays[0] = 0;
  475. fire[1] = 1;
  476. igniterStartDelays[1] = 0; //eg 200
  477. igniterEndDelays[1] = 20; //remember these ge 10x when the client finally receives them
  478. solenoidEndDelays[1] = 0;
  479. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  480. break;
  481. case 'k': //burner 2 and 3 safe poof
  482. fire[2] = 1;
  483. igniterStartDelays[2] = 0; //eg 200
  484. igniterEndDelays[2] = 20; //remember these ge 10x when the client finally receives them
  485. solenoidEndDelays[2] = 0;
  486. fire[3] = 1;
  487. igniterStartDelays[3] = 0; //eg 200
  488. igniterEndDelays[3] = 20; //remember these ge 10x when the client finally receives them
  489. solenoidEndDelays[3] = 0;
  490. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  491. break;
  492. case 'm': //burner 2 and 3 safe poof
  493. fire[4] = 1;
  494. igniterStartDelays[4] = 0; //eg 200
  495. igniterEndDelays[4] = 20; //remember these ge 10x when the client finally receives them
  496. solenoidEndDelays[4] = 0;
  497. fire[5] = 1;
  498. igniterStartDelays[5] = 0; //eg 200
  499. igniterEndDelays[5] = 20; //remember these ge 10x when the client finally receives them
  500. solenoidEndDelays[5] = 0;
  501. sendData(burnerNumbers, fire, igniterStartDelays, igniterEndDelays, solenoidEndDelays);
  502. break;
  503. }
  504. }
  505. }
  506. }

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


  1. /*
  2. homegreeter slave v6
  3. This is poorly written code shared by Dan from Gears, Code, and Fire
  4. https://youtube.com/@gearscodeandfire
  5. Gigantic credit to Rui Santos of randomnerdtutorials.com
  6. You are advised against reproducing this for the original putpose (shooting fire)
  7. That said, this is simply code for the master chip using the ESPNow library.
  8. Pressing buttons on the recommended keyboard (https://www.tindie.com/products/arturo182/bb-q20-keyboard-with-trackpad-usbi2cpmod/)
  9. controls varying combos of 6 ESPNow slave esp32 chips running two 12V relays and an LED
  10. Originally posted 9/23/2023 to https://www.electromaker.io/
  11. v6: tweaked some logging
  12. */
  13.  
  14. #include <Arduino.h>
  15. #include <esp_now.h>
  16. #include <WiFi.h>
  17.  
  18. #include <iostream>
  19. #include <string>
  20.  
  21. #define CHANNEL 0
  22. #define LEDPIN 2
  23. #define SOLENOIDPIN 4
  24. #define IGNITERPIN 5
  25. #define FIREPIN 33
  26.  
  27. //when below is true, the main loop will execute the firing logic
  28. bool runFire = false;
  29.  
  30. //globals for how long to run the fire routine
  31. int igniterStartDelay;
  32. int igniterEndDelay;
  33. int solenoidEndDelay;
  34.  
  35. //flags for the loop
  36. bool igniterIsOn = false;
  37. bool solenoidIsOn = false;
  38. bool fireStarted = false;
  39. unsigned long startTime;
  40.  
  41. //below is the basic transmission payload that I determine
  42. typedef struct payload_struct {
  43. uint8_t burnerNumbers[6];
  44. bool fire[6];
  45. 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
  46. uint8_t igniterEndDelays[6];
  47. uint8_t solenoidEndDelays[6];
  48. } payload_struct;
  49.  
  50. payload_struct* payload;
  51.  
  52. //array of mac address Strings associated with burners
  53. String burnerMac[6] = {"94:3C:C6:33:99:24",
  54. "08:B6:1F:34:A5:50",
  55. "08:B6:1F:3B:53:1C",
  56. "C8:F0:9E:74:E1:A0",
  57. "08:B6:1F:3D:22:70",
  58. "C8:F0:9E:50:7D:40"};
  59.  
  60.  
  61. // Init ESP Now with fallback
  62. void InitESPNow() {
  63. WiFi.disconnect();
  64. if (esp_now_init() == ESP_OK) {
  65. Serial.println("ESPNow Init Success");
  66. }
  67. else {
  68. Serial.println("ESPNow Init Failed");
  69. // Retry InitESPNow, add a counte and then restart?
  70. // InitESPNow();
  71. // or Simply Restart
  72. ESP.restart();
  73. }
  74. }
  75.  
  76. // config AP SSID
  77. void configDeviceAP() {
  78. String Prefix = "Slave:";
  79. //String Mac = WiFi.macAddress(); // i made this global for easy access
  80. String Mac = WiFi.macAddress();
  81. String SSID = Prefix + Mac;
  82. String Password = "123456789";
  83. bool result = WiFi.softAP(SSID.c_str(), Password.c_str(), CHANNEL, 0);
  84. if (!result) {
  85. Serial.println("AP Config failed.");
  86. } else {
  87. Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
  88. }
  89. }
  90.  
  91. // callback when data is recv from Master
  92. void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  93. char macStr[18];
  94. payload = (payload_struct*) data;
  95.  
  96. snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
  97. mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  98. // DEBUG
  99. //Serial.print("Last Packet Recv from: "); Serial.println(macStr);
  100. //solenoidStart always = 0
  101. //NEW CODE FOR NEW PAYLOAD 5/3/2023
  102. for(int i=0; i<6; i++){
  103. Serial.print("payload->fire["); Serial.print(i); Serial.print("]: "); Serial.println(payload->fire[i]);
  104. Serial.print("WiFi.macAddress()==burnerMac["); Serial.print(i); Serial.print("]: "); Serial.println(WiFi.macAddress()==burnerMac[i]);
  105. if(payload->fire[i] == true && WiFi.macAddress()==burnerMac[i]){
  106. //burner i is instructed to fire, check if burnerMac[i] is this device's mac
  107. //put timers in global variables igniterStartDelay, igniterEndDelay, solenoidEndDelay
  108. igniterStartDelay = payload->igniterStartDelays[i]*10;
  109. igniterEndDelay = payload->igniterEndDelays[i]*10;
  110. solenoidEndDelay = payload->solenoidEndDelays[i]*10;
  111. Serial.print ("Last Packet Recv:n");
  112. Serial.print("Bool Fire: "); Serial.println(payload->fire[i]);
  113. Serial.print("Int burnerNumber: "); Serial.println(payload->burnerNumbers[i]);
  114. runFire = true;
  115. }
  116. }
  117.  
  118. Serial.println("");
  119. }
  120.  
  121.  
  122. void setup() {
  123.  
  124. pinMode(IGNITERPIN, OUTPUT);
  125. digitalWrite(IGNITERPIN, LOW);
  126. pinMode(LEDPIN, OUTPUT);
  127. pinMode(SOLENOIDPIN, OUTPUT);
  128. pinMode(FIREPIN, OUTPUT);
  129. digitalWrite(SOLENOIDPIN, LOW);
  130.  
  131. Serial.begin(115200);
  132. delay(1000);
  133. Serial.println("ESPNow ***SLAVE***");
  134. //Set device in AP mode to begin with
  135. WiFi.mode(WIFI_AP);
  136. // configure device AP mode
  137. configDeviceAP();
  138. // This is the mac address of the Slave in AP Mode
  139. Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
  140. Serial.print("Actual MAC: "); Serial.println(WiFi.macAddress());
  141. // Init ESPNow with a fallback logic
  142. InitESPNow();
  143. esp_now_register_recv_cb(OnDataRecv);
  144.  
  145. Serial.println("Setup complete");
  146. }
  147.  
  148. // the loop is very intentionally blocking code- I don't want to miss any logic to turn the thing off once it's on!
  149. void loop() {
  150. if(runFire){
  151. //noInterrupts();
  152. Serial.println("Fire Started");
  153. digitalWrite(FIREPIN, HIGH); //D33
  154. digitalWrite(LEDPIN, HIGH); //D2
  155.  
  156. digitalWrite(SOLENOIDPIN, HIGH); //D4
  157. Serial.println("Solenoid On");
  158.  
  159. delay(igniterStartDelay);
  160. digitalWrite(IGNITERPIN, HIGH); //D5
  161.  
  162. Serial.println("Igniter On");
  163.  
  164. delay(igniterEndDelay);
  165. digitalWrite(IGNITERPIN, LOW);
  166. Serial.println("Igniter Off");
  167.  
  168. delay(solenoidEndDelay);
  169. digitalWrite(SOLENOIDPIN, LOW);
  170. Serial.println("Solenoid Off");
  171.  
  172. runFire = false; //reset all the flags to off
  173. digitalWrite(FIREPIN, LOW); //D33
  174. digitalWrite(LEDPIN, LOW); //D2
  175. Serial.println("Fire Done");
  176. }
  177. }

And lastly, the video, which breaks things down into a fair amount of detail.



Schematics, diagrams and documents

BOM

Just for completeness sake

Schematic

json format

Board layout

I THINK this might be needed to upload to PCBWay or JPLCB

CAD, enclosures and custom parts

Sleeve fit solenoid attachment

Cone with aerator and electrode holes

Cannister housing for high voltage generator

Make sure this is on the OPPOSITE side of the esp32, or it is more likely to interfere with it.

Part of the cone

Propane cannister bottom with pcb housing

Half of the lower part of the light cone

I split this up to print it easier. It can be in PLA as it's pretty far from the flame.

Screw-in solenoid attachment

Screws into solenoid with (i think) M4 bolts

Code

esp32 slave code

I prefer "voluntary helper"

esp32 master code

Credits

Photo of Dan

Dan

11-year-old boy in a man's body pretending to be a 25-year-old Youtuber.

   

Leave your feedback...