Published on November 20, 2025

The Bell That Stopped Ringing:
We have this really old bell at Moody Church. For the two years I've been going there, it's rung before every service. You can hear it all across Lincoln Park. It's one of those neighborhood things that you don't really think about until it's gone.
A few weeks ago, it stopped.
I was volunteering on sound one Sunday when Mike, our Director of AV, mentioned it. The controller had finally died after 20+ years. He'd gotten some quotes for replacements and they were all coming in at $4,000 or more.
Here's the thing about bell controllers: they're ridiculously proprietary. The bell striker mechanism itself was fine (it's basically just an electromagnetic solenoid that physically hits the bell). But the control systems are locked into specific manufacturers. Even other bell companies' controllers run thousands of dollars, and there's no guarantee they'll work with your existing hardware.
So we had this beautiful old bell that all of Lincoln Park could hear, and we just needed something to tell it when to ring. The old controller from 2002 couldn't do anything smart. No network access, no way to adjust schedules without physically accessing it, no logging. Just a very heavy metal box bolted to the wall.
Right People, Right Time:
Lucky for me, I was serving that day with Matthew Banevich, who happens to be a ridiculously talented electrical engineer. The three of us got talking: could we actually replace this thing with a Raspberry Pi for under $50?
Matt's response was basically "let me take the old controller home and see what we're working with."
This is where the project got real. I can do software dev all day, but I had absolutely no idea how the existing system actually worked electrically. Matt is a wizard with a multimeter. He reverse-engineered the entire thing.
Perfect marriage of specialties: Matt figures out the electrical side, I handle the software, Mike coordinates with the church and understands what they actually need day-to-day.
What Matt Found:
Matt traced through the old controller's circuitry and figured out exactly what we needed to bypass. The bell striker runs on 250VDC (no joke, this thing hits HARD), and the old controller was basically just switching that high voltage on and off with the right timing.
His insight: we could skip the entire old controller and drive the striker directly through a solid-state relay.
Here's why that's brilliant: solid-state relays have no moving parts. Traditional mechanical relays click when they switch and eventually wear out. With solid-state circuitry, there's nothing to fail mechanically. You also can't troubleshoot it by listening for clicks, which is kind of weird at first, but way more reliable long-term.

System Architecture:
The hardware stack is pretty straightforward:
- Raspberry Pi 4B (yeah, it's probably overkill, but it's $35 and we wanted headroom)
- 5" touchscreen so church staff can control it from the office
- GPIO breakout HAT for clean labeled connections
- Solid-state relay module
For software, I went with a two-tier setup because it made sense for how the system would actually be used:
Backend (Flask/Python):
- Flask REST API handling all the logic
- APScheduler for cron-style scheduled rings
- SQLAlchemy + SQLite for storing schedules
- GPIO control with a simulation fallback for development
Frontend (React):
- Single-page app optimized for touchscreen use
- Polls the backend every 10 seconds for status
- Manual controls for ringing the bell right now
- Schedule managers for setting up automatic rings and quiet hours
The frontend just talks to the backend over REST. Simple, debuggable, and I could develop the whole thing on my laptop before ever touching the Pi.
Bell Timing:
Getting the bell to ring isn't just turning a pin on and off. There's a rhythm to it.
Each ring pulse is:
- GPIO pin goes HIGH for 100ms (this triggers the striker)
- Pin goes LOW
- Wait 2.9 seconds (total 3 second cadence)
- Repeat if we're doing multiple tolls
Why 100ms? That's enough time to reliably trigger the striker mechanism. Why 3 seconds between rings? The bell needs time to swing and resonate. If you hit it again too fast, it sounds terrible and the tones interfere with each other.
The actual code is pretty straightforward:
def ring_bell(count=1):
"""Execute bell ring with proper timing."""
if is_muted():
log_event("Ring blocked by mute schedule")
return
for i in range(count):
GPIO.output(BELL_PIN, GPIO.HIGH)
time.sleep(0.1) # 100ms strike
GPIO.output(BELL_PIN, GPIO.LOW)
if i < count - 1:
time.sleep(2.9) # 3s total cadence
Everything respects the mute schedules (more on that in a sec), and every action gets logged so we can see what actually happened.
Scheduling:
The system ships with sensible defaults for church use:
Ring Schedule:
- Monday through Saturday: 9 AM, noon, 3 PM, 6 PM
- Sunday can have its own pattern
- Everything's configurable through the UI
Mute Schedules:
This part actually got pretty sophisticated. We have two types:
-
Recurring mute windows: The big one is 8 PM to 6 AM every night. Lincoln Park residents appreciate not being woken up by church bells at 3 AM.
-
One-time mutes: For special events, maintenance, or "please don't ring during the wedding ceremony" situations.
The tricky part was handling cross-midnight windows (8 PM today to 6 AM tomorrow) without breaking the logic. And then there's manual override.
If someone needs to ring the bell during a scheduled quiet period (emergency, special occasion, whatever), they can manually override the mute. But here's the smart part: the system tracks which specific mute schedule was overridden and clears that override when the window naturally ends. This prevents the system from permanently drifting away from the intended schedule.

Data Model:
The SQLite database is pretty minimal:
Ring Schedules:
- Day of week (or "all days")
- Hour and minute
- How many tolls
- Enabled/disabled flag
Mute Schedules:
- Name (like "Nighttime Quiet Hours")
- Start and end datetime
- Is it recurring or one-time?
- Enabled/disabled flag
Activity Log:
This was crucial. Every action (scheduled rings, manual rings, mute toggles, schedule changes) gets logged with a timestamp. The log rotates at 100KB and keeps the last 100 entries in memory for quick access.
During testing, this meant we could verify the system was working correctly without actually ringing the bell constantly and driving everyone in the neighborhood insane.
Frontend:
The React app is designed for someone standing in the church office using the 5" touchscreen:
Main screen:
- Big bell button in the center for "ring it right now"
- Mute toggle with clear visual feedback
- Quick presets for 3, 6, 9, 12, or 15 tolls
- Live feed showing recent activity
- Status bar with current time and whether we're in a mute window
Schedule management:
- Ring schedules grouped by day, sorted by time
- Add/edit/delete with validation
- Mute schedules split between recurring and one-time events
One UI detail I'm proud of: when you try to unmute during a scheduled quiet period, the app asks for confirmation:
Mute schedule 'Nighttime Quiet (8 PM - 6 AM)' is currently active. Override until 6:00 AM?
This makes it obvious what you're doing and prevents accidental unmutes.
Deployment:
I wrote a bash installer that handles the entire setup process:
- Validates you're actually running on a Raspberry Pi
- Installs all the system packages (Python, Node, Nginx, Chromium)
- Copies the code to
/opt/moody-bell - Sets up a Python virtualenv and installs dependencies
- Builds the React production bundle
- Configures systemd to run the Flask app with auto-restart
- Sets up Nginx to serve the frontend and proxy API calls
- Optionally configures Wi-Fi, NTP time sync, and touchscreen rotation
- Sets up kiosk mode so the Pi boots straight into the UI
The kiosk mode is pretty slick. The Pi boots up, disables screen blanking, hides the cursor, and launches Chromium in fullscreen pointing at the local UI. No desktop environment, no distractions, just the bell controller.
Remote Access:
Mike (our church IT guy) set up something really clever for remote access. Instead of poking holes in the firewall or setting up a VPN, he used Cloudflare Access.
Here's how it works:
Cloudflare Tunnel runs as a service on the Pi and creates an outbound-only connection to Cloudflare's network. No inbound firewall rules needed, no exposed ports, nothing listening publicly.
When you want to access the bell controller (either the web UI or SSH), you go through Cloudflare's network:
- Navigate to something like
bells.moody.church - Cloudflare intercepts and requires authentication
- We configured it to require specific email addresses (just our team)
- Once you're authenticated, Cloudflare proxies your connection through to the Pi
Same thing works for SSH. You use cloudflared access ssh and it handles the authentication and proxying.
Why this is awesome:
- The Pi can be on any network (church Wi-Fi, cellular backup, whatever)
- No public IP address needed
- No VPN configuration
- Complete audit logs of who accessed what
- Can instantly revoke someone's access if needed
- All traffic is encrypted end-to-end
For a volunteer-run church IT setup, this is incredibly maintainable. No one has to remember weird VPN credentials or deal with port forwarding.
Testing:
Testing a church bell system in a residential neighborhood is... delicate.
Our approach:
-
Simulation mode during development: The backend has a fallback mode where it pretends to control GPIO without actually doing it. I could develop the entire scheduling system on my laptop.
-
Activity logging: Every ring attempt gets logged whether it actually executed or got blocked by a mute schedule. We could verify the logic was correct before making noise.
-
Strategic timing: We tested during mid-day hours with advance warning to neighbors.
-
Start small: First test was a single ring to confirm the GPIO control worked. Then we verified the timing. Then we tried sequences.
-
Remote verification: I live close enough to hear the bells from my apartment. When the system went live, I could confirm the 9 AM schedule worked without even being at the church.
The logging turned out to be critical for building confidence with church staff too. They could see that yes, the system tried to ring at the scheduled time, yes it actually executed, and yes it logged everything properly.
Lessons Learned:
What worked really well:
- Solid-state relay approach: zero mechanical failures so far
- Touch UI: church staff picked it up immediately, no training needed
- Activity logging: made debugging trivial and built trust
- Cloudflare Access: remote updates without climbing ladders
Tricky bits:
- Timezone handling: SQLite stores timestamps without timezone info, so we had to be really careful about DST transitions
- Cross-midnight mute windows: Handling "8 PM today to 6 AM tomorrow" was trickier than expected
- APScheduler reliability: Added 60 second misfire grace period for brief power interruptions
Things we could add:
- Weather integration (auto-mute during storms)
- Holiday detection (special patterns for Christmas/Easter)
- Audio monitoring (confirm the bell actually rang with a mic)
- Multi-bell support (some churches have multiple bells)
- Calendar sync (automatically mute during scheduled events)
Cost Breakdown:
Parts:
- Raspberry Pi 4B (4GB): $35
- 5" touchscreen: $35
- Solid-state relay module: $8
- GPIO breakout HAT: $12
- MicroSD card, case, cables: ~$15
- Total: about $105
What we got that a $4,000 proprietary system wouldn't give us:
- Web interface accessible from anywhere
- Complete activity logs
- Unlimited schedule flexibility
- Remote access and updates
- Touch-optimized local control
- Open source code we can modify
- No vendor lock-in
The bells now ring reliably every day at 9 AM, noon, 3 PM, and 6 PM. Lincoln Park still gets its familiar soundtrack. And when something needs tweaking, we can SSH in from anywhere and make changes.
Final Thoughts:
This project is basically everything I love about working on embedded systems. Real problem, practical constraints, legacy hardware that needs reverse engineering, and building something that's actually maintainable.
Sometimes the best solution isn't the expensive commercial one. It's the one built by people who understand the problem, have complementary skills, and aren't afraid to figure things out.
And honestly? Hearing those bells ring on schedule from my apartment, knowing the code I wrote is making it happen, is pretty satisfying.