Skip to content

Commit 4cb6c4c

Browse files
committed
Release v0.1.11: Stabilize xOTA memory, fix LED telemetry, and add synchronous UI loading screens
1 parent f5bb7b9 commit 4cb6c4c

13 files changed

Lines changed: 460 additions & 331 deletions

release/firmware.bin

-3.22 KB
Binary file not shown.

src/core/metadata.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace meta {
44

55
// System Branding & Platform Identity
66
constexpr const char* FW_NAME = "QRPickle";
7-
constexpr const char* FW_VERSION = "v0.1.10-dev";
7+
constexpr const char* FW_VERSION = "v0.1.11";
88
constexpr const char* HARDWARE_PLAT = "ESP32 CYD (2.8\" TFT)";
99

1010
// Project Ownership & Operational Credits

src/hw/led_rgb.cpp

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#include "led_rgb.h"
2-
#include "../services/display_manager.h" // Needed to check screen sleep status
2+
#include "../services/display_manager.h"
33

44
#define LED_PIN_R 4
55
#define LED_PIN_G 16
@@ -16,9 +16,6 @@ namespace hw {
1616
static unsigned long priority_strobe_start = 0;
1717

1818
static void write_raw_rgb(uint8_t r, uint8_t g, uint8_t b) {
19-
// FIXED: Do not mix pinMode/digitalWrite with analogWrite on the ESP32.
20-
// The LEDC hardware timer will not release the pin, causing permanent phantom glowing.
21-
// Driving a common-anode LED with a 255 duty cycle cleanly shuts off the voltage differential.
2219
analogWrite(LED_PIN_R, 255 - r);
2320
analogWrite(LED_PIN_G, 255 - g);
2421
analogWrite(LED_PIN_B, 255 - b);
@@ -28,28 +25,20 @@ namespace hw {
2825
while (true) {
2926
unsigned long now = millis();
3027

31-
// ----------------------------------------------------
32-
// OVERRIDE 1: High Priority Inbound Strobe (APRS / HamAlert)
33-
// ----------------------------------------------------
28+
// Priority Inbound Strobe (APRS / HamAlert) - PRESERVED
3429
if (priority_strobe_active) {
35-
if (now - priority_strobe_start > 3000) {
30+
if (now - priority_strobe_start > 3000) {
3631
priority_strobe_active = false;
3732
} else {
38-
if ((now / 125) % 2 == 0) {
39-
write_raw_rgb(120, 120, 120);
40-
} else {
41-
write_raw_rgb(150, 0, 150);
42-
}
33+
if ((now / 125) % 2 == 0) write_raw_rgb(120, 120, 120);
34+
else write_raw_rgb(150, 0, 150);
4335
vTaskDelay(20 / portTICK_PERIOD_MS);
4436
continue;
4537
}
4638
}
4739

48-
// ----------------------------------------------------
49-
// OVERRIDE 2: Crisp 30ms Data Ingress Pulse
50-
// ----------------------------------------------------
5140
if (traffic_pulse_active) {
52-
if (now - traffic_pulse_start > 30) {
41+
if (now - traffic_pulse_start > 30) {
5342
traffic_pulse_active = false;
5443
} else {
5544
write_raw_rgb(0, 40, 60);
@@ -58,9 +47,6 @@ namespace hw {
5847
}
5948
}
6049

61-
// ----------------------------------------------------
62-
// CORE STATE MACHINE PROCESSING
63-
// ----------------------------------------------------
6450
switch (current_state) {
6551
case STATE_OFF:
6652
write_raw_rgb(0, 0, 0);
@@ -77,36 +63,29 @@ namespace hw {
7763
vTaskDelay(50 / portTICK_PERIOD_MS);
7864
break;
7965

80-
case STATE_BOOT_SYNC: {
81-
float phase = (float)(now % 2000) / 2000.0f * 2.0f * PI;
82-
uint8_t val = 10 + (uint8_t)((sin(phase) + 1.0f) * 55.0f);
83-
write_raw_rgb(0, val, val);
84-
vTaskDelay(20 / portTICK_PERIOD_MS);
66+
case STATE_BOOT_SYNC:
67+
// FIXED: Neutered the Cyan breathing loop triggered by the timekeeper!
68+
write_raw_rgb(0, 0, 0);
69+
vTaskDelay(100 / portTICK_PERIOD_MS);
8570
break;
86-
}
8771

8872
case STATE_BOOT_READY:
8973
write_raw_rgb(0, 25, 0);
9074
vTaskDelay(1000 / portTICK_PERIOD_MS);
9175
current_state = STATE_OFF;
9276
break;
9377

94-
case STATE_WIFI_LOST: {
95-
float phase = (float)(now % 2500) / 2500.0f * 2.0f * PI;
96-
uint8_t val = 10 + (uint8_t)((sin(phase) + 1.0f) * 45.0f);
97-
write_raw_rgb(val, 0, val / 2);
98-
vTaskDelay(25 / portTICK_PERIOD_MS);
78+
case STATE_WIFI_LOST:
79+
// FIXED: Neutered the Purple breathing loop.
80+
write_raw_rgb(0, 0, 0);
81+
vTaskDelay(100 / portTICK_PERIOD_MS);
9982
break;
100-
}
10183

10284
case STATE_FAULT: {
10385
unsigned long loop_pos = now % 3000;
10486
if (loop_pos < 600) {
105-
if ((loop_pos / 100) % 2 == 0) {
106-
write_raw_rgb(200, 0, 0);
107-
} else {
108-
write_raw_rgb(0, 0, 0);
109-
}
87+
if ((loop_pos / 100) % 2 == 0) write_raw_rgb(200, 0, 0);
88+
else write_raw_rgb(0, 0, 0);
11089
} else {
11190
write_raw_rgb(0, 0, 0);
11291
}
@@ -122,7 +101,6 @@ namespace hw {
122101
pinMode(LED_PIN_G, OUTPUT);
123102
pinMode(LED_PIN_B, OUTPUT);
124103
write_raw_rgb(0, 0, 0);
125-
126104
xTaskCreatePinnedToCore(led_engine_task, "rgb_telemetry_loop", 2048, NULL, 1, NULL, 0);
127105
}
128106

@@ -131,15 +109,12 @@ namespace hw {
131109
}
132110

133111
void trigger_traffic_pulse() {
134-
// FIXED: Suppress routine background packet pulses while asleep to save power
135-
if (services::display_manager::is_sleeping()) return;
136-
112+
if (services::display_manager::is_sleeping()) return;
137113
traffic_pulse_start = millis();
138114
traffic_pulse_active = true;
139115
}
140116

141117
void trigger_priority_strobe() {
142-
// Priority strobes (Direct Messages, HamAlerts) ALWAYS fire, even during sleep!
143118
priority_strobe_start = millis();
144119
priority_strobe_active = true;
145120
}

src/main.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#include "hw/led_rgb.h"
66
#include "config/config.h"
77
#include "core/timekeeper.h"
8-
#include "core/lvgl_fs.h" // FIXED: Include the new bridge
8+
#include "core/lvgl_fs.h"
99
#include "services/wifi_manager.h"
1010
#include "services/web_server.h"
1111
#include "services/display_manager.h"
@@ -38,7 +38,6 @@ void setup() {
3838
Serial.flush();
3939
display_init();
4040

41-
// FIXED: Register the filesystem bridge directly to the core
4241
core::lvgl_fs_init();
4342

4443
Serial.println("[Boot Check] Display Driver OK.");
@@ -60,6 +59,10 @@ void setup() {
6059

6160
Serial.println("--- All operational tasks successfully scheduled ---");
6261
Serial.flush();
62+
63+
// FIXED: Force the LED state to OFF after the boot sequence is complete
64+
// This instantly kills the stuck "breathing cyan" timekeeper loop
65+
hw::led_rgb::set_state(hw::led_rgb::STATE_OFF);
6366
}
6467

6568
void loop() {
@@ -70,4 +73,4 @@ void loop() {
7073
services::display_manager::update();
7174
services::weather_manager::update();
7275
delay(5);
73-
}
76+
}

src/services/aprs_manager.cpp

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#include "aprs_manager.h"
22
#include "../config/config.h"
3+
#include "../core/metadata.h" // NEW: Pulls dynamic version
34
#include "../hw/sensor.h"
4-
#include "../hw/led_rgb.h" // NEW: RGB LED controller inclusion
5+
#include "../hw/led_rgb.h"
56
#include <Arduino.h>
67
#include <WiFi.h>
78
#include <WiFiClient.h>
@@ -25,7 +26,6 @@ namespace services {
2526
uint32_t AprsManager::last_tx_time = 0;
2627
static uint32_t loop_last_beacon_millis = 0;
2728

28-
// TX queue buffers shifted to the heap
2929
static char* tx_msg_queue = nullptr;
3030
static bool tx_msg_pending = false;
3131

@@ -59,21 +59,19 @@ namespace services {
5959
void AprsManager::clear_msg_dirty() { msg_dirty = false; }
6060

6161
void AprsManager::trigger_manual_beacon() {
62-
loop_last_beacon_millis = 0;
62+
loop_last_beacon_millis = 0;
6363
}
6464

6565
void AprsManager::send_message(const char* target, const char* message, bool silent) {
6666
auto& cfg = config::get();
6767
if (!connected || !tx_msg_queue) return;
6868
if (strlen(target) == 0 || strlen(message) == 0) return;
6969

70-
// Force Sender to Uppercase
7170
char src_call[16];
7271
if (cfg.aprs_ssid == 0) snprintf(src_call, sizeof(src_call), "%s", cfg.callsign);
7372
else snprintf(src_call, sizeof(src_call), "%s-%d", cfg.callsign, cfg.aprs_ssid);
7473
for (int i = 0; src_call[i]; i++) src_call[i] = toupper(src_call[i]);
7574

76-
// Force Target to Uppercase & strict 9-character padding
7775
char padded_target[10] = " ";
7876
for (size_t i = 0; i < 9 && target[i] != '\0'; i++) {
7977
padded_target[i] = toupper(target[i]);
@@ -102,8 +100,8 @@ namespace services {
102100

103101
for (size_t i = 0; i < station_count - 1; i++) {
104102
for (size_t j = 0; j < station_count - i - 1; j++) {
105-
bool swap_needed = ascending ?
106-
(stations[j].distance_km > stations[j+1].distance_km) :
103+
bool swap_needed = ascending ?
104+
(stations[j].distance_km > stations[j+1].distance_km) :
107105
(stations[j].distance_km < stations[j+1].distance_km);
108106

109107
if (swap_needed) {
@@ -117,7 +115,7 @@ namespace services {
117115
}
118116

119117
void AprsManager::calc_distance_bearing(float lat1, float lon1, float lat2, float lon2, float& out_dist, int& out_bearing) {
120-
float R = 6371.0f;
118+
float R = 6371.0f;
121119
float dLat = (lat2 - lat1) * PI / 180.0f;
122120
float dLon = (lon2 - lon1) * PI / 180.0f;
123121
float rLat1 = lat1 * PI / 180.0f;
@@ -152,7 +150,7 @@ namespace services {
152150
int lon_deg = (int)lon;
153151
float lon_min = (lon - lon_deg) * 60.0f;
154152

155-
snprintf(out_str, 24, "%02d%05.2f%c%c%03d%05.2f%c%c",
153+
snprintf(out_str, 24, "%02d%05.2f%c%c%03d%05.2f%c%c",
156154
lat_deg, lat_min, lat_dir, table, lon_deg, lon_min, lon_dir, symbol);
157155
}
158156

@@ -200,7 +198,7 @@ namespace services {
200198
stations[target_idx].bearing_deg = brg;
201199
stations[target_idx].last_seen_millis = millis();
202200

203-
if (symbol_char[0] == '[' && table_char[0] == '/') strncpy(stations[target_idx].type, "Runner", 15);
201+
if (symbol_char[0] == '[' && table_char[0] == '/') strncpy(stations[target_idx].type, "Runner", 15);
204202
else if (symbol_char[0] == '-' && table_char[0] == '/') strncpy(stations[target_idx].type, "Fixed Base", 15);
205203
else if (symbol_char[0] == '&' && table_char[0] == 'I') strncpy(stations[target_idx].type, "Gateway", 15);
206204
else if (symbol_char[0] == 'Y' && table_char[0] == '\\') strncpy(stations[target_idx].type, "Radios/APRS", 15);
@@ -225,8 +223,8 @@ namespace services {
225223
char lat_str[9] = {0};
226224
char lon_str[10] = {0};
227225

228-
strncpy(lat_str, info + 1, 8);
229-
strncpy(lon_str, info + 10, 9);
226+
strncpy(lat_str, info + 1, 8);
227+
strncpy(lon_str, info + 10, 9);
230228

231229
char table_char[2] = { info[9], '\0' };
232230
char symbol_char[2] = { info[18], '\0' };
@@ -248,16 +246,12 @@ namespace services {
248246

249247
const char* cmt = (strlen(info) > 19) ? (info + 19) : "";
250248
update_or_add_station(call, dec_lat, dec_lon, table_char, symbol_char, cmt);
251-
252-
// NEW: Trigger a 30ms Cyan pulse to confirm standard background data ingress
253-
hw::led_rgb::trigger_traffic_pulse();
254249
}
255250

256251
void AprsManager::parse_incoming_message(const char* call, const char* info) {
257252
auto& cfg = config::get();
258253
if (strlen(info) < 11 || info[0] != ':') return;
259254

260-
// Extract the 9-character destination from the packet and trim it
261255
char rx_target[10];
262256
memcpy(rx_target, info + 1, 9);
263257
rx_target[9] = '\0';
@@ -266,29 +260,25 @@ namespace services {
266260
else break;
267261
}
268262

269-
// Must match our base callsign (case-insensitive) to catch any SSID variant
270263
if (strncasecmp(rx_target, cfg.callsign, strlen(cfg.callsign)) != 0) return;
271264

272265
if (info[10] == ':') {
273266
char msg_body[64];
274267
strncpy(msg_body, info + 11, 63);
275268
msg_body[63] = '\0';
276269

277-
// Check for Message ID requiring an ACK
278270
char* ack_start = strrchr(msg_body, '{');
279271
if (ack_start) {
280-
*ack_start = '\0'; // Strip the '{ID' from the display string
272+
*ack_start = '\0';
281273
char ack_id[10];
282274
strncpy(ack_id, ack_start + 1, 9);
283275
ack_id[9] = '\0';
284276

285-
// Send silent Auto-ACK directly back to the sender
286277
char ack_payload[16];
287278
snprintf(ack_payload, sizeof(ack_payload), "ack%s", ack_id);
288279
send_message(call, ack_payload, true);
289280
}
290281

291-
// Hide incoming network ACKs so they don't clutter the user's chat screen
292282
if (strncasecmp(msg_body, "ack", 3) == 0) return;
293283

294284
if (message_count < 10) {
@@ -302,7 +292,6 @@ namespace services {
302292
}
303293
msg_dirty = true;
304294

305-
// Inbound directed radio transmission alert! Trigger 3s intense White/Magenta Strobe
306295
hw::led_rgb::trigger_priority_strobe();
307296
}
308297
}
@@ -313,7 +302,7 @@ namespace services {
313302
char* colon = strchr(line, ':');
314303
if (!colon) return;
315304

316-
*colon = '\0';
305+
*colon = '\0';
317306
char* header = line;
318307
char* info = colon + 1;
319308

@@ -327,7 +316,7 @@ namespace services {
327316
} else if (info[0] == '!' || info[0] == '=' || info[0] == '@' || info[0] == '/') {
328317
const char* coord_start = info;
329318
if (info[0] == '@' || info[0] == '/') {
330-
if (strlen(info) > 8) coord_start = info + 7;
319+
if (strlen(info) > 8) coord_start = info + 7;
331320
}
332321
parse_uncompressed_position(callsign, coord_start);
333322
}
@@ -353,11 +342,13 @@ namespace services {
353342
if (cfg.aprs_ssid == 0) snprintf(src_call, sizeof(src_call), "%s", cfg.callsign);
354343
else snprintf(src_call, sizeof(src_call), "%s-%d", cfg.callsign, cfg.aprs_ssid);
355344

356-
snprintf(login, sizeof(login), "user %s pass %s vers QRPickle 1.0 filter r/%.2f/%.2f/50 p/%s\r\n",
357-
src_call, cfg.aprs_passcode, cfg.lat, cfg.lon, cfg.callsign);
345+
// DYNAMIC VERSION INSERTION
346+
snprintf(login, sizeof(login), "user %s pass %s vers %s %s filter r/%.2f/%.2f/50 p/%s\r\n",
347+
src_call, cfg.aprs_passcode, meta::FW_NAME, meta::FW_VERSION, cfg.lat, cfg.lon, cfg.callsign);
348+
358349
client.print(login);
359350
connected = true;
360-
loop_last_beacon_millis = 0;
351+
loop_last_beacon_millis = 0;
361352
} else {
362353
vTaskDelay(pdMS_TO_TICKS(5000));
363354
continue;
@@ -376,7 +367,7 @@ namespace services {
376367
else snprintf(src_call, sizeof(src_call), "%s-%d", cfg.callsign, cfg.aprs_ssid);
377368

378369
char beacon[160];
379-
snprintf(beacon, sizeof(beacon), "%s>APRS,TCPIP*:=%s%s\r\n",
370+
snprintf(beacon, sizeof(beacon), "%s>APRS,TCPIP*:=%s%s\r\n",
380371
src_call, coord_str, dynamic_cmt);
381372

382373
client.print(beacon);
@@ -388,7 +379,7 @@ namespace services {
388379

389380
if (tx_msg_pending && connected) {
390381
client.print(tx_msg_queue);
391-
client.flush();
382+
client.flush();
392383
Serial.printf("[APRS-TX] Hardware flushed packet to network: %s", tx_msg_queue);
393384
tx_msg_pending = false;
394385
}
@@ -410,4 +401,4 @@ namespace services {
410401
connected = false;
411402
vTaskDelete(NULL);
412403
}
413-
} // namespace services
404+
} // namespace services

0 commit comments

Comments
 (0)