-----
The Austin N5OAK Ham Radio Club Repeater seems to be pretty active. That shouldn't surprise us much because the club has a lot of activities and a lot of enthusiastic members. Still, we wondered just how active and opened the spare parts drawer to see if it held a solution.
-----
Turns out the spare parts drawer did; take a look at the wiring diagram at the top of the page. We simply used a C3 Dev Board with an input pin wired to to the output speaker of a Baofeng HT tuned to the repeater frequency. We monitor the C3 input pin and if it sees a signal the repeater in transmitting. We push that data to AdaFruit IO at the top of each hour to graph the history.
The code also creates a small web server that lets us see the status of the C3 data collection in real time:
The project is pretty straightforward and flexible. The Baofeng HT can be used to monitor any frequency in the ham bands for monitoring sat passes, simplex frequencies, etc. Below is the code that was, like all code today, written with the help of AI:
-----
//
// ESP32-C3 N5OAK=R Repeater TX TIME LOGGER + HOURLY ADAFRUIT IO
// Speaker output of Baofeng HT feed into uC IO pin via a diode to drop voltage.
//
// IDE Settings: ESP32-C3 Dev Module + USB CDC On Boot Enabled (needed for Serial Monitor)
// https://www.whiskeytangohotel.com/
// APRIL 2026
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <WiFi.h>
#include "time.h"
#include <WebServer.h>
// ===================== ADAFRUIT IO =====================
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"
#define AIO_SERVER "io.adafruit.com"
#define AIO_SERVERPORT 1883
#define AIO_USERNAME "UR_ADAFRUIT_USERNAM"
#define AIO_KEY "UR_ADAFRUIT_PRIVATE_KEY"
WiFiClient client;
Adafruit_MQTT_Client mqtt(
&client,
AIO_SERVER,
AIO_SERVERPORT,
AIO_USERNAME,
AIO_KEY
);
Adafruit_MQTT_Publish N5OAK_R(
&mqtt,
AIO_USERNAME "/feeds/N5OAK-R"
);
// ===================== WIFI SETTINGS =====================
const char* ssid="UR_SSID";
const char* password="UR_WIFIPASSWORD";
const char* ntpServer="pool.ntp.org";
#define INPUT_PIN 3 // From BF HT Speaker via diode for voltage drop
#define LED_PIN 8 // On board LED ON when TX is detected
// =========================================================
// C3 On Board OLED
U8G2_SSD1306_72X40_ER_F_HW_I2C
u8g2(U8G2_R0,U8X8_PIN_NONE);
// WEB Server Setup to view stats from a browser
WebServer server(80);
// ==== DEFINE SOME VARS =====================
uint64_t highTimeMs = 0;
uint64_t totalHighTimeMs = 0;
uint64_t lastIntegratorMs = 0;
unsigned long lastBlink = 0;
unsigned long lastSerial = 0;
bool colonVisible = true;
bool pinState = LOW;
String lastSendMsg="Waiting...";
int lastHour=-1;
// ===================== ADAFRUIT MQTT CONNECT =====================
void MQTT_connect(){
if(mqtt.connected()) return;
Serial.print("Connecting to Adafruit MQTT...");
int8_t ret;
while((ret=mqtt.connect())!=0){
Serial.println(" retry...");
mqtt.disconnect();
delay(2000);
}
Serial.println(" connected");
}
// =========== PUBLISH TO ADAFRUIT =============
void publishToAdafruit(
float minutesHigh,
const char* source
){
Serial.print(source);
Serial.print(" -> N5OAK-R = ");
Serial.println(minutesHigh);
MQTT_connect();
if(!N5OAK_R.publish(minutesHigh)){
Serial.println("MQTT FAIL");
lastSendMsg=
String(source)+" FAILED";
}
else{
Serial.println("MQTT OK");
lastSendMsg=
String(source)+
": "+
String(minutesHigh,2)+
" MINS SENT";
}
}
// ====== HOW LONG HAS THE PROGRAM BEEN RUNNING =====================
String formatUptime(){
uint32_t seconds = millis() / 1000;
uint32_t days = seconds / 86400;
uint32_t hours = (seconds % 86400) / 3600;
return String(days) + "d " + String(hours) + "h";
}
// ===================== SETUP =====================
void setup(){
Serial.begin(115200);
delay(1000);
Serial.println("\nBOOT OK");
Wire.begin(5,6);
u8g2.begin();
pinMode(INPUT_PIN,INPUT);
pinMode(LED_PIN,OUTPUT);
digitalWrite(LED_PIN,HIGH);
lastIntegratorMs=millis();
// OLED startup
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_fur20_tf);
u8g2.drawStr(8,30,".??");
u8g2.sendBuffer();
// WiFi
WiFi.begin(ssid,password);
Serial.print("WiFi connecting");
while(WiFi.status()!=WL_CONNECTED){
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi CONNECTED");
Serial.println(WiFi.localIP());
// Time
configTzTime(
"CST6CDT,M3.2.0/2,M11.1.0/2",
ntpServer
);
setenv(
"TZ",
"CST6CDT,M3.2.0/2,M11.1.0/2",
1
);
tzset();
// ======= PUBLISH TO WEB SERVER =================
server.on("/", [](){
time_t now;
time(&now);
struct tm tm_info;
localtime_r(&now,&tm_info);
char tbuf[20];
strftime(
tbuf,
sizeof(tbuf),
"%H:%M:%S",
&tm_info
);
// Get IP address
String ip = WiFi.localIP().toString();
String html =
"<html><head>"
"<meta http-equiv='refresh' content='3'>"
"<title>N5OAK-R Monitor</title>"
"</head><body><pre>";
// ===== TITLE =====
html += "<b style='font-size:18px;'>N5OAK-R TX DATA AT: ";
html += ip;
html += "</b>\n\n";
// ===== DATA =====
html += "LOCAL TIME: ";
html += tbuf;
html += "\n";
html += "TX SENSE PIN: ";
html += (pinState ? "HIGH" : "LOW");
html += "\n";
html += "CURRENT MINS THIS HOUR: ";
html += String(highTimeMs/60000.0,2);
html += "\n";
html += "UPTIME: ";
html += formatUptime();
html += "\n";
html += "DATA LAST SENT: ";
html += lastSendMsg;
html += "\n\n";
// ===== LINK =====
html += "<a href='https://io.adafruit.com/ironjungle/dashboards/n5oak-r-usage-by-whiskeytangohotel-dot-com?kiosk=true' target='_blank'>";
html += "View Adafruit Dashboard";
html += "</a>";
html += "</pre></body></html>";
server.send(200,"text/html",html);
});
server.begin();
// IP display on the C3 at boot
String ip=WiFi.localIP().toString();
String lastOctet="."+ip.substring(ip.lastIndexOf('.')+1);
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_fur20_tf);
int16_t x=(72-u8g2.getStrWidth(lastOctet.c_str()))/2;
u8g2.drawStr(x,30,lastOctet.c_str());
u8g2.sendBuffer();
delay(3000);
Serial.println("SYSTEM READY");
}
// ==========FOREVER LOOP =====================
void loop(){
uint64_t nowMs=millis();
// ACCURATE INTEGRATOR
uint64_t elapsed=nowMs-lastIntegratorMs;
lastIntegratorMs=nowMs;
pinState=digitalRead(INPUT_PIN);
if(pinState){
highTimeMs += elapsed;
totalHighTimeMs += elapsed;
}
digitalWrite(LED_PIN,!pinState);
// TIME
time_t now;
time(&now);
struct tm tm_info;
localtime_r(&now,&tm_info);
int currentHour=tm_info.tm_hour;
if(lastHour==-1) lastHour=currentHour;
// TOP OF HOUR SO LONG TO ADAFRUIT
if(currentHour!=lastHour){
float minutesHigh=highTimeMs/60000.0;
if(minutesHigh>0.0 && minutesHigh<1.0){
minutesHigh=1.0;
}
publishToAdafruit(minutesHigh,"AUTO TOP OF HOUR");
highTimeMs=0;
lastHour=currentHour;
}
// HEARTBEAT OLED FLASH THE ":" TO SHOW IT'S RUNNING
if(nowMs-lastBlink>=1000){
lastBlink=nowMs;
colonVisible=!colonVisible;
}
int displayMinutes=(highTimeMs/60000)%60;
char buf[6];
snprintf(
buf,
sizeof(buf),
colonVisible ? ":%02d" : " %02d",
displayMinutes
);
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_fur20_tf);
int16_t x=(72-u8g2.getStrWidth(buf))/2;
u8g2.drawStr(x,30,buf);
u8g2.sendBuffer();
// SERIAL MONITOR OUTPUT
if(nowMs-lastSerial>=1000){
lastSerial=nowMs;
Serial.print("SENSE=");
Serial.print(pinState?"H":"L");
Serial.print(" | HR MIN=");
Serial.print(highTimeMs/60000.0,2);
Serial.print(" | UPTIME=");
Serial.print(formatUptime());
Serial.println();
}
server.handleClient();
delay(20);
}
-----

