How the Legend of the Dragon Cache Came to Be
Geocaching is a hobby of mine. It is a global treasure hunting game where participants use GPS coordinates to locate hidden containers called "geocaches" hidden all around the world. Players navigate to the specified coordinates and then try to find the geocache hidden at that location, often containing small trinkets and a logbook to sign. It gets me to go outside, and I have tons of fun finding others' Geocaches.
However, one of the things I've always wanted is to hide a Geocache of my own (while putting me own spin on it, of course). I was inspired by a "Simon Says" geocache I came across a while ago, which unfortunately is no longer available at the time of writing. To open this Geocache, you need to bring 4 AA batteries and play a game of Simon Says. When you win, a small locker opens up containing the logbook.
I wanted to incorporate electronics in a geocache of my own as well, with the final idea being: The Legend of the Dragon Cache. It is a geocache in the form of a locker. By bringing a power bank and connecting your device (e.g. a smart phone), you are able to play a game that, once won, will cause the locker to unlock. To win, you need to convince a dragon to show their treasure. They will only do so if they deem you worthy. Solve the puzzle they gave you and grab your chance to take a peek.
Hardware
My geocache has got a few requirements, namely:
- Run off a power bank (± 5 watts at most).
- Some way to connect to the user's device (e.g. Wi-Fi or Bluetooth).
- The game must run on lower-end hardware such as smart phones.
- Be able to run in the web browser (this is a requirement set by Geocaching.com).
- A lock that can be electronically controlled.
After a bunch of brainstorming, the hardware we eventually ended up using and that allowed us to meet the requirements consists of the following main components:
- ESP32 This is the brains of the operation. This micro-controller runs our code and comes with integrated Wi-Fi hardware.
- USB-C DIP Adapters There are two of these to provide redundancy and compatibility, allowing two cables to be attached with each a different plug on the other hand (USB-C and USB-A respectively). Using these adapters in combination with a few 5.1 kΩ resistors, we can configure them as a 5V input. This is what will provide power to all the components.
- SD card adapter Allows the ESP32 to interface with an SD card, which stores the game's files and other data.
- Step up Responsible for converting 5 V to 12 V power, required by the relay and the solenoid lock.
- Solenoid lock A kind of lock that uses an electromagnet to open and close. The lock opens when the electromagnet is powered. It requires 12 V power from the step up. An importent thing to consider is that these locks act like inductors, meaning that CEMF occurs.
- Relay Using a relay, we can use a 3.3V signal from the ESP32 to control when 12V power gets supplied to the solenoid lock. Relays are also able to handle CEMF and do not need a flyback diode.
The components were then wired up according to the following diagram.
Thanks to a friend of mine, Filip, we were also able to get a custom PCB made! This makes the hardware more reliable and much easier to repair and replace.
He let me know that he is willing to share the files with anyone who wants to make their own design. As such, you can contact me to request the files if you feel they would be useful to you.
Locker
With a lot of help of my father, we were able to design and make a locker to go together with the hardware. It all started off with a bunch of sketches of what the lock could possibly look like, and picked a design based on factors such as sturdiness, water-proofing and ease-of-use. The locker would be made out of thick plastic sheets welded together.
The final design ended up consisting of two doors. The upper door is openable at all time and reveals a compartment for the user to insert their powerbank. The bigger lower door opens up to reveal the bigger compartment which will contain the logbook and other trinkets Geocachers left. It also reveals a small compartment housing the electronics, which is closed off using screws. A roof was added to help keep rain away from the door seams.
For the size, we used an A4 paper as reference. Essentially, the locker should be able to fit an A4 size sheet of paper without bending or folding. This also means the locker would end up quite large. I hope the large size encourages people to bring bigger tradeables and Travel Bugs whenever they go out finding this cache.
The first step in the process is to cut the plastic sheets to size. After carefully measuring everything, we cut them to size using a table saw.
After the pieces were cut, it was time to weld them together! By heating up the seams of two neighboring sheets, it is possible to make them stick together.
After welding, the bond is made more permanent and water resistant by extruding a thin strip of plastic onto the welded seams. Because the plastic is extruded at a high temperature, it will melt onto sheets it comes into contact with.
After putting everything together, and adding finishing touches such as door knobs and hinges, it was ready!
Game Engine
I needed an engine that would allow me to build a game involving a simple story and a puzzle for the player to solve that could run in a web browser on lower-end devices such as phones. Many traditional engines like Unity, Unreal Engine and Godot are complex and have incomplete support for the web, so I had to look elsewhere.
When coming up with the idea for this game, I was very much inspired by the Professor Layton games on the DS. In these games, you slowly unravel an overarching mystery by solving brain teasers. The game presented itself somewhat like a visual novel broken up by puzzles, which gave me an idea: are there perhaps any game engines specifically for visual novels out there?
Doki Doki Literature Club is a visual novel game that went viral due to its psychological horror elements despite appearing as a light-hearted dating sim. The popularity of this game also put the games engine in the spotlight, Ren'Py. To my surprise, it is an open-source game engine aimed at visual novels that is easy to get started with and can be exported to web -- fantastic! Totally not something I expected from an engine that makes use of Python instead of JavaScript.
RenPy games are able to run in the browser thanks to WebAssembly, also referred to as Wasm. It allows web browsers to run highly efficient assembly code in the web browser. In turn, this also allows web browsers to run Python code to some extent. Though Wasm is relatively new, all modern browsers have included support for it by now. Some unexpected upsides are also that image and audio formats that might be unsupported by the browser can be run by the browser, increasing compatibility.
With that, I got started, and I think I really struck gold with my choice of engine. It offered everything I was looking for and came with most things I needed out of the box. Its scripting language was easy to pick up, and in no time I coded up a working game. If anything, I spent more time on finding images, music and sound effects for my game!
Troubleshooting
Unfortunately, it wasn't all smooth sailing. During development, there were a few setbacks we had to deal with. In this section, we will go over what these were and how we overcame them.
Long loading times
The game took an unacceptable amount of time to load in web browsers. By running the game on more capable web server from the same SD card, we determined that neither the client nor the SD card is a limiting factor. This leaves us with the following things that are the crux of the issue:
- Web server The web server software/library is responsible for serving files to the client. An alternative web server might provide better performance.
- Wi-Fi Transfer Speeds The ESP32 comes with relatively simple 2.4 Ghz Wi-Fi hardware. The maximum throughput for TCP (which is the protocol used by web servers) is 20 Mbps, which equals 2.5 MB/s. When it comes to Wi-Fi hardware, the real life speeds will usually be much lower.
- ESP32 CPU/Memory Limitations Generally, when transferring files from the SD card to the client, the ESP32 first needs to copy a chunk of that file into its memory before it can be transferred. This acts can as a bottleneck.
The way I went about solving this, is my compressing the game's assets. Originally, the game was built with raw, uncompressed assets. That is to say, assets like images and music were stored in high quality, lossless formats which take up a lot of space and also need to be transferred over when the game is run. By compressing these files, we might be able to make the game size a lot smaller. First, a quick overview of image and audio file formats, which is what my compression effort was mainly focused on.
- JPEG A lossy image format that is able to make images considerably smaller by removing image data, at the cost of introducing artifacts into the image.
- PNG A lossless image format that compresses images without losing any information, similar to a ZIP file. As a consequence, images tend to be quite large.
- WAV A lossless audio format without any compression. It stores audio as-is.
- FLAC A lossless audio format that compresses audio without losing any information, similar to ZIP files. Audio will sound exactly the same in FLAC format as WAV format, despite the file being smaller in size.
- MP3 A lossy audio format that is able to make audio considerably smaller by removing audio data, at the cost of introducing artifacts into the audio.
These were the kinds of files that were up to then used in the project, but none of them are ideal when saving space is of high importance. In the case of JPEG and MP3, these formats do not provide the best compression possible and have been superseded by newer formats. In the case of PNG, WAV and FLAC, these formats store data in a lossless way. This is great when you need to keep a master of the file, but it is overkill for the purposes of a simple game. What we need, is to convert these assets to a modern file format and determining reasonable compression settings for them. Some of the criterea I was looking for were compatibility with RenPy, achieving maximum efficiency and availability of good compression tools, and these are the file formats I ended up choosing.
- WebP An open, lossy image format with a compression efficiency greater than JPEG. This allows us to make image files smaller without sacrificing much on fidelity.
- Opus An open, lossy audio format with a compression efficiency greater than MP3. This allows us to make audio files smaller without sacrificing much on fidelity.
The problem I found with WebP compression, is that compression settings aren't universal. Some images might still look great with heavier compression, whereas others show significant artifacting. As such, I went over each image manually using Squoosh, a tool that allows you to compare images before and after compression. This way, I was able to keep all my images to a certain standard of quality. The end result is that one can only really tell the images were compressed if compared side-by-side with the original.
Opus compression, meanwhile, was much easier to work with. I found that a bitrate of 64 kbps was the sweet spot I could use to convert and compress all my audio files. While you can definitely hear the loss of quality compared to the original, it is not really noticeable when listening to the files on their own. We should also consider that most people won't be listening to the music using high quality headphones either.
The final result of all my compression artifacts is that the game was now only 20% the size of the original, causing the loading times of the game to be halved — a great result. I still wish the loading times would be shorter, but achieving any savings from this point forward will be more complicated. The remaining data that needs to be downloaded when the game is loaded mostly consists of the game engine itself now. There is nothing I can do to decrease the size of the engine unfortunately, so any speed improvements would now have to come from increased bandwidth rather than reducing the payload.
A thing we could still try, is switching web servers. Originally, I used ESPAsyncWebserver for it's asynchronous design. However, as a proof of concept, I did try switching from ESPAsyncWebserver to "WebServer", which is one of the default libraries that comes with the ESP32. Unfortunately, the performance was still roughly the same, meaning that there is most likely not much performance gains to be had from optimising the web server. I did, however, end up sticking with the WebServer library for a different reason, which I will mention later.
The last thing to do, would be to pick different hardware for the project that overcomes the CPU, memory and Wi-Fi bandwidth limitations of the ESP32. This, however, is very impractical, as the entire project had been designed around the ESP32. Furthermore, picking different hardware would most likely also increase costs and power usage, which may cause more trouble than it'd worth.
Crashes under load
While testing, we noticed that the watchdog for ESPAsyncWebserver would trigger and cause the ESP32 to restart, dropping the Wi-Fi connection with the client's device. As a test, I recompiled the library with the watchdog disabled. While it was slightly harder for the ESP32 to crash, when it did, it would freeze the device instead of restarting it.
It turned out that the instability stems from the ESP32 and/or the web server library not being able to handle the requests in time. What was especially troublesome, is if many requests were made at once. For example, when the client requested to download multiple assets simultaneously or in quick succession, a crash would occur.
The way a Ren'Py game is served to the client, is by first serving the game engine to the device in the form of WebAssemvly. This comes in the form of one large continuous file. Once the game engine has been downloaded, the games files are served to the client in the form of a ZIP file. This is also one large continuous file. Where the trouble would start, is in-game. Due to progressive downloading, the client would sometimes send out many requests all at once.
Ren'Py has support for progressive downloading, where assets such as images and audio are dynamically requested from the server as they are needed, rather than being all loaded in at the start of a game. By default, all assets that support progressive downloading are being loaded in this way. However, when limitations to the bandwidth and the amount of requests comes into play, this can deteriorate the experience. Though it may decrease loading times when loading the game initially, having too many assets being downloaded progressively at once can cause images to appear pixelated, audio not being played or -- indeed -- a crash. To solve this issue, there need to manually determine which assets should be loaded in from the start and which may be progressively downloaded. In general, the assets for which progressive downloading should be disabled are:
- Images (e.g. backgrounds) that appear in quick succession. For example, there is one scene in the game where the player is transported into a different world, and images shown in quick succession are meant to represent the transition.
- Character sprites, as by their nature, they can change rapidly during dialogue due to changing facial expressions.
- Audio that needs to be played in a timely matter. If progressive downloading is enabled, the playback would be delayed until the audio has been fully downloaded.
With these measures, we do not necessarily decrease loading times of the game, but we do decrease hiccups during gameplay.
The second thing I took a look at is switching the web server software. As part of trying to improve the performance, I already made the switch from ESPAsyncWebserver to WebServer. The reliability was found to be much improved. Previously, a crash would still occur when the user would try to overload the page by spamming refreshes. However, with WebServer, this now only causes a delay at most.
Solenoid lock power usage
The component that, by bar, uses the most amount of power is the solenoid lock. At least, when the its electro magnet is powered, which in turn opens up the lock. For the project, we set a limit of 5 watts as our peak power usage, as the project should be able to be powered by USB powerbank. After careful testing, the solenoid lock we had purchased for this project was just barely able to scrape by.
However, though it may not require so much power that it would cross our 5 watts threshold, that doesn't mean it won't if it can. The electric magnet inside the lock is usually a little more powerful than what is actually required to open the lock, and if it is able to take that extra amount of power, it will.
Now, this is not a huge issue. Any proper power banks limit the amount of current so that they never exceed the output they are rated for. After all, you don't want a 5 W powerbank to explode if you attach a phone capable of 10 W charging to it. What isn't being regulated however, is how that power is divided up over all of our components.
In an ideal scenario, in the case where the components need more power than is available to them (which in practise only happens while the solenoid lock gets unlocked), the solenoid lock should be de-prioritised over all the other components. The solenoid lock is still able to operate with less power supplied to it then its maximum, whereas a component such as the ESP32 would fail if it did not get the amount of power it is asking for. Unfortunately, in practise, that solenoid lock happily slurps up all that juicy power, leaving too little for the ESP32 too operate. The ESP32 crashes, and gets stuck in a boot loop because it does not have enough power to fully start up, causing it to crash again.
We tried two different approaches to solve the problem.
- Using a capacitor. Capacitors are able to provide power for a short amount of time. Seeing as the lock will only be open for short amounts of time at once, a capacitor might be able to provide just enough power to keep it operating at its maximum rated power.
- Software-based We can architect the code in such a way that it can handle a crash caused by the solenoid lock. This does not solve the root problem, which is hardware-related, but it means this flaw will not cause any issues for the end-user.
Trying a multitude of capacitors, we found that all the commonly available capacitors were not able to provide an adequate amount of power to keep the ESP32 from crashing. This means that the next thing to try would be to use a rechargeable battery, but that would add a lot of hardware complexity to the projects. As such, we went with the software-based solution.
As it turns out, the ESP32 does not simply crash when the lock is opened. It will only do that when, specifically, Wi-Fi is enabled. Maintaining an active Wi-Fi connection actually uses a lot of power. Without the radio on, the ESP32 uses anywhere between 2 and 50 mA. When Wi-Fi is turned on, this jumps up to somewhere between 95 and 240 mA. That is roughly 7 times as much! The solution that ended up working for us, was to simply disable the ESP32's radio as the lock is being opened. In case a crash does nonetheless occur, it will delay turning on the radio and close the lock during the reboot.
This solution should be mostly transparent to the end-user. Opening the lock occurs at the very end of the game, at which point the game would already be fully cached in the web browser. This means that, even when the Wi-Fi disconnects, the game will continue normally even if the device will not automatically reconnect to the Wi-Fi after the lock closes and the radio gets enabled again.
Closing thoughts
This was an incredibly fun project to work on, and I am very happy how things turned out. It certainly wasn't a completely smooth ride, but seeing the end result fills me with a lot of gratitude. It's awesome to see an idea you have had stuck in your head for so long turn into something physical. Thank you to everyone who has helped me out, without you we would have never gotten here! 💙