Showcase §
Probably cooler if I show the final product here before talking about how I made it.
That's Dad in the pic btw

The image above was the first version of the project. It had 4 outlets. Each outlet had a constant flow of gas which was ignited as a pilot light. Each of them also had a solenoid controlled auxiliary line that would allow more gas to flow into the outlet section. Controlling these solenoids would make big flames come out the top.
This particular version had a lower pressure gas regulator than later versions, and the solenoids were hard wired to a bunch of buttons on a breadboard.
Later on I moved to a larger gas regulator and controlled the solenoids with the map files from BeatSaber, as shown below
Below on the left is a Beatsaber generated song, and the right is manual control with a Guitar Hero guitar
Both of these below are Guitar Hero control only
Inspiration §
Back when I had more time on my hands, I came across this awesome video
And I thought that was super cool, and luckily dad thought it was cool enough to help out.
This was my first proper project that went bigger than a few scripts.
The plan §
I was only 15 and had no idea what I was doing. Software and electronics wise I could figure it out, but I couldn't weld or do anything with gas. So, thank you Dad for doing all the welding and making sure we didn't explode 👍.
But I knew where to start with the control. I had used an Arduino before which was a start.
I decided I was going to have control over the flames with some buttons and a Guitar Hero guitar like I'd seen in the inspo video. But I had also just discovered the world of virtual reality and a game called Beatsaber. For some context, this is Beatsaber below

It's virtual reality Fruit Ninja Guitar Hero. The blocks come flying at you in sync with a song, and you chop them up in the right direction. These blocks would become instructions for the solenoids later on ...
Version 1 - Manual control §
In conjunction with the team pyrotechnical engineer (Dad), I was advised that controlling gas was done with a solenoid, and that the propane technician (Dad) would pipe up the main propane feed into one side of the solenoid, I would turn it on, and there would be flames out the top. Cool.
After I had confidently selected some $10 gas solenoids off of wish.com to control the whole thing, I ventured into the realm of relay control
I was just using the generic Raspberry Pi GPIO ports to control each channel individually. Note that I had only ever used the basic Arduino breakout kits with jumpers. So, naive me went and wired it up like a toy.
And then I set it all up outside with a breadboard and some buttons.

Then it was time to turn to more fancy control methods
Guitar Hero Controller §
For those who haven't played Guitar Hero before, there are 5 buttons on the top of the Guitar neck which are pressed while you play, and the signals are sent to a USB dongle which is normally plugged into a console.

I was lucky enough to find a library that let me poll the state of those buttons right in my Python script. I would add the code if I found it, but I think it has been lost to the .gitignore aether.
Regardless, here's a video of me using the controller to actuate the solenoids.
Beatsaber §
Now, this was probably my favourite.
Remembering back to the gameplay of a Guitar Hero Fruit Ninja game

Each of the blocks in a map are stored internally with a timestamp, row and column value inside a .dat file
{
"_time": 173,
"_BPMChanges": [...],
"_events": [...],
"_notes": [
...
{
"_time": 9.875, // time relative to bpm
"_lineIndex": 2, // column
"_lineLayer": 0, // row
"_type": 0,
"_cutDirection": 1
},
...
],
"_obstacles": [],
"_bookmarks": []
}
If we only consider the _time and _lineIndex (column) value, we can convert this data into a set of timestamped bursts for 4 different flames.

So I made a script that would go through all the _notes in a game file and extract the time and column value into a massive list like this
[
[9.875, 0, 0, 1, 1], # time, flame1, flame2, flame3, flame4
[11, 1, 1, 0, 0],
[12, 0, 0, 1, 1],
[12.938, 1, 0, 1, 0],
...
]
In the first list of the 2D list above [9.875, 0, 0, 1, 1], the 9.875 is the time the block appears, and the set of 4 numbers after are the boolean values for the relays. The 0, 0, 1, 1 means that the 2 most left relays remain off whilst the 2 far right ones are on.
Then, it's just a matter of starting a software stopwatch and triggering a burst of flames each time a block is detected
b = datetime.datetime.now()
c = (b - a)
stp = "{:.3f}".format(c.seconds + c.microseconds * 1e-6)
stopwatch = float(stp)*(_bpm/60)
Note that stopwatch is relative to the BPM by multiplying the seconds (1.123) by the BPM scale (_bpm / 60).
In fact, looking back at this code in particular, I think it might have a few issues.
Struggle §
I would also like to note that I was so incredibly bad at coding that I re-invented the continue keyword by accident. I think this took me like 1 hour of debugging. Clearly my first experience with it's use case
Before learning about continue
for m in time_hold:
holder_list[0]=round(notes[m]['_time'],3)
_count=1
i_finished = False
if notes[m]['_lineIndex'] == 0 and channel_1_set == False:
holder_list[1] = 1
i_finished = True
channel_1_set = True
elif channel_1_set == False:holder_list[1] = 0
if notes[m]['_lineIndex'] == 1 and i_finished == False and channel_2_set == False:
holder_list[2] = 1
i_finished = True
channel_2_set = True
elif i_finished == True and channel_2_set == False:holder_list[2] = 0
if notes[m]['_lineIndex'] == 2 and i_finished == False and channel_3_set == False:
holder_list[3] = 1
i_finished = True
channel_3_set = True
elif i_finished == True and channel_3_set == False:holder_list[3] = 0
if notes[m]['_lineIndex'] == 3 and i_finished == False and channel_4_set == False:
holder_list[4] = 1
i_finished = True
channel_4_set = True
elif i_finished == True and channel_4_set == False:holder_list[4] = 0
form.append(holder_list)
channel_1_set = False
channel_2_set = False
channel_3_set = False
channel_4_set = False
time_hold = []
time_hold.append(i)
After learning about continue
for m in time_hold:
holder_list[0]=round(notes[m]['_time'],3)
if notes[m]['_lineIndex'] == 0:
holder_list.extend([1,0,0,0])
continue
if notes[m]['_lineIndex'] == 1:
holder_list.extend([0,1,0,0])
continue
if notes[m]['_lineIndex'] == 2:
holder_list.extend([0,0,1,0])
continue
if notes[m]['_lineIndex'] == 3:
holder_list.extend([0,0,0,1])
form.append(holder_list)
time_hold = []
time_hold.append(i)
The codebase obviously wasn't perfect, but in the end it did everything I wanted it to do with no issues.
BPM RGB coloring §
At the time of this project I was obsessed with ansi colors in the terminal (I still think they're cool). I decided that I wanted to color the BPM of the song to represent it's pace.

Obviously a song at 60 BPM is tame, and a song at 300 is bonkers. I set some basic thresholds for color ranges and began playing with the Google color picker. From there, I made a Desmos with some outrageous ramps to pick individual Red, Green and Blue values based on the BPM (x axis).

I ended up asking my Math teachers (shout-out to Mr Y and Miss H) how I could connect the threshold values of 0, 110, 185 and 255 to intersect with the function. This alone taught me about function translation, scaling and piecewise notation.
It looks cool (although it's over complicated) as a piecewise too
Final words §
I am so glad I had Dad to help me with this. This kinda started my love for programming and electronics in general.
If anyone reading this wants to help build a new new one, please message me :)