A part of my “Solar Server” series of projects, Community Hub was a project built around the constraint of doing useful compute using only solar power. This was meant to be the “tiny” attempt. “Eh, let’s make something small on an ESP32 or something”. What it ended up being was a philosophical exercise in thinking about local compute in a new way.
I am very much drinking the solarpunk Kool-Aid right now. I am very filled with zeal about the entire movement. I've got multiple projects in the hopper that are basically, "What kind of tech would be in line with a solarpunk world"? The idea of this came to me in the context of thinking that if a big part of solar punk philosophy is the idea of local manufacturing, local sourcing of materials, local shopping... what does "local" computing look like? How does computing and information flow operate on a PHYSICAL community level when large infrastructure isn’t the focus?
The thought was initially a community chat room that people could connect to. I tried implementing this on an ESP32-C3 and it worked with 4-5 connections, but started failing around 9-10 connections. Just not enough grunt to support THAT many WebSockets open at once.
But then I remembered that part of the idea of a solarpunk world is a less disconnected community. A location-locked chat room doesn’t make sense in a world where people are systematically encouraged to just go and talk to and spend time with each other. So then I thought "what kind of community information utility would be useful at range”? That's how I landed on a bulletin board.
So let’s break down the features!
From the user side, Community Hub creates an open access point that uses a captive portal to funnel users to the main page. Here, people can make postings that fall under one of four categories:
- Notice: For things like scheduled maintenance of utilities, severe weather warnings, and other things the local community should know about.
- Offer: People offering up goods or services, sometimes with a specific trade in mind. Example: “Free ladder”, “Come pick apples from my tree”, “Free pet checkups”, “Offering my ATV for a dirt bike”
- Needs: People asking for help or goods, usually along with an offer for what they have to exchange. Example: “Need subjects to practice portrait photography; can do family portraits as trade”, “Help me fix my skateboard?”, “Need help moving a couch and refrigerator; pizza and drinks for those who help”, “Looking for good jars for making a big batch of jam; one jar of jam for every 5 empty jars”
- Events: Local events that are open to the community (farmers markets, open air concerts, art walks, local sports, etc)
Users can post their name and set an expiration for the posting (1 day, 3 days, 1 week) after which time the post will drop off of the board. They view active postings on the same page and can filter them by type, which are also color coded. There are no user accounts, anyone can post under any name. This may seem odd in today’s world, but this is a website accessible only within the Wi-Fi range of the ESP32 and is only really useful to people in a specific community.
For admins, they are give administrative control via the password protected Admin panel.
- Board Identity: These are settings like the Board Name, tagline, rules, and footer. There’s also a settable icon (which is just a field for Emojis)
- Manage Posts: Admins can look at postings in a list and have the option to delete them individually, flush the RAM cache of postings to storage (more on that later), or wipe the slate clean
- LED Settings: Some may want to use an LED to indicate recent activity or even just to provide light. Settings include:
- LED Usage Toggle
- A toggle for a consistent pulse of the LED
- A toggle for the LED to pulse on recent activity
- Day/Night time settings
- Day/Night brightness settings (PWM)
- LED Pin
- Backups: The postings on the board can be backed up and restored as a JSON file.
- OTA Firmware Update: Since I envision devices like this being installed in fixed locations, OTA updates were imperative in the case that the device couldn’t be reached with a USB cable.
I’m very happy with how this project turned out. That said, it’s not without its limitations.
Known Limitations:
Timekeeping:
The ESP32-C3 doesn't have a built-in RTC (real-time clock), and Community Hub is designed to run fully offline with no NTP access, so the clock is... artisanal. You set it manually through the admin panel and the system keeps track of it moving forward, saving the time to disk every 30 minutes.
The practical drift on a power loss is “however long the device was off” plus up to 30 minutes. Since the shortest post expiry is 24 hours and the longest is a week, this is accurate enough that it doesn't matter in practice. A future revision with an I2C RTC module would fix it properly, but that's a hardware addition for another day.
Hey, it’s better than VCRs used to be!
Captive Portal:
Captive portals are weird. It works pretty reliably on iOS devices, and generally reliably on Android, but (for example) it doesn’t work with any of my Samsung devices. If it doesn't trigger, connect to http://192.168.4.1 directly. My way of making this convenient is by having a sign with a QR code that takes people to that URL. Not perfect, but it works.
How to Use:
- Download the Arduino Sketch from my GitHub.
- Change WiFi AP configuration if needed (line 51).
- Make sure the partition scheme in the Arduino IDE is set to “Default 4MB with spiffs (1.2MB APP / 1.5MB SPIFFS)”
- Program as normal. It should work on any ESP32 device.
- Connect to the WiFi AP.
- If the captive portal doesn’t work for you (it may not), connect to http://192.168.4.1
- The admin panel is reachable at http://192.168.4.1/admin
- Change settings as you wish.
- You’re done!
Technical FAQ
How does the captive portal work?
When you connect to the Community Hub WiFi, your device does a little connectivity check — a background request to a known URL to see if you're on the internet or stuck behind a login wall. The code actually handles a dozen specific probe URLs across Apple, Android, Microsoft, and Firefox. Community Hub intercepts that probe and redirects it to the board, which is what triggers that "Sign in to network" popup.
The catch: every OS does this differently. iOS is pretty cooperative. Windows is hit or miss. Android — especially Samsung devices running One UI — is famously persnickety about it. Samsung's firmware does extra validation that a redirect to an offline IP doesn't satisfy, so the portal popup either doesn't appear or comes with a "this network has no internet" warning.
The reliable fallback is always http://192.168.4.1. In a community that uses the board regularly, people would learn this address fast. My practical solution is having a sign with a QR code for that URL.
How does OTA updating work without bricking the device?
The ESP32's flash is divided into partitions. Community Hub uses the default 4MB layout, which includes two app slots (~1.2MB each) and a data partition (~1.5MB). Only one app slot is running at a time.
When you upload a firmware binary through the admin panel, it gets written to the inactive slot — the one that isn't currently running. Once the write completes and the image verifies, the otadata partition is updated to flag the new slot as the boot target, and the device reboots into it. You can't overwrite the firmware you're currently running; you'd be sawing off the branch you're sitting on.
Crucially, the data partition is completely separate from both app slots. OTA updates don't touch it. Your posts, config, and admin key survive a firmware update intact.
How is data stored?
Of the 4MB of storage on the device, 1.5MB is set aside for data. This is typically called the SPIFFS partition, but I chose to use LittleFS since it has better power-loss resilience and doesn't degrade as badly as it fills up. For a device that might lose power at any moment because a cloud rolled over a solar panel, that's not a trivial distinction. And, for the record, it CAN run 100% on solar (I used a 5v, 6w panel to test this).
That said, LittleFS still isn't magic. It won't save you from writing half a file and pulling the plug. Which is why every config file in Community Hub is saved by writing to a .tmp file first, then atomically renaming it over the real one. At any moment in time you either have the old complete file or the new complete file — never a half-baked one. I learned this lesson in a different project that I’ll document at some point.
Why is the whole UI stored in PROGMEM as one giant string?
The entire HTML, CSS, and JavaScript for both the main board and the admin panel lives in the .ino file as raw string literals tagged with PROGMEM, which tells the compiler to keep them in flash memory instead of copying them into RAM on boot.
The alternative would be storing the HTML as actual files in LittleFS and serving them from there. That's arguably cleaner and would open up more program memory for more features, but it adds deployment friction: you'd need to flash the filesystem image separately from the firmware and make sure that the website file versions were in sync with the Arduino code.
One file, one flash operation, done. The tradeoff is that editing the frontend requires a reflash, but given OTA support is built in, that's pretty painless. The whole process takes, like, 10 seconds. Also, I did my best to give as many options as possible. I tried including the CSS colors in the admin panel, but that started to feel like too much. If you don’t like my color scheme, you can change it.
What did you mean by ”flush the RAM cache”?
Messages are not written to flash the instant someone posts. Instead, the code sets a msgsDirty flag and waits 60 seconds before committing. If another post comes in during that window, the timer resets. Only once things have been quiet for a minute does it actually write.
This is all just to reduce the wear on the flash storage. NOR flash has a finite number of write cycles per block — somewhere around 100,000 on the low end. Writing on every single post would burn through that budget fast on a busy board. Batching writes means a flurry of posts in quick succession costs one write instead of ten.
The admin panel has a "Force Save" button if you want to flush immediately - say, before pulling the power intentionally.
How are expired posts automatically dealt with?
They’re mostly not.
Two reasons: First: automatic cleanup would need background processing, and the loop is already busy handling DNS, HTTP requests, and updating the LED on every tick. I mean, it’s just a lil guy.
Second: given there is no TRUE RTC, a post being “expired” might be a time issue. So it’s safer to keep expired posts stored, just in case.
So, how do you keep expired posts, but not show them? You fake it. Expired posts are filtered out at render time on the client. If a post is expired, it just doesn’t show up on the site. But they stay in the array on the server until something displaces them. That "something" is the capacity eviction logic.
How many posts can the board hold?
The board holds up to 200 posts. If it fills up and a new post comes in, the firmware looks for the oldest expired post and evicts it to make room. If there are no expired posts to evict, the board is genuinely full: at which point a banner appears on the board and the post button disables itself. The board self-heals as posts expire naturally.
200 was chosen because at worst-case post size (300-character message, 24-character name), the JSON payload for the full board is about 76KB. That fits comfortably in the ESP32-C3's heap alongside the WiFi stack and WebServer, with room to breathe. 300 would be pushing it.
And, let’s be real: If your community is adding 200 posts within a week, you might want something a bit beefier than an ESP32 to run a community board.
Why no user accounts or moderation?
This is an intentional design decision rather than an oversight. Any given Community Hub is only accessible within WiFi range of the physical device - we're talking tens of meters, maybe a bit more outdoors. This thing isn’t on the internet; it’s on your street.
In that context, the social dynamics are different. You probably know, or could find out, which “Dave” made what post. If Marge from the HOA is posting too much, someone can tell her so. Anonymity is limited by proximity. The admin can delete posts and the board has configurable rules — that's the moderation model, and it's deliberately lightweight because the expectation is that people behave because they're neighbors, not because a system is enforcing it. It's less a technical decision and more a philosophical one about what "local" computing is actually for.
Victor Frost