Bindicator
Contents |
---|
- Building for Others - Hardware - Software - Working on Battery - WiFi Setup - Configuration - Web UI - Bin Day Grabber - Keeping Time - Tracking Bindication - Deployment - The Grand Reveal |
I’ve received this meme from multiple people in my life.

I didn’t make one for myself, since living in an apartment I’ve lived a blissful life of not needing to put bins out. A friend requested help putting one together, perhaps as a favour so I’d have an excuse to do it.
Building for others⌗
For most of my projects I have the privilege of being the only user. I can hardcode my wifi config, set the exact behaviour that I want. If I want anything changed, I can just reflash the device myself easy peasy.

However since this is for a friend, I needed to set things up much more generically and get it closer to a product. It would be easy to program a LED to turn on once a week and call it done, but I wanted the option of setting the WiFi and what address to check bin day for. Unfortunately I couldn’t take this all the way to something that can work for everyone; my implementation only works for the local government website that my friend lives in. Each area has a totally different website/API to get bin collection info. It seems one chap has gone through the effort to fully productise this concept, which they sell closed source and honestly fair enough.
So instead of posting the complete source code that will only work in one suburb in the world, I thought I’d run through the libraries and tricks I used to make it work that you might find helpful in your own projects.
Hardware⌗
I basically decided to copy some prior art, and ordered a set of hardware for myself and shipped a set to my friend:
- ESP32 S3 SuperMini
- 2x little Neopixel boards
- Lipo battery
Basic info for the S3 SuperMini is all over the place, but I eventually discovered this page that includes full schematics (Don’t click on english, it’ll 404. Learn Chinese instead). The charge controller is a nice touch, and it even has an onboard Neopixel LED I can use for testing.

Since it only required connecting off the shelf modules being wired together, it is simple to make and still small enough to fit in the bin shell. The battery remains an optional nice to have, I don’t know how well it’ll fit in the bin without interfering with the light diffusion.
For the 3D print, I directed my friend to this existing design by JayWll. The S3 SuperMini has the same footprint as the C3 zero this was designed for. I didn’t realise it needed to be stated, but printing it in plain white PLA is fine. My mate went and bought special transparent PETG thinking that’s the only way the light would shine through.

Software⌗
Whilst tempted to try MicroPython, I stuck with good ol’ Arduino IDE for this project. There are just so many fantastic libraries that I already knew I wanted to use. The basic features I wanted:
- Ability to configure the WiFi (In case they change their router/SSID/passsword one day)
- Ability to set the address (In case they move one day)
- A touch button that turns on a web ui configuration page
And of course the real feature:
- Read the bin day from the government website directly
- Turn on with a suitable colour LED the day before
- Once you’ve put the bins out, a touch of the button turns off the light
The rest of this blog goes through the software details, as it was by far the more involved.
Working on battery⌗
Since the bindicator will spend most of it’s life sitting idle, and the S3 SuperMini board I was using had a built-in battery charger, I figured attempting to run on battery was worth a shot. This battery consideration affected how every feature operated as you’ll see in the rest of this blog.
Key to this is leveraging the ESP’s deep sleep. Unlike some other microprocessors I’ve worked with, the ESP series seems to basically reboot when it wakes from deep sleep. That or my app was crashing on wakeup and restarting, hard to say. Going to sleep is as simple as:
esp_deep_sleep_start();
lol jks it’s slightly more complicated. First you set how you’d like the ESP to wake up later, either a timer, GPIO input, or even a capacitive touch event. To keep variables between sleeps/reboots, I used the trusty Preferences.h
.
There are different levels of sleep, with different limitations and considerations. Not to mention the concept of reducing the WiFi power or turning it off. All complications that I didn’t really consider too deeply, since even basic deep sleep should be getting me down to ~ 100µA. The Neopixels (WS2812B) aren’t great, soaking up ~15-20mA per colour per LED when at full bright, and even when set to 0 draw ~0.5mA per chip. For our 6 LED setup, that’s 360mA for full on, 3mA full off. A 500mAh lipo would be getting a little over an hour with the LEDs on, and just under a week when off. Will see how it goes long term once my friend is using it, but needing a recharge every week would be pathetic.
The S3 SuperMini has a pair of voltage dividing resistors to let you measure the USB supply voltage (VBUS
) on GPIO3. This is cool, but personally I’m more interested in the battery’s voltage to guess charge level with. Thanks to the schottky protection diode between VBUS
and BAT
, there’s a voltage drop that limits the accuracy of guessing the battery voltage.

I played with guessing a fixed drop, but it can be wrong by ~0.2V since the exact voltage drop depends on the current and temperature. Might give you a very rough “The battery is getting flat” warning at least.
WiFi Setup⌗
I used MycilaESPConnect to make it easy to configure the WiFi after flashing. It’ll create an AP and captive portal that lets you specify the SSID and password. Then every boot it’ll connect to the configured WiFi. It’s also handy since we can recycle the AsyncWebServer to host our own separate web ui once connected. It also comes with mDNS, so once on the same network, the web ui can be reached at http://bindicator.local/1. No need for static IP addresses.
I had some small edits that I wanted to make to the portal page, which is created with Node.js and svelte. They have everything you need to build the page from the svelte sources. Personally I found it easier instead to use CyberChef to decode and decompress the ESPCONNECT_HTML
in espconnect_webpage.h
. ‘From Decimal’ (Comma delimiter) -> ‘Gunzip’ to decode, reverse to encode again.
Configuration⌗
Once the Wifi is sorted, I still wanted a web UI to view and set some settings2:
- Address to get the bin day info for.
- What colour each type of bin should be
- What time to light up the indicator3
For this instead of using a framework or real tools, I’m a neanderthal that writes plain HTML and javascript. Everything jammed into a single HTML file, I copied MycilaESPConnect’s trick and store it as an integer array in the header file. Staying in PROGMEM
, the ESP just needs to send it straight over as is.
server.on("/", HTTP_GET, [&](AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", SETTINGS_HTML, SETTINGS_HTML_SIZE);
response->addHeader("Content-Encoding", "gzip");
return request->send(response);
}).setFilter([](__unused AsyncWebServerRequest *request) {
return espConnect.getState() != Mycila::ESPConnect::State::PORTAL_STARTED;
});
It needs the .setFilter
in there so that it serves our bindicator config page only when it’s connected to the configured WiFi.
I decided to go with a static HTML page, and use basic JS to download a settings.json
to fill the page with after loading. This lets me limit the ESP’s code to three endpoints;
/
: static HTML with JS and CSS jammed into a single file./getsettings.json
: Return all of the bindicator settings in JSON withArduinoJson.h
/setsettings
: POST to receive all of the settings in the same format and update the ESP’s variables withPreferences.h
.
Since the bindicator is in deep sleep most of the time, entering config mode requires tapping the capacitive button to wake it up. It will connected to the WiFi and serve the config web UI on http://bindicator.local/. After 5 minutes of inactivity, it’ll go back to sleep.
Web UI⌗
When making little webapps, I like to use super basic HTML with a CSS file to spruce it up. I recently came across pruger’s tiny-brutalism-css and fell in love. It’s stoic beauty felt fitting for a rubbish bin app.

I found it important to allow full control of the bin colours, since the neopixels have really poor colour accuracy compared to sRGB. Instead of doing any sort of calibration, I just show the colour on the LEDs for a few seconds after each adjustment. This also indirectly lets you set the brightness of the LEDs. Again, since this isn’t a real product, I can at least make assumptions about the type, names and number of bins that get collected.

Bin Day Grabber⌗
Both my friend and I were really keen to actually get the bin day information from the government website directly. Lucky for us, the website had a page that let you enter your address and up pops your next bin day and type. Under the hood this used Google Maps API to autocomplete the address, (limited to the local area), and their own API that takes street_number
, street_name
and suburb_name
. Using Google maps Places API makes sense, since it can take any address format and give you the components neatly split up in JSON.
I replicated this system to make the API request and save the result. The local government website was smart and limited the google maps API key to their domain. So I got a bit cheeky, and actually make the requests on the ESP with HTTPClient.h
. That way I could forge the Referer
and Origin
headers so that Google accepted the borrowed API key. This is yet another case of “Fine for a personal toy project, problematic for a product”. This chicanery ruined my neat endpoints as I now needed to replicate each of the google API calls too.4
Once configured, I can save the number, street and suburb with Preferences.h
, and getting the bin day once a week to save as a variable in memory, since it might change. Ironically, I found out that this mob don’t ever change bin day. No matter if it’s a public holiday or disaster strikes, the bins are picked up on the same day every week. It makes you question the need for an API checking bindicator, but my friend is apparently that much of an idiot.
Keeping Time⌗
We can get accurate time with NTP:
#include "time.h"
const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 0; //time zone offset in seconds
const int daylightOffset_sec = 0;
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
This is fine until the ESP goes to sleep. Then it turns off the accurate internal clock and starts to drift roughly 1-10 minutes per day. I figured I really only care about what day of the week it is, so I can live with being wrong by an ~hour. Once a week I turn the WiFi on and do another configTime()
, and double check the council’s bin day API while I’m at it.5
I simply use strftime()
to get what day of the week it is to know if it’s bin day:
char dayBuffer[10];
struct tm timeinfo;
strftime(dayBuffer, sizeof(dayBuffer), "%A", &timeinfo);
String currentDay = String(dayBuffer);
if (currentDay == BIN_DAY) {
Wait that won’t work 🤔⌗
I’ll be honest, I messed up. I’m an adult, I thought I knew how bin collection works. Still I had ‘bin day’ in my mind and originally made the light turn on when it was that day. How could I forget that the whole point is to remind you the night before so that the bins are out when they get collected at 6am. To cover for this, I utterly abused time zones. The bindicator lives 24 hours in the future, so I can still do the simple weekday string comparison.
//Set time zone to be 24 hours (86400 seconds) in the future.
//Needs to be negative when using setTimeZone, positive when using configTime 🤷
setTimeZone(-86400, 0);
I was pretty unfamiliar with time.h
, it’s based on the C++ standard library so really feature packed but was unclear how best to use. It took me a while to realise that configTime()
will actually trigger a NTP sync, while setTimeZone()
will setup the time without the need for WiFi/NTP. That and the gmtOffset_sec
needs to be negative for setTimeZone()
.
Tracking Bindication⌗
With the ESP waking up from deep sleep once an hour to check the time, I used a super simple state machine to keep track of what was happening in terms of bindicating:
enum NotificationState {
NOTBINDAY,
PREWAKEUP, //It is bin day but not time to light up yet
LIT, //The LEDs have been lit up
SNOOZED //The button was pressed, can turn off the LEDs
};
State transitions are mostly from one to the next in order, except for LIT -> NOTBINDAY
which happens if the button is never pressed but we still need to turn the LEDs off. A simple LEDsOn
boolean wasn’t enough to store this kind of logic I wanted.
I perhaps should expand the states or use the technique more. For example the setup()
function required some consideration, since it’ll be running in a lot of scenarios:
- Initial boot so WiFi needs setup with
MycilaESPConnect
. - Wakeup to check day/time and turn on LEDs.
- Wakeup button pressed, need to run the config web server.
- Wakeup button pressed when the LEDs were on, need turn them off and go straight back to sleep.
- Once a week, connect to WiFi to update clock and bin day information.
At first simple checks like ‘Is there a WiFi config saved?’ seemed suitable to make a decision, but as the number of boot scenarios grew, it was approaching unreasonable. More reason to never publish the source 😜
Deployment⌗
To save my friend the struggle of installing Arduino IDE, all the libraries, and configuring the flasher. I figured how to easily send him the firmware.
Turning on verbose in Arduino IDE preferences will make it print out where the sketch was compiled to, probably a rando folder like this:
/Library/Caches/arduino/sketches/AE499ACFA7BC6050925DA8C3CA3F0D3A/
In that folder there’s a bindicator.ino.merged.bin
that is the entire 4MB ROM that can be flashed to the ESP. A copy of esptool.exe is all you need to flash it. I put the following command in a bat file to make it easy:
.\esptool.exe --before default_reset --after hard_reset write_flash 0x0 .\bindicator.ino.merged.bin
Zip up esptool.exe
, bindicator.ino.merged.bin
, and flash.bat
, and you have a neat package to send over.
The Grand Reveal⌗
So it’s all done, here’s where I’d put a little demo of the bindicator in action. However my friend is a lazy bastard that has yet to actually put it together yet. My prototype is simply the S3 SuperMini with the onboard LED lit up, so it’s hardly an impressive visual.

Thing is, it’s getting so long that I’ve been starting to forget the code I wrote. I’ve written this blog as a personal note to remind myself what I did and why. In the distant future when it’s put together I’ve inevitably have bugs to squash and can use this blog as a reference.
-
This is finally supported on Android as of version 12. ↩︎
-
This is where limiting the scope to a single local government really helps. I can make assumptions about the number and types of bins, and how often they are collected. The only variable to figure out is bin day. ↩︎
-
To save battery I don’t want the LED turning on at midnight the day before when no one can notice it. Making it a configurable options leds my friend control the balance between battery life and what is useful. ↩︎
-
Fun fact, since the bin API only takes street number, entering the address for a massive apartment block will give a huge response listing identical entries for each apartment. This crashes the ESP. ↩︎
-
The real solution is to either perform another NTP request each startup, or use a separate real time clock IC. ↩︎