<pre><code>
#include <math.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <TinyGPS++.h>
// Pin definitions
#define OUT_PIN 2 // AFSK output
#define LED_PIN 5 // PTT/LED transmit
#define ONBOARD_LED 2 // LED built-in ESP32 Dev Module
// Tone constants
#define _1200 1
#define _2200 0
#define _FLAG 0x7e
#define _CTRL_ID 0x03
#define _PID 0xf0
#define _DT_EXP ','
#define _DT_STATUS '>'
#define _DT_POS '!'
#define _NORMAL 1
#define _BEACON 2
#define _FIXPOS 3
#define _STATUS 2
#define _FIXPOS_STATUS 1
bool nada = _2200;
const float baud_adj = 0.975f;
const float adj_1200 = 1.0f * baud_adj;
const float adj_2200 = 1.0f * baud_adj;
unsigned int tc1200 = (unsigned int)(0.5f * adj_1200 * 1000000.0f / 1200.0f);
unsigned int tc2200 = (unsigned int)(0.5f * adj_2200 * 1000000.0f / 2200.0f);
// Config variables
char mycall[10] = "YD2XXX"; //calsign anda
uint8_t myssid = 7;
char mystatus[100] = "Tracker Test Over ESP32 - Havid";
bool smartBeaconing = false;
unsigned long tx_interval = 60000UL; // ms
const char *dest = "APRS";
const char *dest_beacon = "BEACON";
// === PATH DIGI YANG DIPERBAIKI (untuk terdigi MAKSIMAL) ===
char digi1[6] = "WIDE1"; // Local digipeater
uint8_t digissid1 = 1;
char digi2[6] = "WIDE2"; // Wide-area digipeater
uint8_t digissid2 = 2;
// ========================================================
char lat[9] = "0000.00N";
char lon[10] = "00000.00E";
const char sym_ovl = '/';
const char sym_tab = 'k';
char bit_stuff = 0;
unsigned short crc = 0xffff;
TinyGPSPlus gps;
HardwareSerial gpsSerial(1);
bool gps_valid = false;
bool gps_locked = false;
double last_lat = 0.0;
double last_lon = 0.0;
float last_speed = 0.0;
unsigned long last_tx_time = 0;
WebServer server(80);
char dummy_lat[9] = "0659.00S";
char dummy_lon[10] = "10649.00E";
Preferences prefs; // Preferences object
// Additional for GPS status
String gps_connection_status = "Not Connected";
unsigned long last_gps_data_time = 0;
unsigned long last_status_print = 0;
void set_nada_1200() {
digitalWrite(OUT_PIN, HIGH);
delayMicroseconds(tc1200);
digitalWrite(OUT_PIN, LOW);
delayMicroseconds(tc1200);
}
void set_nada_2200() {
digitalWrite(OUT_PIN, HIGH);
delayMicroseconds(tc2200);
digitalWrite(OUT_PIN, LOW);
delayMicroseconds(tc2200);
digitalWrite(OUT_PIN, HIGH);
delayMicroseconds(tc2200);
digitalWrite(OUT_PIN, LOW);
delayMicroseconds(tc2200);
}
void set_nada(bool n) {
if (n) set_nada_1200();
else set_nada_2200();
}
void calc_crc(bool in_bit) {
unsigned short xor_in = crc ^ in_bit;
crc >>= 1;
if (xor_in & 0x01) crc ^= 0x8408;
}
void send_crc() {
unsigned char crc_lo = crc ^ 0xff;
unsigned char crc_hi = (crc >> 8) ^ 0xff;
send_char_NRZI(crc_lo, HIGH);
send_char_NRZI(crc_hi, HIGH);
}
void send_header(char msg_type) {
char temp;
const char *dest_str = (msg_type == _NORMAL) ? dest : dest_beacon;
temp = strlen(dest_str);
for (int j = 0; j < temp; j++) send_char_NRZI(dest_str[j] << 1, HIGH);
for (int j = temp; j < 6; j++) send_char_NRZI(' ' << 1, HIGH);
send_char_NRZI('0' << 1, HIGH);
temp = strlen(mycall);
for (int j = 0; j < temp; j++) send_char_NRZI(mycall[j] << 1, HIGH);
for (int j = temp; j < 6; j++) send_char_NRZI(' ' << 1, HIGH);
send_char_NRZI((myssid + '0') << 1, HIGH);
// === PATH DIGI 2 FIELD (WIDE1-1,WIDE2-2) - HANYA BAGIAN INI YANG DIPERBAIKI ===
// Digi 1 (WIDE1-1) - extension bit = 0
temp = strlen(digi1);
for (int j = 0; j < temp; j++) send_char_NRZI(digi1[j] << 1, HIGH);
for (int j = temp; j < 6; j++) send_char_NRZI(' ' << 1, HIGH);
send_char_NRZI((digissid1 + '0') << 1, HIGH);
// Digi 2 (WIDE2-2) - extension bit = 1 (last address)
temp = strlen(digi2);
for (int j = 0; j < temp; j++) send_char_NRZI(digi2[j] << 1, HIGH);
for (int j = temp; j < 6; j++) send_char_NRZI(' ' << 1, HIGH);
send_char_NRZI(((digissid2 + '0') << 1) + 1, HIGH);
// =============================================================================
send_char_NRZI(_CTRL_ID, HIGH);
send_char_NRZI(_PID, HIGH);
}
void send_payload(char type, bool use_dummy = false) {
const char *use_lat = use_dummy ? dummy_lat : lat;
const char *use_lon = use_dummy ? dummy_lon : lon;
if (type == _FIXPOS) {
send_char_NRZI(_DT_POS, HIGH);
send_string_len(use_lat, strlen(use_lat));
send_char_NRZI(sym_ovl, HIGH);
send_string_len(use_lon, strlen(use_lon));
send_char_NRZI(sym_tab, HIGH);
} else if (type == _STATUS) {
send_char_NRZI(_DT_STATUS, HIGH);
send_string_len(mystatus, strlen(mystatus));
} else if (type == _FIXPOS_STATUS) {
send_char_NRZI(_DT_POS, HIGH);
send_string_len(use_lat, strlen(use_lat));
send_char_NRZI(sym_ovl, HIGH);
send_string_len(use_lon, strlen(use_lon));
send_char_NRZI(sym_tab, HIGH);
send_char_NRZI(' ', HIGH);
send_string_len(mystatus, strlen(mystatus));
}
}
void send_char_NRZI(unsigned char in_byte, bool enBitStuff) {
bool bits;
for (int i = 0; i < 8; i++) {
bits = in_byte & 0x01;
calc_crc(bits);
if (bits) {
set_nada(nada);
bit_stuff++;
if (enBitStuff && bit_stuff == 5) {
nada ^= 1;
set_nada(nada);
bit_stuff = 0;
}
} else {
nada ^= 1;
set_nada(nada);
bit_stuff = 0;
}
in_byte >>= 1;
}
}
void send_string_len(const char *in_string, int len) {
for (int j = 0; j < len; j++) send_char_NRZI(in_string[j], HIGH);
}
void send_flag(unsigned char flag_len) {
for (int j = 0; j < flag_len; j++) send_char_NRZI(_FLAG, LOW);
}
void send_packet(char packet_type, char dest_type, bool use_dummy = false) {
print_debug(packet_type, dest_type, use_dummy);
digitalWrite(LED_PIN, HIGH);
send_flag(100);
crc = 0xffff;
send_header(dest_type);
send_payload(packet_type, use_dummy);
send_crc();
send_flag(3);
digitalWrite(LED_PIN, LOW);
}
void print_debug(char type, char dest_type, bool use_dummy) {
Serial.print(mycall);
Serial.print('-');
Serial.print(myssid, DEC);
Serial.print('>');
Serial.print((dest_type == _NORMAL) ? dest : dest_beacon);
Serial.print(',');
// === DEBUG PATH DITAMPILKAN BENAR (WIDE1-1,WIDE2-2) ===
Serial.print(digi1);
Serial.print('-');
Serial.print(digissid1, DEC);
Serial.print(',');
Serial.print(digi2);
Serial.print('-');
Serial.print(digissid2, DEC);
Serial.print(':');
// ====================================================
const char *use_lat = use_dummy ? dummy_lat : lat;
const char *use_lon = use_dummy ? dummy_lon : lon;
if (type == _FIXPOS) {
Serial.print(_DT_POS);
Serial.print(use_lat);
Serial.print(sym_ovl);
Serial.print(use_lon);
Serial.print(sym_tab);
} else if (type == _STATUS) {
Serial.print(_DT_STATUS);
Serial.print(mystatus);
} else if (type == _FIXPOS_STATUS) {
Serial.print(_DT_POS);
Serial.print(use_lat);
Serial.print(sym_ovl);
Serial.print(use_lon);
Serial.print(sym_tab);
Serial.print(' ');
Serial.print(mystatus);
}
Serial.println(' ');
}
void updateCoordinates() {
char lat_str[9];
char lon_str[10];
double lat_val = gps.location.lat();
char lat_dir = (lat_val < 0) ? 'S' : 'N';
lat_val = fabs(lat_val);
int lat_deg = (int)lat_val;
double lat_min = (lat_val - lat_deg) * 60.0;
sprintf(lat_str, "%02d%05.2f%c", lat_deg, lat_min, lat_dir);
double lon_val = gps.location.lng();
char lon_dir = (lon_val < 0) ? 'W' : 'E';
lon_val = fabs(lon_val);
int lon_deg = (int)lon_val;
double lon_min = (lon_val - lon_deg) * 60.0;
sprintf(lon_str, "%03d%05.2f%c", lon_deg, lon_min, lon_dir);
strncpy(lat, lat_str, 8);
lat[8] = '\0';
strncpy(lon, lon_str, 9);
lon[9] = '\0';
Serial.print("Latitude: ");
Serial.println(lat);
Serial.print("Longitude: ");
Serial.println(lon);
}
String getGPSStatus() {
if (gps_locked) {
return "Locked - Ready to Transmit";
} else {
return "Not Locked - Not Ready";
}
}
String getRootHtml() {
String gpsColor = gps_locked ? "green" : "red";
String connColor = (gps_connection_status == "Locked") ? "green" : ((gps_connection_status == "Searching") ? "orange" : "red");
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APRS Tracker By YD2CLX</title>
<style>
body {font-family: Arial, sans-serif; background-color: #f0f4f8; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 100vh; color: #333;}
.container {background-color: #fff; padding: 30px; border-radius: 15px; box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1); max-width: 450px; width: 100%;}
h1 {text-align: center; color: #007bff; margin-bottom: 25px; font-size: 26px;}
label {display: block; margin-bottom: 10px; font-weight: bold; color: #555;}
input[type="text"], input[type="number"], select {width: 100%; padding: 12px; margin-bottom: 20px; border: 1px solid #ddd; border-radius: 8px; box-sizing: border-box; font-size: 16px;}
input[type="checkbox"] {margin-right: 10px;}
.checkbox-label {display: flex; align-items: center; margin-bottom: 20px; font-size: 16px;}
button {width: 100%; padding: 14px; background-color: #28a745; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; margin-top: 10px; transition: background-color 0.3s;}
button:hover {background-color: #218838;}
.status {margin-top: 25px; padding: 15px; background-color: #e9ecef; border-radius: 8px; text-align: center;}
.status p {margin: 8px 0; font-size: 16px;}
#transmitBtn {background-color: #007bff;}
#transmitBtn:hover {background-color: #0056b3;}
#dummyTransmitBtn {background-color: #ffc107;}
#dummyTransmitBtn:hover {background-color: #e0a800;}
#gpsStatus {color: )rawliteral" + gpsColor + R"rawliteral(;}
#gpsConnStatus {color: )rawliteral" + connColor + R"rawliteral(;}
@media (max-width: 600px) {.container {padding: 20px;} h1 {font-size: 22px;}}
</style>
<script>
function manualTransmit() {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/transmit", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
xhr.send();
}
function dummyTransmit() {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/dummy_transmit", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
xhr.send();
}
let refreshTimer;
function startRefresh() {
refreshTimer = setInterval(() => {
if (!document.activeElement ||
(document.activeElement.tagName !== 'INPUT' &&
document.activeElement.tagName !== 'TEXTAREA' &&
document.activeElement.tagName !== 'SELECT')) {
location.reload();
}
}, 5000);
}
document.addEventListener('DOMContentLoaded', () => {
const smartBeacon = document.getElementById('smartbeaconing');
const intervalSelect = document.getElementById('interval');
smartBeacon.addEventListener('change', () => {
intervalSelect.disabled = smartBeacon.checked;
});
startRefresh();
const inputs = document.querySelectorAll('input, select');
inputs.forEach(input => {
input.addEventListener('focus', () => { clearInterval(refreshTimer); });
input.addEventListener('blur', () => { startRefresh(); });
});
});
</script>
</head>
<body>
<div class="container">
<h1>APRS Tracker Configuration</h1>
<form action='/save' method='POST'>
<label for="callsign">Callsign:</label>
<input type="text" id="callsign" name="callsign" value=")rawliteral" + String(mycall) + R"rawliteral(" maxlength="9">
<label for="ssid">SSID:</label>
<input type="number" id="ssid" name="ssid" value=")rawliteral" + String(myssid) + R"rawliteral(" min="0" max="15">
<label for="comment">APRS Status/Comment:</label>
<input type="text" id="comment" name="comment" value=")rawliteral" + String(mystatus) + R"rawliteral(" maxlength="99">
<div class="checkbox-label">
<input type="checkbox" id="smartbeaconing" name="smartbeaconing" )rawliteral" + (smartBeaconing ? "checked" : "") + R"rawliteral(>
<label for="smartbeaconing" style="display: inline; font-weight: normal;">Enable Smart Beaconing</label>
</div>
<label for="interval">Transmit Interval (minutes):</label>
<select id="interval" name="interval" )rawliteral" + (smartBeaconing ? "disabled" : "") + R"rawliteral(>
<option value="1" )rawliteral" + (tx_interval == 60000 ? "selected" : "") + R"rawliteral(>1 Minute</option>
<option value="2" )rawliteral" + (tx_interval == 120000 ? "selected" : "") + R"rawliteral(>2 Minutes</option>
<option value="5" )rawliteral" + (tx_interval == 300000 ? "selected" : "") + R"rawliteral(>5 Minutes</option>
<option value="10" )rawliteral" + (tx_interval == 600000 ? "selected" : "") + R"rawliteral(>10 Minutes</option>
</select>
<button type="submit">Save Settings</button>
</form>
<button id="transmitBtn" onclick="manualTransmit()">Manual Transmit</button>
<button id="dummyTransmitBtn" onclick="dummyTransmit()">TX Dummy Data</button>
<div class="status">
<p>Latitude: <span>)rawliteral" + String(lat) + R"rawliteral(</span></p>
<p>Longitude: <span>)rawliteral" + String(lon) + R"rawliteral(</span></p>
<p>GPS Connection: <span id="gpsConnStatus">)rawliteral" + gps_connection_status + R"rawliteral(</span></p>
<p>GPS Status: <span id="gpsStatus">)rawliteral" + getGPSStatus() + R"rawliteral(</span></p>
</div>
</div>
</body>
</html>
)rawliteral";
return html;
}
void handleRoot() {
server.send(200, "text/html", getRootHtml());
}
void handleSave() {
if (server.hasArg("callsign")) {
String s = server.arg("callsign");
if (s.length() <= 9) {
s.toCharArray(mycall, 10);
mycall[9] = '\0';
Serial.print("New callsign: "); Serial.println(mycall);
}
}
if (server.hasArg("ssid")) {
myssid = server.arg("ssid").toInt();
Serial.print("New SSID: "); Serial.println(myssid);
}
if (server.hasArg("comment")) {
String s = server.arg("comment");
if (s.length() <= 99) {
s.toCharArray(mystatus, 100);
mystatus[99] = '\0';
Serial.print("New status: "); Serial.println(mystatus);
}
}
smartBeaconing = server.hasArg("smartbeaconing");
Serial.print("Smart Beaconing: "); Serial.println(smartBeaconing ? "ON" : "OFF");
if (server.hasArg("interval")) {
int min = server.arg("interval").toInt();
tx_interval = (unsigned long)min * 60000UL;
Serial.print("New interval (ms): "); Serial.println(tx_interval);
}
prefs.begin("aprs_config", false);
prefs.putString("callsign", mycall);
prefs.putUChar("ssid", myssid);
prefs.putString("status", mystatus);
prefs.putBool("smartbeac", smartBeaconing);
prefs.putULong("interval", tx_interval);
prefs.end();
Serial.println("Settings SAVED to Preferences!");
server.sendHeader("Location", "/");
server.send(303);
}
void handleTransmit() {
if (gps_valid) {
send_packet(_FIXPOS_STATUS, _NORMAL);
server.send(200, "text/plain", "Packet transmitted");
} else {
server.send(400, "text/plain", "No valid GPS data");
}
}
void handleDummyTransmit() {
send_packet(_FIXPOS_STATUS, _NORMAL, true);
server.send(200, "text/plain", "Dummy packet transmitted");
}
void setup() {
pinMode(ONBOARD_LED, OUTPUT);
pinMode(OUT_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
Serial.begin(115200);
delay(500);
Serial.println("\nAPRS Tracker ESP32 - Starting... (Path: WIDE1-1,WIDE2-2)");
gpsSerial.begin(9600, SERIAL_8N1, 16, 17);
prefs.begin("aprs_config", false);
String call = prefs.getString("callsign", "YD2CLX");
call.toCharArray(mycall, 10);
myssid = prefs.getUChar("ssid", 7);
String stat = prefs.getString("status", "Tracker Test Over ESP32 - Havid");
stat.toCharArray(mystatus, 100);
smartBeaconing = prefs.getBool("smartbeac", false);
tx_interval = prefs.getULong("interval", 60000UL);
prefs.end();
Serial.print("Loaded callsign: "); Serial.println(mycall);
Serial.print("Loaded SSID: "); Serial.println(myssid);
Serial.print("Loaded status: "); Serial.println(mystatus);
Serial.print("Loaded smartBeaconing: "); Serial.println(smartBeaconing);
Serial.print("Loaded tx_interval: "); Serial.println(tx_interval);
WiFi.softAP("APRS-Tracker", "12345678");
Serial.print("AP IP: "); Serial.println(WiFi.softAPIP());
server.on("/", HTTP_GET, handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.on("/transmit", HTTP_POST, handleTransmit);
server.on("/dummy_transmit", HTTP_POST, handleDummyTransmit);
server.begin();
Serial.println("Web server started");
}
void loop() {
server.handleClient();
bool data_received = false;
while (gpsSerial.available() > 0) {
data_received = true;
gps.encode(gpsSerial.read());
}
if (data_received) {
last_gps_data_time = millis();
}
unsigned long now = millis();
if (now - last_gps_data_time > 2000) {
gps_connection_status = "Not Connected";
} else if (!gps_locked) {
gps_connection_status = "Searching";
} else {
gps_connection_status = "Locked";
}
if (gps.location.isUpdated()) {
updateCoordinates();
}
gps_locked = gps.location.isValid() && gps.satellites.value() >= 3;
gps_valid = gps_locked;
unsigned long effective_interval = tx_interval;
if (smartBeaconing && gps_valid) {
last_speed = gps.speed.kmph();
if (last_speed > 50) effective_interval /= 4;
else if (last_speed > 10) effective_interval /= 2;
if (effective_interval < 30000) effective_interval = 30000;
}
if (gps_valid && (now - last_tx_time >= effective_interval)) {
send_packet(_FIXPOS_STATUS, _NORMAL);
last_tx_time = now;
}
if (now - last_status_print >= 5000) {
Serial.print("GPS Connection Status: "); Serial.println(gps_connection_status);
Serial.print("GPS Lock Status: "); Serial.println(gps_locked ? "Locked" : "Not Locked");
Serial.print("Satellites: "); Serial.println(gps.satellites.value());
last_status_print = now;
}
}
</code></pre>
---- UPDATE 8 April 2026
Penyempurnaan Kode APRS Tracker by YF9UAG
- pindah/geser pin I/O agar pin 2 tidak bentrok dengan LED onboard,
- modif tampilan web,
- Penambahan fitur "channel bussy" pakai sensor speaker out HT ( squelch tidak boleh dibuka full
- Penyempurnaan User Interface
skema channel bussy
SPK radio ----[100k]----+-----> GPIO34
|
[100k]
|
GND
#include <math.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <TinyGPS++.h>
#include <ctype.h>
// ========================= PIN DEFINITIONS =========================
#define AFSK_PIN 25
#define PTT_PIN 26
#define STATUS_LED 2
#define GPS_RX_PIN 16
#define GPS_TX_PIN 17
#define BUSY_IN_PIN 34 // speaker audio detect via ADC
// ========================= APRS / AX.25 ============================
#define AX25_FLAG 0x7E
#define AX25_CTRL 0x03
#define AX25_PID 0xF0
#define DT_STATUS '>'
#define DT_POS '!'
#define PKT_STATUS 1
#define PKT_FIXPOS 2
#define PKT_FIXPOS_STATUS 3
// ========================= MODEM ============================
static bool nrziLevel = 0;
static uint8_t bitStuffCount = 0;
static uint16_t crc = 0xFFFF;
const float baud_adj = 0.985f;
const unsigned int tc1200 = (unsigned int)(0.5f * baud_adj * 1000000.0f / 1200.0f);
const unsigned int tc2200 = (unsigned int)(0.5f * baud_adj * 1000000.0f / 2200.0f);
// ========================= CONFIG ============================
char mycall[7] = "YD2XXX";
uint8_t myssid = 7;
char mystatus[100] = "Tracker Test Over ESP32 - Havid";
bool smartBeaconing = false;
unsigned long tx_interval = 60000UL;
char destCall[7] = "APRS";
uint8_t destSSID = 0;
char digi1[7] = "WIDE1";
uint8_t digissid1 = 1;
char digi2[7] = "WIDE2";
uint8_t digissid2 = 2;
char lat[9] = "0000.00N";
char lon[10] = "00000.00E";
char dummy_lat[9] = "0659.00S";
char dummy_lon[10] = "10649.00E";
char sym_ovl = '/';
char sym_tab = 'k';
// ========================= BUSY DETECT FROM SPEAKER AUDIO =========================
// 0 = OFF
// 1 = AUDIO SPEAKER DETECT (ADC)
uint8_t busyMode = 1;
// Threshold deviasi ADC terhadap center
uint16_t busyAdcThreshold = 140;
// durasi sampling audio untuk memutuskan BUSY
unsigned long busySampleWindowMs = 25;
// berapa lama status busy ditahan setelah burst audio hilang
unsigned long busyHangMs = 220;
// channel harus clear stabil selama ini sebelum TX
unsigned long busyClearHoldMs = 300;
// retry TX saat busy
unsigned long busyRetryMs = 5000;
// batas tunggu maksimum sebelum skip TX
unsigned long busyMaxWaitMs = 30000UL;
// jumlah sample untuk kalibrasi center ADC
uint16_t busyCenterCalSamples = 400;
// interval rekalibrasi center ketika channel clear
unsigned long busyRecalMs = 15000UL;
// ========================= RUNTIME ============================
bool gps_valid = false;
bool gps_locked = false;
float last_speed = 0.0f;
unsigned long last_tx_time = 0;
unsigned long pending_tx_since = 0;
bool pending_auto_tx = false;
bool pending_dummy_tx = false;
bool pending_manual_tx = false;
String gps_connection_status = "Not Connected";
String channel_busy_status = "UNKNOWN";
unsigned long last_gps_data_time = 0;
unsigned long last_status_print = 0;
unsigned long last_busy_retry_time = 0;
unsigned long lastBusyDetectedMs = 0;
unsigned long lastBusyCalMs = 0;
uint16_t busyAdcCenter = 2048;
uint16_t lastBusyPeak = 0;
// ========================= OBJECTS ============================
TinyGPSPlus gps;
HardwareSerial gpsSerial(1);
WebServer server(80);
Preferences prefs;
// ========================= UTIL ============================
void safeCopyUpper(char *dst, size_t dstSize, const String &src, size_t maxLen) {
String s = src;
s.trim();
s.toUpperCase();
if (s.length() > maxLen) s = s.substring(0, maxLen);
s.toCharArray(dst, dstSize);
dst[dstSize - 1] = '\0';
}
bool isValidBaseCall(const String &in) {
if (in.length() < 1 || in.length() > 6) return false;
for (size_t i = 0; i < in.length(); i++) {
char c = in.charAt(i);
if (!(isalnum((unsigned char)c))) return false;
}
return true;
}
bool isValidSSID(int v) {
return v >= 0 && v <= 15;
}
bool isValidSymbolChar(char c) {
return c >= 33 && c <= 126;
}
unsigned long getEffectiveInterval() {
unsigned long effective_interval = tx_interval;
if (smartBeaconing && gps_valid && gps.speed.isValid()) {
last_speed = gps.speed.kmph();
if (last_speed > 70.0f) effective_interval = tx_interval / 4;
else if (last_speed > 30.0f) effective_interval = tx_interval / 2;
else if (last_speed < 5.0f) effective_interval = tx_interval;
if (effective_interval < 30000UL) effective_interval = 30000UL;
}
return effective_interval;
}
// ========================= AFSK TONES ============================
void tone1200() {
digitalWrite(AFSK_PIN, HIGH);
delayMicroseconds(tc1200);
digitalWrite(AFSK_PIN, LOW);
delayMicroseconds(tc1200);
}
void tone2200() {
digitalWrite(AFSK_PIN, HIGH);
delayMicroseconds(tc2200);
digitalWrite(AFSK_PIN, LOW);
delayMicroseconds(tc2200);
digitalWrite(AFSK_PIN, HIGH);
delayMicroseconds(tc2200);
digitalWrite(AFSK_PIN, LOW);
delayMicroseconds(tc2200);
}
void sendTone(bool level) {
if (level) tone1200();
else tone2200();
}
// ========================= CRC ============================
void crcReset() {
crc = 0xFFFF;
}
void crcUpdate(bool bitIn) {
unsigned short xor_in = crc ^ bitIn;
crc >>= 1;
if (xor_in & 0x01) crc ^= 0x8408;
}
void sendRawBit(bool bitVal, bool updateCrc, bool enableBitStuff) {
if (updateCrc) crcUpdate(bitVal);
if (bitVal) {
sendTone(nrziLevel);
if (enableBitStuff) {
bitStuffCount++;
if (bitStuffCount == 5) {
nrziLevel ^= 1;
sendTone(nrziLevel);
bitStuffCount = 0;
}
}
} else {
nrziLevel ^= 1;
sendTone(nrziLevel);
bitStuffCount = 0;
}
}
void sendByteNRZI(uint8_t b, bool updateCrc, bool enableBitStuff) {
for (int i = 0; i < 8; i++) {
bool bitVal = b & 0x01;
sendRawBit(bitVal, updateCrc, enableBitStuff);
b >>= 1;
}
}
void sendFlag(uint8_t count) {
for (uint8_t i = 0; i < count; i++) {
sendByteNRZI(AX25_FLAG, false, false);
}
}
void sendCRC() {
uint8_t crc_lo = crc ^ 0xFF;
uint8_t crc_hi = (crc >> 8) ^ 0xFF;
sendByteNRZI(crc_lo, false, true);
sendByteNRZI(crc_hi, false, true);
}
// ========================= AX.25 ADDRESS ============================
void sendAX25Address(const char *call, uint8_t ssid, bool last) {
char padded[7] = {' ', ' ', ' ', ' ', ' ', ' ', '\0'};
size_t len = strlen(call);
if (len > 6) len = 6;
for (size_t i = 0; i < len; i++) padded[i] = toupper((unsigned char)call[i]);
for (int i = 0; i < 6; i++) {
sendByteNRZI(((uint8_t)padded[i]) << 1, true, true);
}
uint8_t ssidByte = 0x60 | ((ssid & 0x0F) << 1);
if (last) ssidByte |= 0x01;
sendByteNRZI(ssidByte, true, true);
}
void sendHeader() {
sendAX25Address(destCall, destSSID, false);
sendAX25Address(mycall, myssid, false);
sendAX25Address(digi1, digissid1, false);
sendAX25Address(digi2, digissid2, true);
sendByteNRZI(AX25_CTRL, true, true);
sendByteNRZI(AX25_PID, true, true);
}
// ========================= PAYLOAD ============================
void sendStringAX25(const char *s) {
while (*s) sendByteNRZI((uint8_t)*s++, true, true);
}
void sendPayload(uint8_t type, bool useDummy = false) {
const char *useLat = useDummy ? dummy_lat : lat;
const char *useLon = useDummy ? dummy_lon : lon;
if (type == PKT_FIXPOS) {
sendByteNRZI(DT_POS, true, true);
sendStringAX25(useLat);
sendByteNRZI(sym_ovl, true, true);
sendStringAX25(useLon);
sendByteNRZI(sym_tab, true, true);
} else if (type == PKT_STATUS) {
sendByteNRZI(DT_STATUS, true, true);
sendStringAX25(mystatus);
} else if (type == PKT_FIXPOS_STATUS) {
sendByteNRZI(DT_POS, true, true);
sendStringAX25(useLat);
sendByteNRZI(sym_ovl, true, true);
sendStringAX25(useLon);
sendByteNRZI(sym_tab, true, true);
sendByteNRZI(' ', true, true);
sendStringAX25(mystatus);
}
}
// ========================= DEBUG ============================
void printDebug(uint8_t type, bool useDummy = false) {
const char *useLat = useDummy ? dummy_lat : lat;
const char *useLon = useDummy ? dummy_lon : lon;
Serial.print(mycall);
Serial.print("-");
Serial.print(myssid);
Serial.print(">");
Serial.print(destCall);
Serial.print(",");
Serial.print(digi1);
Serial.print("-");
Serial.print(digissid1);
Serial.print(",");
Serial.print(digi2);
Serial.print("-");
Serial.print(digissid2);
Serial.print(":");
if (type == PKT_FIXPOS) {
Serial.print("!");
Serial.print(useLat);
Serial.print(sym_ovl);
Serial.print(useLon);
Serial.print(sym_tab);
} else if (type == PKT_STATUS) {
Serial.print(">");
Serial.print(mystatus);
} else if (type == PKT_FIXPOS_STATUS) {
Serial.print("!");
Serial.print(useLat);
Serial.print(sym_ovl);
Serial.print(useLon);
Serial.print(sym_tab);
Serial.print(" ");
Serial.print(mystatus);
}
Serial.println();
}
// ========================= BUSY DETECT FROM SPEAKER AUDIO ============================
uint16_t readBusyPeak(unsigned long windowMs) {
unsigned long start = millis();
uint16_t peak = 0;
while (millis() - start < windowMs) {
int v = analogRead(BUSY_IN_PIN);
int dev = abs(v - (int)busyAdcCenter);
if ((uint16_t)dev > peak) peak = (uint16_t)dev;
delayMicroseconds(200);
}
return peak;
}
void calibrateBusyCenter(uint16_t samples = 400) {
uint32_t sum = 0;
for (uint16_t i = 0; i < samples; i++) {
sum += analogRead(BUSY_IN_PIN);
delayMicroseconds(200);
}
busyAdcCenter = (uint16_t)(sum / samples);
}
bool isBusyAudioRaw() {
lastBusyPeak = readBusyPeak(busySampleWindowMs);
if (lastBusyPeak >= busyAdcThreshold) {
lastBusyDetectedMs = millis();
return true;
}
return false;
}
bool isChannelBusyRaw() {
if (busyMode == 0) return false;
if (isBusyAudioRaw()) return true;
// hold/hang time supaya burst tidak langsung dianggap clear
if (millis() - lastBusyDetectedMs < busyHangMs) return true;
return false;
}
bool isChannelClearStable(unsigned long clearHoldMs) {
unsigned long start = millis();
while (millis() - start < clearHoldMs) {
if (isChannelBusyRaw()) return false;
delay(5);
}
return true;
}
bool canTransmitNow() {
if (busyMode == 0) return true;
if (isChannelBusyRaw()) return false;
return isChannelClearStable(busyClearHoldMs);
}
void updateBusyStatusText() {
if (busyMode == 0) {
channel_busy_status = "DISABLED";
return;
}
bool busy = isChannelBusyRaw();
if (busy) channel_busy_status = "BUSY";
else channel_busy_status = "CLEAR";
}
// ========================= TX ============================
void beginTX() {
digitalWrite(STATUS_LED, HIGH);
digitalWrite(PTT_PIN, HIGH);
delay(120); // PTT attack delay
}
void endTX() {
delay(50); // tail delay
digitalWrite(PTT_PIN, LOW);
digitalWrite(STATUS_LED, LOW);
}
void sendPacket(uint8_t packetType, bool useDummy = false) {
printDebug(packetType, useDummy);
bitStuffCount = 0;
nrziLevel = 0;
beginTX();
sendFlag(80); // precarrier
crcReset();
sendHeader();
sendPayload(packetType, useDummy);
sendCRC();
sendFlag(3); // trailer
endTX();
}
// ========================= GPS ============================
void updateCoordinates() {
double lat_val = gps.location.lat();
double lon_val = gps.location.lng();
char lat_dir = (lat_val < 0) ? 'S' : 'N';
char lon_dir = (lon_val < 0) ? 'W' : 'E';
lat_val = fabs(lat_val);
lon_val = fabs(lon_val);
int lat_deg = (int)lat_val;
int lon_deg = (int)lon_val;
double lat_min = (lat_val - lat_deg) * 60.0;
double lon_min = (lon_val - lon_deg) * 60.0;
snprintf(lat, sizeof(lat), "%02d%05.2f%c", lat_deg, lat_min, lat_dir);
snprintf(lon, sizeof(lon), "%03d%05.2f%c", lon_deg, lon_min, lon_dir);
}
String getGPSStatus() {
if (gps_locked) return "Locked - Ready to Transmit";
return "Not Locked - Not Ready";
}
// ========================= PREFERENCES ============================
void savePrefs() {
prefs.begin("aprs_config", false);
prefs.putString("callsign", mycall);
prefs.putUChar("ssid", myssid);
prefs.putString("status", mystatus);
prefs.putBool("smartbeac", smartBeaconing);
prefs.putULong("interval", tx_interval);
prefs.putString("digi1", digi1);
prefs.putUChar("digissid1", digissid1);
prefs.putString("digi2", digi2);
prefs.putUChar("digissid2", digissid2);
prefs.putChar("symtab", sym_tab);
prefs.putChar("symovl", sym_ovl);
prefs.putUChar("busymode", busyMode);
prefs.putUShort("busythr", busyAdcThreshold);
prefs.putULong("busywin", busySampleWindowMs);
prefs.putULong("busyhang", busyHangMs);
prefs.putULong("busyhold", busyClearHoldMs);
prefs.putULong("busyretry", busyRetryMs);
prefs.putULong("busymaxw", busyMaxWaitMs);
prefs.putUShort("busycents", busyCenterCalSamples);
prefs.putULong("busyrecal", busyRecalMs);
prefs.end();
}
void loadPrefs() {
prefs.begin("aprs_config", true);
String call = prefs.getString("callsign", "YD2XXX");
safeCopyUpper(mycall, sizeof(mycall), call, 6);
myssid = prefs.getUChar("ssid", 7);
String stat = prefs.getString("status", "Tracker Test Over ESP32 - Havid");
stat.toCharArray(mystatus, sizeof(mystatus));
mystatus[sizeof(mystatus) - 1] = '\0';
smartBeaconing = prefs.getBool("smartbeac", false);
tx_interval = prefs.getULong("interval", 60000UL);
String d1 = prefs.getString("digi1", "WIDE1");
String d2 = prefs.getString("digi2", "WIDE2");
safeCopyUpper(digi1, sizeof(digi1), d1, 6);
safeCopyUpper(digi2, sizeof(digi2), d2, 6);
digissid1 = prefs.getUChar("digissid1", 1);
digissid2 = prefs.getUChar("digissid2", 2);
sym_tab = prefs.getChar("symtab", 'k');
sym_ovl = prefs.getChar("symovl", '/');
busyMode = prefs.getUChar("busymode", 1);
if (busyMode > 1) busyMode = 1;
busyAdcThreshold = prefs.getUShort("busythr", 140);
busySampleWindowMs = prefs.getULong("busywin", 25);
busyHangMs = prefs.getULong("busyhang", 220);
busyClearHoldMs = prefs.getULong("busyhold", 300);
busyRetryMs = prefs.getULong("busyretry", 5000);
busyMaxWaitMs = prefs.getULong("busymaxw", 30000UL);
busyCenterCalSamples = prefs.getUShort("busycents", 400);
busyRecalMs = prefs.getULong("busyrecal", 15000UL);
prefs.end();
}
// ========================= WEB ============================
String getRootHtml() {
String gpsColor = gps_locked ? "green" : "red";
String connColor = (gps_connection_status == "Locked") ? "green" :
(gps_connection_status == "Searching") ? "orange" : "red";
String busyColor = (channel_busy_status == "BUSY") ? "red" :
(channel_busy_status == "CLEAR") ? "green" : "gray";
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>APRS Tracker ESP32</title>
<style>
body{font-family:Arial,sans-serif;background:#eef2f7;margin:0;padding:20px;color:#222}
.card{max-width:920px;margin:auto;background:#fff;border-radius:16px;padding:22px;box-shadow:0 8px 24px rgba(0,0,0,.08)}
h1{margin-top:0;color:#0b63ce}
h2{margin-top:24px;color:#0f172a;font-size:18px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:700px){.grid{grid-template-columns:1fr}}
label{display:block;font-weight:bold;margin:10px 0 6px}
input,select{width:100%;padding:11px;border:1px solid #d0d7e2;border-radius:10px;box-sizing:border-box}
button{padding:12px 16px;border:0;border-radius:10px;color:#fff;cursor:pointer;margin-top:12px;font-weight:bold}
.save{background:#16a34a}.tx{background:#2563eb}.dummy{background:#d97706}
.status{margin-top:18px;padding:16px;border-radius:12px;background:#f6f8fb}
small{color:#667;display:block;margin-top:4px}
</style>
<script>
function postAction(url){
fetch(url,{method:'POST'})
.then(r=>r.text().then(t=>({ok:r.ok,text:t})))
.then(x=>alert(x.text))
.catch(e=>alert('Error: '+e));
}
setInterval(()=>{
const ae=document.activeElement;
if(!ae || (ae.tagName!=='INPUT' && ae.tagName!=='SELECT' && ae.tagName!=='TEXTAREA')){
location.reload();
}
},5000);
</script>
</head>
<body>
<div class="card">
<h1>APRS Tracker ESP32</h1>
<form action="/save" method="POST">
<h2>APRS Settings</h2>
<div class="grid">
<div>
<label>Callsign Base</label>
<input name="callsign" maxlength="6" value=")rawliteral" + String(mycall) + R"rawliteral(">
</div>
<div>
<label>SSID</label>
<input name="ssid" type="number" min="0" max="15" value=")rawliteral" + String(myssid) + R"rawliteral(">
</div>
<div>
<label>Status / Comment</label>
<input name="comment" maxlength="99" value=")rawliteral" + String(mystatus) + R"rawliteral(">
</div>
<div>
<label>Interval (minutes)</label>
<select name="interval">
<option value="1" )rawliteral" + String(tx_interval == 60000UL ? "selected" : "") + R"rawliteral(>1</option>
<option value="2" )rawliteral" + String(tx_interval == 120000UL ? "selected" : "") + R"rawliteral(>2</option>
<option value="5" )rawliteral" + String(tx_interval == 300000UL ? "selected" : "") + R"rawliteral(>5</option>
<option value="10" )rawliteral" + String(tx_interval == 600000UL ? "selected" : "") + R"rawliteral(>10</option>
<option value="15" )rawliteral" + String(tx_interval == 900000UL ? "selected" : "") + R"rawliteral(>15</option>
</select>
</div>
<div>
<label>Digi 1</label>
<input name="digi1" maxlength="6" value=")rawliteral" + String(digi1) + R"rawliteral(">
</div>
<div>
<label>Digi1 SSID</label>
<input name="digissid1" type="number" min="0" max="15" value=")rawliteral" + String(digissid1) + R"rawliteral(">
</div>
<div>
<label>Digi 2</label>
<input name="digi2" maxlength="6" value=")rawliteral" + String(digi2) + R"rawliteral(">
</div>
<div>
<label>Digi2 SSID</label>
<input name="digissid2" type="number" min="0" max="15" value=")rawliteral" + String(digissid2) + R"rawliteral(">
</div>
<div>
<label>Symbol Table</label>
<input name="symtab" maxlength="1" value=")rawliteral" + String(sym_tab) + R"rawliteral(">
</div>
<div>
<label>Symbol Overlay / Table ID</label>
<input name="symovl" maxlength="1" value=")rawliteral" + String(sym_ovl) + R"rawliteral(">
</div>
</div>
<label style="margin-top:12px">
<input type="checkbox" name="smartbeaconing" )rawliteral" + String(smartBeaconing ? "checked" : "") + R"rawliteral(>
Enable Smart Beaconing
</label>
<h2>Channel Busy dari Speaker Radio</h2>
<div class="grid">
<div>
<label>Busy Mode</label>
<select name="busymode">
<option value="0" )rawliteral" + String(busyMode == 0 ? "selected" : "") + R"rawliteral(>OFF</option>
<option value="1" )rawliteral" + String(busyMode == 1 ? "selected" : "") + R"rawliteral(>Speaker Audio Detect</option>
</select>
</div>
<div>
<label>ADC Threshold</label>
<input name="busythr" type="number" min="10" max="2000" value=")rawliteral" + String(busyAdcThreshold) + R"rawliteral(">
<small>Naikkan jika terlalu sensitif, turunkan jika tidak mendeteksi burst APRS</small>
</div>
<div>
<label>Sample Window (ms)</label>
<input name="busywin" type="number" min="5" max="200" value=")rawliteral" + String(busySampleWindowMs) + R"rawliteral(">
</div>
<div>
<label>Busy Hang (ms)</label>
<input name="busyhang" type="number" min="20" max="3000" value=")rawliteral" + String(busyHangMs) + R"rawliteral(">
</div>
<div>
<label>Clear Hold (ms)</label>
<input name="busyhold" type="number" min="50" max="5000" value=")rawliteral" + String(busyClearHoldMs) + R"rawliteral(">
</div>
<div>
<label>Retry Delay (ms)</label>
<input name="busyretry" type="number" min="500" max="60000" value=")rawliteral" + String(busyRetryMs) + R"rawliteral(">
</div>
<div>
<label>Max Wait Before Skip TX (ms)</label>
<input name="busymaxw" type="number" min="1000" max="300000" value=")rawliteral" + String(busyMaxWaitMs) + R"rawliteral(">
</div>
<div>
<label>Center Calibration Samples</label>
<input name="busycents" type="number" min="50" max="3000" value=")rawliteral" + String(busyCenterCalSamples) + R"rawliteral(">
</div>
<div>
<label>Recalibration Interval (ms)</label>
<input name="busyrecal" type="number" min="1000" max="120000" value=")rawliteral" + String(busyRecalMs) + R"rawliteral(">
</div>
</div>
<button class="save" type="submit">Save Settings</button>
</form>
<button class="tx" onclick="postAction('/transmit')">Manual Transmit</button>
<button class="dummy" onclick="postAction('/dummy_transmit')">TX Dummy Data</button>
<div class="status">
<p><b>Latitude:</b> )rawliteral" + String(lat) + R"rawliteral(</p>
<p><b>Longitude:</b> )rawliteral" + String(lon) + R"rawliteral(</p>
<p><b>GPS Connection:</b> <span style="color:)rawliteral" + connColor + R"rawliteral(;">)rawliteral" + gps_connection_status + R"rawliteral(</span></p>
<p><b>GPS Status:</b> <span style="color:)rawliteral" + gpsColor + R"rawliteral(;">)rawliteral" + getGPSStatus() + R"rawliteral(</span></p>
<p><b>Channel Busy:</b> <span style="color:)rawliteral" + busyColor + R"rawliteral(;">)rawliteral" + channel_busy_status + R"rawliteral(</span></p>
<p><b>ADC Center:</b> )rawliteral" + String(busyAdcCenter) + R"rawliteral(</p>
<p><b>Last Peak:</b> )rawliteral" + String(lastBusyPeak) + R"rawliteral(</p>
<p><b>Path:</b> )rawliteral" + String(digi1) + "-" + String(digissid1) + "," + String(digi2) + "-" + String(digissid2) + R"rawliteral(</p>
</div>
</div>
</body>
</html>
)rawliteral";
return html;
}
void handleRoot() {
updateBusyStatusText();
server.send(200, "text/html", getRootHtml());
}
void handleSave() {
if (server.hasArg("callsign")) {
String s = server.arg("callsign");
s.trim();
s.toUpperCase();
if (isValidBaseCall(s)) safeCopyUpper(mycall, sizeof(mycall), s, 6);
}
if (server.hasArg("ssid")) {
int v = server.arg("ssid").toInt();
if (isValidSSID(v)) myssid = v;
}
if (server.hasArg("comment")) {
String s = server.arg("comment");
if (s.length() > 99) s = s.substring(0, 99);
s.toCharArray(mystatus, sizeof(mystatus));
mystatus[sizeof(mystatus) - 1] = '\0';
}
if (server.hasArg("interval")) {
int minVal = server.arg("interval").toInt();
if (minVal >= 1 && minVal <= 60) tx_interval = (unsigned long)minVal * 60000UL;
}
if (server.hasArg("digi1")) {
String s = server.arg("digi1");
s.trim();
s.toUpperCase();
if (isValidBaseCall(s)) safeCopyUpper(digi1, sizeof(digi1), s, 6);
}
if (server.hasArg("digissid1")) {
int v = server.arg("digissid1").toInt();
if (isValidSSID(v)) digissid1 = v;
}
if (server.hasArg("digi2")) {
String s = server.arg("digi2");
s.trim();
s.toUpperCase();
if (isValidBaseCall(s)) safeCopyUpper(digi2, sizeof(digi2), s, 6);
}
if (server.hasArg("digissid2")) {
int v = server.arg("digissid2").toInt();
if (isValidSSID(v)) digissid2 = v;
}
if (server.hasArg("symtab")) {
String s = server.arg("symtab");
if (s.length() >= 1 && isValidSymbolChar(s.charAt(0))) sym_tab = s.charAt(0);
}
if (server.hasArg("symovl")) {
String s = server.arg("symovl");
if (s.length() >= 1 && isValidSymbolChar(s.charAt(0))) sym_ovl = s.charAt(0);
}
smartBeaconing = server.hasArg("smartbeaconing");
if (server.hasArg("busymode")) {
int v = server.arg("busymode").toInt();
if (v >= 0 && v <= 1) busyMode = v;
}
if (server.hasArg("busythr")) {
int v = server.arg("busythr").toInt();
if (v >= 10 && v <= 2000) busyAdcThreshold = v;
}
if (server.hasArg("busywin")) {
unsigned long v = strtoul(server.arg("busywin").c_str(), nullptr, 10);
if (v >= 5 && v <= 200) busySampleWindowMs = v;
}
if (server.hasArg("busyhang")) {
unsigned long v = strtoul(server.arg("busyhang").c_str(), nullptr, 10);
if (v >= 20 && v <= 3000) busyHangMs = v;
}
if (server.hasArg("busyhold")) {
unsigned long v = strtoul(server.arg("busyhold").c_str(), nullptr, 10);
if (v >= 50 && v <= 5000) busyClearHoldMs = v;
}
if (server.hasArg("busyretry")) {
unsigned long v = strtoul(server.arg("busyretry").c_str(), nullptr, 10);
if (v >= 500 && v <= 60000) busyRetryMs = v;
}
if (server.hasArg("busymaxw")) {
unsigned long v = strtoul(server.arg("busymaxw").c_str(), nullptr, 10);
if (v >= 1000 && v <= 300000) busyMaxWaitMs = v;
}
if (server.hasArg("busycents")) {
int v = server.arg("busycents").toInt();
if (v >= 50 && v <= 3000) busyCenterCalSamples = v;
}
if (server.hasArg("busyrecal")) {
unsigned long v = strtoul(server.arg("busyrecal").c_str(), nullptr, 10);
if (v >= 1000 && v <= 120000) busyRecalMs = v;
}
savePrefs();
calibrateBusyCenter(busyCenterCalSamples);
server.sendHeader("Location", "/");
server.send(303);
}
void handleTransmit() {
if (!gps_valid) {
server.send(400, "text/plain", "No valid GPS data");
return;
}
if (canTransmitNow()) {
sendPacket(PKT_FIXPOS_STATUS, false);
last_tx_time = millis();
pending_manual_tx = false;
server.send(200, "text/plain", "Packet transmitted");
} else {
pending_manual_tx = true;
pending_tx_since = millis();
last_busy_retry_time = 0;
server.send(200, "text/plain", "Channel busy from speaker audio, manual TX queued");
}
}
void handleDummyTransmit() {
if (canTransmitNow()) {
sendPacket(PKT_FIXPOS_STATUS, true);
last_tx_time = millis();
pending_dummy_tx = false;
server.send(200, "text/plain", "Dummy packet transmitted");
} else {
pending_dummy_tx = true;
pending_tx_since = millis();
last_busy_retry_time = 0;
server.send(200, "text/plain", "Channel busy from speaker audio, dummy TX queued");
}
}
// ========================= SETUP ============================
void setup() {
pinMode(AFSK_PIN, OUTPUT);
pinMode(PTT_PIN, OUTPUT);
pinMode(STATUS_LED, OUTPUT);
pinMode(BUSY_IN_PIN, INPUT);
analogReadResolution(12);
analogSetPinAttenuation(BUSY_IN_PIN, ADC_11db);
digitalWrite(AFSK_PIN, LOW);
digitalWrite(PTT_PIN, LOW);
digitalWrite(STATUS_LED, LOW);
Serial.begin(115200);
delay(500);
Serial.println();
Serial.println("APRS Tracker ESP32 starting with speaker-audio busy detect...");
gpsSerial.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
loadPrefs();
calibrateBusyCenter(busyCenterCalSamples);
lastBusyCalMs = millis();
Serial.print("Callsign: "); Serial.println(mycall);
Serial.print("SSID: "); Serial.println(myssid);
Serial.print("Status: "); Serial.println(mystatus);
Serial.print("Path: "); Serial.print(digi1); Serial.print("-"); Serial.print(digissid1);
Serial.print(","); Serial.print(digi2); Serial.print("-"); Serial.println(digissid2);
Serial.print("Busy Mode: "); Serial.println(busyMode);
Serial.print("Busy Threshold: "); Serial.println(busyAdcThreshold);
Serial.print("Busy ADC Center: "); Serial.println(busyAdcCenter);
WiFi.softAP("APRS-Tracker", "12345678");
Serial.print("AP IP: ");
Serial.println(WiFi.softAPIP());
server.on("/", HTTP_GET, handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.on("/transmit", HTTP_POST, handleTransmit);
server.on("/dummy_transmit", HTTP_POST, handleDummyTransmit);
server.begin();
Serial.println("Web server started");
}
// ========================= LOOP ============================
void loop() {
server.handleClient();
bool data_received = false;
while (gpsSerial.available() > 0) {
data_received = true;
gps.encode(gpsSerial.read());
}
unsigned long now = millis();
if (data_received) last_gps_data_time = now;
if (now - last_gps_data_time > 3000) {
gps_connection_status = "Not Connected";
} else if (!gps_locked) {
gps_connection_status = "Searching";
} else {
gps_connection_status = "Locked";
}
gps_locked = gps.location.isValid() && gps.satellites.isValid() && gps.satellites.value() >= 3;
gps_valid = gps_locked;
if (gps.location.isUpdated() && gps.location.isValid()) {
updateCoordinates();
}
// Recalibrate center only when clear and not transmitting/queueing
if (busyMode == 1 && (now - lastBusyCalMs >= busyRecalMs)) {
bool noPending = !(pending_manual_tx || pending_dummy_tx || pending_auto_tx);
if (noPending && !isChannelBusyRaw()) {
calibrateBusyCenter(busyCenterCalSamples);
lastBusyCalMs = now;
}
}
updateBusyStatusText();
// ================= AUTO TX WITH CHANNEL BUSY =================
unsigned long effective_interval = getEffectiveInterval();
if (gps_valid && !pending_auto_tx && (now - last_tx_time >= effective_interval)) {
pending_auto_tx = true;
pending_tx_since = now;
last_busy_retry_time = 0;
}
// ================= PROCESS QUEUED TX =================
bool havePending = pending_manual_tx || pending_dummy_tx || pending_auto_tx;
if (havePending) {
if (canTransmitNow()) {
if (pending_dummy_tx) {
sendPacket(PKT_FIXPOS_STATUS, true);
pending_dummy_tx = false;
} else if (pending_manual_tx) {
sendPacket(PKT_FIXPOS_STATUS, false);
pending_manual_tx = false;
} else if (pending_auto_tx && gps_valid) {
sendPacket(PKT_FIXPOS_STATUS, false);
pending_auto_tx = false;
}
last_tx_time = millis();
pending_tx_since = 0;
last_busy_retry_time = 0;
} else {
if (now - pending_tx_since >= busyMaxWaitMs) {
Serial.println("TX skipped: channel busy too long");
pending_manual_tx = false;
pending_dummy_tx = false;
pending_auto_tx = false;
pending_tx_since = 0;
last_busy_retry_time = 0;
last_tx_time = now;
} else if (now - last_busy_retry_time >= busyRetryMs) {
Serial.print("Channel busy from speaker audio, peak=");
Serial.print(lastBusyPeak);
Serial.print(", center=");
Serial.println(busyAdcCenter);
last_busy_retry_time = now;
}
}
}
// ================= STATUS PRINT =================
if (now - last_status_print >= 5000) {
Serial.print("GPS Connection: ");
Serial.println(gps_connection_status);
Serial.print("GPS Locked: ");
Serial.println(gps_locked ? "YES" : "NO");
Serial.print("Satellites: ");
if (gps.satellites.isValid()) Serial.println(gps.satellites.value());
else Serial.println("N/A");
Serial.print("Lat/Lon: ");
Serial.print(lat);
Serial.print(" / ");
Serial.println(lon);
Serial.print("Channel Busy Status: ");
Serial.println(channel_busy_status);
Serial.print("ADC Center: ");
Serial.println(busyAdcCenter);
Serial.print("Last Peak: ");
Serial.println(lastBusyPeak);
last_status_print = now;
}
}
</code></pre>
