This is a submission for the Built with Google Gemini: Writing Challenge
What I Built with Google Gemini
Between organizing STEAM workshops and teaching my 4-month beginner Arduino course, I am constantly exploring new ways to push the boundaries of what microcontrollers can achieve. I wanted a project that moved beyond basic sensor reading and dove deep into the complexities of radio frequency communication and real-time processing.
That drive led to OpenPager: a high-precision POCSAG (pager) transceiver library for Arduino-compatible microcontrollers (ESP8266, ESP32) and the TI CC1101 radio module.
The Features:
Auto-Baud Decoding: Automatically detects and receives 512, 1200, and 2400 baud messages simultaneously using parallel software decoders.
True Non-Blocking RX: A hardware timer ISR samples GDO0 at ~19.2 kHz. The main loop() just drains a ring buffer, ensuring it never busy-waits or bit-bangs.
Dual Radio Mode: Supports using two CC1101 modules on the same SPI bus, allowing for true simultaneous TX and RX operations without ever dropping a received packet.
Gemini's Role in the Build:
Implementing a protocol from the 1980s on modern IoT hardware requires a lot of bitwise gymnastics and register configuration. I leaned heavily on Google Gemini for:
Register Configurations: Generating baseline hex values for CC1101 initialization, specifically calculating data rate multipliers for the different baud rates.
BCH Error Correction: Translating the mathematical polynomials of POCSAG's BCH(31,21) error-correcting code into efficient, bitwise C++ functions.
Alphanumeric Decoding: Helping write the logic to unpack 7-bit ASCII from 20-bit payload chunks, which requires reversing bits and shifting across arbitrary boundaries.
Demo
You can view the full project, documentation, and source code on GitHub:
https://github.com/ktauchathuranga/openpager
Here is a quick look at the non-blocking architecture in action. While the hardware timer samples the radio in the background, the main loop remains completely free to run other tasks—like a heartbeat LED, sensor reads, and uptime tracking—without stuttering:
void loop() {
// Pager processing (non-blocking)
pager.loop(); [cite: 14]
uint32_t now = millis(); [cite: 14]
// Task 1: LED heartbeat
static uint32_t lastBlink = 0; [cite: 14]
static bool ledOn = false; [cite: 14]
if (now - lastBlink >= HEARTBEAT_MS) { [cite: 15]
lastBlink = now; [cite: 15]
ledOn = !ledOn; [cite: 15]
digitalWrite(HEARTBEAT_LED, ledOn); [cite: 16]
}
// Task 2: Analog sensor read
static uint32_t lastSensor = 0; [cite: 16]
if (now - lastSensor >= SENSOR_MS) { [cite: 17]
lastSensor = now; [cite: 17]
int val = analogRead(SENSOR_PIN); [cite: 18]
Serial.printf("[Sensor] A0 = %d\n", val); [cite: 18]
}
}
What I Learned
This project was a masterclass in hardware timers and interrupt safety.
When building the non-blocking architecture, I had to ensure the ISR executed in sub-microseconds. I learned the hard way about what you can and cannot do inside an Interrupt Service Routine. Passing data from the ISR to the main loop using a volatile ring buffer taught me a ton about atomicity and race conditions. For example, on the ESP8266, using Timer1 for the ISR means standard functions like Servo and analogWrite (PWM) are entirely unavailable while receiving.
Additionally, managing the ESP8266's software watchdog timer was an unexpected lesson. During transmission, clocking out bits blocks the processor. I had to strategically insert yield() calls on the ESP8266 to prevent watchdog resets, while carefully avoiding this on the ESP32 to prevent FreeRTOS from introducing multi-millisecond gaps that corrupt bit timing.
Looking forward, I want to take this rock-solid asynchronous architecture and apply it to modern IoT projects. I'm currently conceptualizing a Smart Agro-Cloud Observer, and these lessons on keeping the main loop non-blocking while handling strict sensor timing will be invaluable.
Google Gemini Feedback
What worked well:
Gemini is phenomenal at explaining binary and bitwise operations. When staring at a hex dump of a pager transmission, I could paste the raw binary into Gemini, and it would perfectly break down the 32-bit codewords into the Address, Function bits, and BCH parity bits. It made debugging the decoder significantly less painful.
Where I ran into friction:
Where Gemini struggled was with the highly specific, hardware-dependent quirks of the ESP32 and ESP8266 SDKs.
For instance, when setting up the SPI bus on the ESP32, Gemini suggested a standard SPI.begin(). However, the ESP32's default SPI implementation claims GPIO 5 as the SS pin. If you're using GPIO 5 for the CC1101's GDO0 interrupt pin (like I was), this creates bus contention. I had to manually debug this and pass -1 for the SS pin to prevent the conflict. AI is incredible for logic and algorithms, but it doesn't always have the "street smarts" of undocumented hardware quirks.
Top comments (0)