<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>
