From 6fdbf1d258c8a960eb0c67c154c8e7ff401e4e0b Mon Sep 17 00:00:00 2001 From: John Poole Date: Fri, 24 Apr 2026 16:30:27 -0700 Subject: [PATCH 1/4] Reformatting lib files and am starting to add DOxygen comments --- exercises/00_usb_radio_check/src/main.cpp | 21 +- exercises/09_GPS_Time/src/main.cpp | 751 ++++++++++++++-------- lib/tbeam_clock/src/TBeamClock.cpp | 561 ++++++++-------- lib/tbeam_clock/src/TBeamClock.h | 114 ++-- lib/tbeam_logger/src/TBeamLogger.cpp | 146 +++-- 5 files changed, 955 insertions(+), 638 deletions(-) diff --git a/exercises/00_usb_radio_check/src/main.cpp b/exercises/00_usb_radio_check/src/main.cpp index 108ba16..9b73f00 100644 --- a/exercises/00_usb_radio_check/src/main.cpp +++ b/exercises/00_usb_radio_check/src/main.cpp @@ -18,11 +18,27 @@ #define LORA_CR 5 #endif +/** + * This sketch is intended to be used as a quick test of the LoRa radio on the + * T-Beam Supreme board, to verify that the radio is functional and can be used + * in a USB-connected application. + * It will attempt to initialize the radio, and then repeatedly transmit a test + * frame and call startReceive() to verify that the radio is responsive. + * Note that this sketch is not intended to be a full test of the radio's + * functionality, but rather a quick check that the radio can be initialized + * and used without errors. If you are seeing -706 or -707 errors, it likely means + * that the radio is not starting up correctly, which can be caused by incorrect + * pin connections or power issues. If you are seeing other errors, it may indicate + * a different issue with the radio or the code. + */ // SX1262 on T-Beam Supreme (tbeam-s3-core pinout) SX1262 radio = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY); int state; // = radio.begin(915.0, 125.0, 7, 5, 0x12, 14); +/* + @brief Setup function. Initializes the radio and prints the result to the serial console. +*/ void setup() { Serial.begin(115200); delay(2000); // give USB time to enumerate @@ -42,7 +58,10 @@ void setup() { } - +/* + @brief Loop function. Transmits a test frame and calls startReceive() to verify that + the radio is responsive. Repeats every second. +*/ void loop() { static uint32_t counter = 0; Serial.printf("alive %lu\n", counter++); diff --git a/exercises/09_GPS_Time/src/main.cpp b/exercises/09_GPS_Time/src/main.cpp index c8fd2a7..6556989 100644 --- a/exercises/09_GPS_Time/src/main.cpp +++ b/exercises/09_GPS_Time/src/main.cpp @@ -33,12 +33,12 @@ static const uint32_t kSerialDelayMs = 5000; static const uint32_t kMinuteMs = 60000; static const uint32_t kGpsDiagnosticLogMs = 15000; -static const char* kGpsLogDir = "/gpsdiag"; -static const char* kGpsLogPath = "/gpsdiag/current.log"; -static const char* kBuildDate = __DATE__; -static const char* kBuildTime = __TIME__; +static const char *kGpsLogDir = "/gpsdiag"; +static const char *kGpsLogPath = "/gpsdiag/current.log"; +static const char *kBuildDate = __DATE__; +static const char *kBuildTime = __TIME__; -static XPowersLibInterface* g_pmu = nullptr; +static XPowersLibInterface *g_pmu = nullptr; static StartupSdManager g_sd(Serial); static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE); static HardwareSerial g_gpsSerial(1); @@ -74,7 +74,8 @@ static uint8_t g_rawLogGsvCount = 0; static uint8_t g_rawLogRmcCount = 0; static uint8_t g_rawLogPubxCount = 0; -enum class GpsModuleKind : uint8_t { +enum class GpsModuleKind : uint8_t +{ UNKNOWN = 0, L76K, UBLOX @@ -88,7 +89,8 @@ static const GpsModuleKind kExpectedGpsModule = GpsModuleKind::L76K; static const GpsModuleKind kExpectedGpsModule = GpsModuleKind::UNKNOWN; #endif -struct RtcDateTime { +struct RtcDateTime +{ uint16_t year; uint8_t month; uint8_t day; @@ -97,7 +99,8 @@ struct RtcDateTime { uint8_t second; }; -struct GpsState { +struct GpsState +{ GpsModuleKind module = kExpectedGpsModule; bool sawAnySentence = false; @@ -131,22 +134,28 @@ static uint8_t displayedSatsInView(); static bool displayHasFreshUtc(); static String formatRtcNow(); -static bool ensureGpsLogDirectory() { - if (!g_spiffsReady) { +static bool ensureGpsLogDirectory() +{ + if (!g_spiffsReady) + { return false; } - if (SPIFFS.exists(kGpsLogDir)) { + if (SPIFFS.exists(kGpsLogDir)) + { return true; } return SPIFFS.mkdir(kGpsLogDir); } -static bool gpsDiagAppendLine(const char* line) { - if (!g_spiffsReady || !line) { +static bool gpsDiagAppendLine(const char *line) +{ + if (!g_spiffsReady || !line) + { return false; } File file = SPIFFS.open(kGpsLogPath, FILE_APPEND); - if (!file) { + if (!file) + { return false; } file.print(line); @@ -155,14 +164,17 @@ static bool gpsDiagAppendLine(const char* line) { return true; } -static void formatGpsSnapshot(char* out, size_t outSize, const char* event) { - if (!out || outSize == 0) { +static void formatGpsSnapshot(char *out, size_t outSize, const char *event) +{ + if (!out || outSize == 0) + { return; } const uint8_t sats = bestSatelliteCount(); - const char* ev = event ? event : "sample"; - if (g_gps.hasValidUtc) { + const char *ev = event ? event : "sample"; + if (g_gps.hasValidUtc) + { snprintf(out, outSize, "ms=%lu event=%s module=%s nmea=%s sats_used=%u sats_view=%u sats_best=%u utc=%04u-%02u-%02uT%02u:%02u:%02u rx=%d tx=%d baud=%lu", @@ -182,7 +194,9 @@ static void formatGpsSnapshot(char* out, size_t outSize, const char* event) { g_gpsRxPin, g_gpsTxPin, (unsigned long)g_gpsBaud); - } else { + } + else + { String rtc = formatRtcNow(); snprintf(out, outSize, @@ -201,37 +215,51 @@ static void formatGpsSnapshot(char* out, size_t outSize, const char* event) { } } -static void appendGpsSnapshot(const char* event) { +static void appendGpsSnapshot(const char *event) +{ char line[256]; formatGpsSnapshot(line, sizeof(line), event); (void)gpsDiagAppendLine(line); } -static String buildStampShort() { +static String buildStampShort() +{ char buf[32]; snprintf(buf, sizeof(buf), "%s %.5s", kBuildDate, kBuildTime); return String(buf); } -static void maybeLogRawSentence(const char* type, const char* sentence) { - if (!type || !sentence || !g_spiffsReady) { +static void maybeLogRawSentence(const char *type, const char *sentence) +{ + if (!type || !sentence || !g_spiffsReady) + { return; } - uint8_t* counter = nullptr; - if (strcmp(type, "GGA") == 0) { + uint8_t *counter = nullptr; + if (strcmp(type, "GGA") == 0) + { counter = &g_rawLogGgaCount; - } else if (strcmp(type, "GSA") == 0) { + } + else if (strcmp(type, "GSA") == 0) + { counter = &g_rawLogGsaCount; - } else if (strcmp(type, "GSV") == 0) { + } + else if (strcmp(type, "GSV") == 0) + { counter = &g_rawLogGsvCount; - } else if (strcmp(type, "RMC") == 0) { + } + else if (strcmp(type, "RMC") == 0) + { counter = &g_rawLogRmcCount; - } else if (strcmp(type, "PUBX") == 0) { + } + else if (strcmp(type, "PUBX") == 0) + { counter = &g_rawLogPubxCount; } - if (!counter || *counter >= 12) { + if (!counter || *counter >= 12) + { return; } (*counter)++; @@ -247,17 +275,21 @@ static void maybeLogRawSentence(const char* type, const char* sentence) { (void)gpsDiagAppendLine(line); } -static void clearGpsSerialInput() { +static void clearGpsSerialInput() +{ g_gpsLineLen = 0; - while (g_gpsSerial.available() > 0) { + while (g_gpsSerial.available() > 0) + { (void)g_gpsSerial.read(); } } -static void ubxChecksum(uint8_t* message, size_t length) { +static void ubxChecksum(uint8_t *message, size_t length) +{ uint8_t ckA = 0; uint8_t ckB = 0; - for (size_t i = 2; i < length - 2; ++i) { + for (size_t i = 2; i < length - 2; ++i) + { ckA = (uint8_t)((ckA + message[i]) & 0xFF); ckB = (uint8_t)((ckB + ckA) & 0xFF); } @@ -265,13 +297,15 @@ static void ubxChecksum(uint8_t* message, size_t length) { message[length - 1] = ckB; } -static size_t makeUbxPacket(uint8_t* out, +static size_t makeUbxPacket(uint8_t *out, size_t outSize, uint8_t classId, uint8_t msgId, - const uint8_t* payload, - uint16_t payloadSize) { - if (!out || outSize < (size_t)payloadSize + 8U) { + const uint8_t *payload, + uint16_t payloadSize) +{ + if (!out || outSize < (size_t)payloadSize + 8U) + { return 0; } out[0] = 0xB5; @@ -280,7 +314,8 @@ static size_t makeUbxPacket(uint8_t* out, out[3] = msgId; out[4] = (uint8_t)(payloadSize & 0xFF); out[5] = (uint8_t)((payloadSize >> 8) & 0xFF); - for (uint16_t i = 0; i < payloadSize; ++i) { + for (uint16_t i = 0; i < payloadSize; ++i) + { out[6 + i] = payload ? payload[i] : 0; } out[6 + payloadSize] = 0; @@ -289,99 +324,116 @@ static size_t makeUbxPacket(uint8_t* out, return (size_t)payloadSize + 8U; } -static bool waitForUbxAck(uint8_t classId, uint8_t msgId, uint32_t waitMs) { +static bool waitForUbxAck(uint8_t classId, uint8_t msgId, uint32_t waitMs) +{ uint8_t ack[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, classId, msgId, 0x00, 0x00}; ubxChecksum(ack, sizeof(ack)); uint8_t ackPos = 0; uint32_t deadline = millis() + waitMs; - while ((int32_t)(deadline - millis()) > 0) { - if (g_gpsSerial.available() <= 0) { + while ((int32_t)(deadline - millis()) > 0) + { + if (g_gpsSerial.available() <= 0) + { delay(2); continue; } uint8_t b = (uint8_t)g_gpsSerial.read(); - if (b == ack[ackPos]) { + if (b == ack[ackPos]) + { ackPos++; - if (ackPos == sizeof(ack)) { + if (ackPos == sizeof(ack)) + { return true; } - } else { + } + else + { ackPos = (b == ack[0]) ? 1 : 0; } } return false; } -static int waitForUbxPayload(uint8_t* buffer, +static int waitForUbxPayload(uint8_t *buffer, uint16_t bufferSize, uint8_t classId, uint8_t msgId, - uint32_t waitMs) { + uint32_t waitMs) +{ uint16_t framePos = 0; uint16_t needRead = 0; uint32_t deadline = millis() + waitMs; - while ((int32_t)(deadline - millis()) > 0) { - if (g_gpsSerial.available() <= 0) { + while ((int32_t)(deadline - millis()) > 0) + { + if (g_gpsSerial.available() <= 0) + { delay(2); continue; } int c = g_gpsSerial.read(); - switch (framePos) { - case 0: - framePos = (c == 0xB5) ? 1 : 0; - break; - case 1: - framePos = (c == 0x62) ? 2 : 0; - break; - case 2: - framePos = (c == classId) ? 3 : 0; - break; - case 3: - framePos = (c == msgId) ? 4 : 0; - break; - case 4: - needRead = (uint16_t)c; - framePos = 5; - break; - case 5: - needRead |= (uint16_t)(c << 8); - if (needRead == 0 || needRead >= bufferSize) { - framePos = 0; - break; - } - if (g_gpsSerial.readBytes(buffer, needRead) != needRead) { - framePos = 0; - break; - } - if (g_gpsSerial.available() >= 2) { - (void)g_gpsSerial.read(); - (void)g_gpsSerial.read(); - } - return (int)needRead; - default: + switch (framePos) + { + case 0: + framePos = (c == 0xB5) ? 1 : 0; + break; + case 1: + framePos = (c == 0x62) ? 2 : 0; + break; + case 2: + framePos = (c == classId) ? 3 : 0; + break; + case 3: + framePos = (c == msgId) ? 4 : 0; + break; + case 4: + needRead = (uint16_t)c; + framePos = 5; + break; + case 5: + needRead |= (uint16_t)(c << 8); + if (needRead == 0 || needRead >= bufferSize) + { framePos = 0; break; + } + if (g_gpsSerial.readBytes(buffer, needRead) != needRead) + { + framePos = 0; + break; + } + if (g_gpsSerial.available() >= 2) + { + (void)g_gpsSerial.read(); + (void)g_gpsSerial.read(); + } + return (int)needRead; + default: + framePos = 0; + break; } } return 0; } -static bool detectUbloxM10() { +static bool detectUbloxM10() +{ uint8_t packet[8]; uint8_t payload[256] = {0}; size_t len = makeUbxPacket(packet, sizeof(packet), 0x0A, 0x04, nullptr, 0); - if (len == 0) { + if (len == 0) + { return false; } clearGpsSerialInput(); g_gpsSerial.write(packet, len); int payloadLen = waitForUbxPayload(payload, sizeof(payload), 0x0A, 0x04, 1200); - if (payloadLen < 40) { + if (payloadLen < 40) + { appendGpsSnapshot("ubx_monver_timeout"); return false; } @@ -392,16 +444,20 @@ static bool detectUbloxM10() { snprintf(line, sizeof(line), "ms=%lu event=ubx_monver hw=%s", (unsigned long)millis(), hwVersion); (void)gpsDiagAppendLine(line); - if (strncmp(hwVersion, "000A0000", 8) == 0) { + if (strncmp(hwVersion, "000A0000", 8) == 0) + { return true; } - for (int pos = 40; pos + 30 <= payloadLen; pos += 30) { - if (strncmp((const char*)(payload + pos), "PROTVER=", 8) == 0) { - int prot = atoi((const char*)(payload + pos + 8)); + for (int pos = 40; pos + 30 <= payloadLen; pos += 30) + { + if (strncmp((const char *)(payload + pos), "PROTVER=", 8) == 0) + { + int prot = atoi((const char *)(payload + pos + 8)); snprintf(line, sizeof(line), "ms=%lu event=ubx_monver prot=%d", (unsigned long)millis(), prot); (void)gpsDiagAppendLine(line); - if (prot >= 27) { + if (prot >= 27) + { return true; } } @@ -412,13 +468,15 @@ static bool detectUbloxM10() { static bool sendUbxValset(uint8_t classId, uint8_t msgId, - const uint8_t* payload, + const uint8_t *payload, uint16_t payloadLen, uint32_t ackMs, - const char* eventName) { + const char *eventName) +{ uint8_t packet[96]; size_t len = makeUbxPacket(packet, sizeof(packet), classId, msgId, payload, payloadLen); - if (len == 0) { + if (len == 0) + { return false; } clearGpsSerialInput(); @@ -436,7 +494,8 @@ static bool sendUbxValset(uint8_t classId, return ok; } -static bool configureUbloxM10() { +static bool configureUbloxM10() +{ static const uint8_t kValsetDisableTxtRam[] = {0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; static const uint8_t kValsetDisableTxtBbr[] = {0x00, 0x02, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; static const uint8_t kValsetEnableNmeaRam[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; @@ -453,15 +512,18 @@ static bool configureUbloxM10() { return ok; } -static void maybeConfigureUblox() { - if (g_ubloxConfigAttempted || kExpectedGpsModule != GpsModuleKind::UBLOX) { +static void maybeConfigureUblox() +{ + if (g_ubloxConfigAttempted || kExpectedGpsModule != GpsModuleKind::UBLOX) + { return; } g_ubloxConfigAttempted = true; appendGpsSnapshot("ubx_config_attempt"); g_ubloxIsM10 = detectUbloxM10(); - if (!g_ubloxIsM10) { + if (!g_ubloxIsM10) + { appendGpsSnapshot("ubx_non_m10_or_unknown"); return; } @@ -469,7 +531,8 @@ static void maybeConfigureUblox() { g_ubloxConfigured = configureUbloxM10(); } -static void logf(const char* fmt, ...) { +static void logf(const char *fmt, ...) +{ char msg[220]; va_list args; va_start(args, fmt); @@ -478,35 +541,45 @@ static void logf(const char* fmt, ...) { Serial.printf("[%10lu][%06lu] %s\r\n", (unsigned long)millis(), (unsigned long)g_logSeq++, msg); } -static void oledShowLines(const char* l1, - const char* l2 = nullptr, - const char* l3 = nullptr, - const char* l4 = nullptr, - const char* l5 = nullptr) { +static void oledShowLines(const char *l1, + const char *l2 = nullptr, + const char *l3 = nullptr, + const char *l4 = nullptr, + const char *l5 = nullptr) +{ g_oled.clearBuffer(); g_oled.setFont(u8g2_font_5x8_tf); - if (l1) g_oled.drawUTF8(0, 12, l1); - if (l2) g_oled.drawUTF8(0, 24, l2); - if (l3) g_oled.drawUTF8(0, 36, l3); - if (l4) g_oled.drawUTF8(0, 48, l4); - if (l5) g_oled.drawUTF8(0, 60, l5); + if (l1) + g_oled.drawUTF8(0, 12, l1); + if (l2) + g_oled.drawUTF8(0, 24, l2); + if (l3) + g_oled.drawUTF8(0, 36, l3); + if (l4) + g_oled.drawUTF8(0, 48, l4); + if (l5) + g_oled.drawUTF8(0, 60, l5); g_oled.sendBuffer(); } -static uint8_t fromBcd(uint8_t b) { +static uint8_t fromBcd(uint8_t b) +{ return ((b >> 4U) * 10U) + (b & 0x0FU); } -static bool rtcRead(RtcDateTime& out, bool& lowVoltageFlag) { +static bool rtcRead(RtcDateTime &out, bool &lowVoltageFlag) +{ Wire1.beginTransmission(RTC_I2C_ADDR); Wire1.write(0x02); - if (Wire1.endTransmission(false) != 0) { + if (Wire1.endTransmission(false) != 0) + { return false; } const uint8_t need = 7; uint8_t got = Wire1.requestFrom((int)RTC_I2C_ADDR, (int)need); - if (got != need) { + if (got != need) + { return false; } @@ -514,7 +587,7 @@ static bool rtcRead(RtcDateTime& out, bool& lowVoltageFlag) { uint8_t min = Wire1.read(); uint8_t hour = Wire1.read(); uint8_t day = Wire1.read(); - (void)Wire1.read(); // weekday + (void)Wire1.read(); // weekday uint8_t month = Wire1.read(); uint8_t year = Wire1.read(); @@ -530,10 +603,12 @@ static bool rtcRead(RtcDateTime& out, bool& lowVoltageFlag) { return true; } -static String formatRtcNow() { +static String formatRtcNow() +{ RtcDateTime now{}; bool lowV = false; - if (!rtcRead(now, lowV)) { + if (!rtcRead(now, lowV)) + { return "RTC: read failed"; } @@ -551,99 +626,124 @@ static String formatRtcNow() { return String(buf); } -static String gpsModuleToString(GpsModuleKind kind) { - if (kind == GpsModuleKind::L76K) { +static String gpsModuleToString(GpsModuleKind kind) +{ + if (kind == GpsModuleKind::L76K) + { return "L76K"; } - if (kind == GpsModuleKind::UBLOX) { + if (kind == GpsModuleKind::UBLOX) + { return "UBLOX"; } return "Unknown"; } -static bool parseUInt2(const char* s, uint8_t& out) { - if (!s || !isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) { +static bool parseUInt2(const char *s, uint8_t &out) +{ + if (!s || !isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) + { return false; } out = (uint8_t)((s[0] - '0') * 10 + (s[1] - '0')); return true; } -static void detectModuleFromText(const char* text) { - if (!text || text[0] == '\0') { +static void detectModuleFromText(const char *text) +{ + if (!text || text[0] == '\0') + { return; } String t(text); t.toUpperCase(); - if (t.indexOf("L76K") >= 0) { + if (t.indexOf("L76K") >= 0) + { g_gps.module = GpsModuleKind::L76K; return; } - if (t.indexOf("QUECTEL") >= 0 || t.indexOf("2021-10-20") >= 0 || t.indexOf("2021/10/20") >= 0) { + if (t.indexOf("QUECTEL") >= 0 || t.indexOf("2021-10-20") >= 0 || t.indexOf("2021/10/20") >= 0) + { g_gps.module = GpsModuleKind::L76K; } } -static void parseGga(char* fields[], int count) { - if (count <= 7) { +static void parseGga(char *fields[], int count) +{ + if (count <= 7) + { return; } int sats = atoi(fields[7]); - if (sats >= 0 && sats <= 255) { + if (sats >= 0 && sats <= 255) + { g_gps.satsUsed = (uint8_t)sats; - if ((uint8_t)sats > g_gps.satsUsedWindowMax) { + if ((uint8_t)sats > g_gps.satsUsedWindowMax) + { g_gps.satsUsedWindowMax = (uint8_t)sats; } g_gps.satsUsedWindowMs = millis(); } } -static void parseGsv(char* fields[], int count) { - if (count <= 3) { +static void parseGsv(char *fields[], int count) +{ + if (count <= 3) + { return; } int sats = atoi(fields[3]); - if (sats >= 0 && sats <= 255) { + if (sats >= 0 && sats <= 255) + { g_gps.satsInView = (uint8_t)sats; - if ((uint8_t)sats > g_gps.satsInViewWindowMax) { + if ((uint8_t)sats > g_gps.satsInViewWindowMax) + { g_gps.satsInViewWindowMax = (uint8_t)sats; } g_gps.satsInViewWindowMs = millis(); } } -static void parseGsa(char* fields[], int count) { - if (count <= 3) { +static void parseGsa(char *fields[], int count) +{ + if (count <= 3) + { return; } } -static void parseRmc(char* fields[], int count) { - if (count <= 9) { +static void parseRmc(char *fields[], int count) +{ + if (count <= 9) + { return; } - const char* utc = fields[1]; - const char* status = fields[2]; - const char* date = fields[9]; + const char *utc = fields[1]; + const char *status = fields[2]; + const char *date = fields[9]; - if (!status || status[0] != 'A') { + if (!status || status[0] != 'A') + { return; } - if (!utc || strlen(utc) < 6 || !date || strlen(date) < 6) { + if (!utc || strlen(utc) < 6 || !date || strlen(date) < 6) + { return; } uint8_t hh = 0, mm = 0, ss = 0; uint8_t dd = 0, mo = 0, yy = 0; - if (!parseUInt2(utc + 0, hh) || !parseUInt2(utc + 2, mm) || !parseUInt2(utc + 4, ss)) { + if (!parseUInt2(utc + 0, hh) || !parseUInt2(utc + 2, mm) || !parseUInt2(utc + 4, ss)) + { return; } - if (!parseUInt2(date + 0, dd) || !parseUInt2(date + 2, mo) || !parseUInt2(date + 4, yy)) { + if (!parseUInt2(date + 0, dd) || !parseUInt2(date + 2, mo) || !parseUInt2(date + 4, yy)) + { return; } @@ -657,24 +757,30 @@ static void parseRmc(char* fields[], int count) { g_gps.utcFixMs = millis(); } -static void parseTxt(char* fields[], int count) { - if (count <= 4) { +static void parseTxt(char *fields[], int count) +{ + if (count <= 4) + { return; } detectModuleFromText(fields[4]); } -static int splitCsvPreserveEmpty(char* line, char* fields[], int maxFields) { - if (!line || !fields || maxFields <= 0) { +static int splitCsvPreserveEmpty(char *line, char *fields[], int maxFields) +{ + if (!line || !fields || maxFields <= 0) + { return 0; } int count = 0; - char* p = line; + char *p = line; fields[count++] = p; - while (*p && count < maxFields) { - if (*p == ',') { + while (*p && count < maxFields) + { + if (*p == ',') + { *p = '\0'; fields[count++] = p + 1; } @@ -684,8 +790,10 @@ static int splitCsvPreserveEmpty(char* line, char* fields[], int maxFields) { return count; } -static void processNmeaLine(char* line) { - if (!line || line[0] != '$') { +static void processNmeaLine(char *line) +{ + if (!line || line[0] != '$') + { return; } g_gps.sawAnySentence = true; @@ -693,51 +801,69 @@ static void processNmeaLine(char* line) { strncpy(rawLine, line, sizeof(rawLine) - 1); rawLine[sizeof(rawLine) - 1] = '\0'; - char* star = strchr(line, '*'); - if (star) { + char *star = strchr(line, '*'); + if (star) + { *star = '\0'; } - char* fields[24] = {0}; + char *fields[24] = {0}; int count = splitCsvPreserveEmpty(line, fields, 24); - if (count <= 0 || !fields[0]) { + if (count <= 0 || !fields[0]) + { return; } - const char* header = fields[0]; - if (strcmp(header, "$PUBX") == 0) { + const char *header = fields[0]; + if (strcmp(header, "$PUBX") == 0) + { g_gps.module = GpsModuleKind::UBLOX; maybeLogRawSentence("PUBX", rawLine); return; } size_t n = strlen(header); - if (n < 6) { + if (n < 6) + { return; } - const char* type = header + (n - 3); + const char *type = header + (n - 3); maybeLogRawSentence(type, rawLine); - if (strcmp(type, "GGA") == 0) { + if (strcmp(type, "GGA") == 0) + { parseGga(fields, count); - } else if (strcmp(type, "GSA") == 0) { + } + else if (strcmp(type, "GSA") == 0) + { parseGsa(fields, count); - } else if (strcmp(type, "GSV") == 0) { + } + else if (strcmp(type, "GSV") == 0) + { parseGsv(fields, count); - } else if (strcmp(type, "RMC") == 0) { + } + else if (strcmp(type, "RMC") == 0) + { parseRmc(fields, count); - } else if (strcmp(type, "TXT") == 0) { + } + else if (strcmp(type, "TXT") == 0) + { parseTxt(fields, count); } } -static void pollGpsSerial() { - while (g_gpsSerial.available() > 0) { +static void pollGpsSerial() +{ + while (g_gpsSerial.available() > 0) + { char c = (char)g_gpsSerial.read(); - if (c == '\r') { + if (c == '\r') + { continue; } - if (c == '\n') { - if (g_gpsLineLen > 0) { + if (c == '\n') + { + if (g_gpsLineLen > 0) + { g_gpsLine[g_gpsLineLen] = '\0'; processNmeaLine(g_gpsLine); g_gpsLineLen = 0; @@ -745,15 +871,19 @@ static void pollGpsSerial() { continue; } - if (g_gpsLineLen + 1 < sizeof(g_gpsLine)) { + if (g_gpsLineLen + 1 < sizeof(g_gpsLine)) + { g_gpsLine[g_gpsLineLen++] = c; - } else { + } + else + { g_gpsLineLen = 0; } } } -static void showGpsLogHelp() { +static void showGpsLogHelp() +{ Serial.println("Command list:"); Serial.println(" help - show command menu"); Serial.println(" stat - show current GPS log file info"); @@ -762,15 +892,18 @@ static void showGpsLogHelp() { Serial.println(" clear - erase current GPS log"); } -static void gpsLogStat() { +static void gpsLogStat() +{ Serial.printf("SPIFFS: %s\r\n", g_spiffsReady ? "ready" : "not ready"); Serial.printf("Path: %s\r\n", kGpsLogPath); - if (!g_spiffsReady || !SPIFFS.exists(kGpsLogPath)) { + if (!g_spiffsReady || !SPIFFS.exists(kGpsLogPath)) + { Serial.println("Current GPS log does not exist"); return; } File file = SPIFFS.open(kGpsLogPath, FILE_READ); - if (!file) { + if (!file) + { Serial.println("Unable to open current GPS log"); return; } @@ -782,51 +915,63 @@ static void gpsLogStat() { (unsigned)(SPIFFS.totalBytes() - SPIFFS.usedBytes())); } -static void gpsLogList() { - if (!g_spiffsReady) { +static void gpsLogList() +{ + if (!g_spiffsReady) + { Serial.println("SPIFFS not ready"); return; } File dir = SPIFFS.open(kGpsLogDir); - if (!dir || !dir.isDirectory()) { + if (!dir || !dir.isDirectory()) + { Serial.printf("Unable to open %s\r\n", kGpsLogDir); return; } Serial.printf("Files in %s:\r\n", kGpsLogDir); File file = dir.openNextFile(); - while (file) { + while (file) + { Serial.printf(" %s (%u bytes)\r\n", file.name(), (unsigned)file.size()); file = dir.openNextFile(); } } -static void gpsLogRead() { - if (!g_spiffsReady || !SPIFFS.exists(kGpsLogPath)) { +static void gpsLogRead() +{ + if (!g_spiffsReady || !SPIFFS.exists(kGpsLogPath)) + { Serial.println("Current GPS log is not available"); return; } File file = SPIFFS.open(kGpsLogPath, FILE_READ); - if (!file) { + if (!file) + { Serial.println("Unable to read current GPS log"); return; } Serial.printf("Reading %s:\r\n", kGpsLogPath); - while (file.available()) { + while (file.available()) + { Serial.write(file.read()); } - if (file.size() > 0) { + if (file.size() > 0) + { Serial.println(); } file.close(); } -static void gpsLogClear() { - if (!g_spiffsReady) { +static void gpsLogClear() +{ + if (!g_spiffsReady) + { Serial.println("SPIFFS not ready"); return; } File file = SPIFFS.open(kGpsLogPath, FILE_WRITE); - if (!file) { + if (!file) + { Serial.println("Unable to clear current GPS log"); return; } @@ -834,50 +979,72 @@ static void gpsLogClear() { Serial.printf("Cleared %s\r\n", kGpsLogPath); } // Process a command received on the serial console. -static void processSerialCommand(const char* line) { - if (!line || line[0] == '\0') { +static void processSerialCommand(const char *line) +{ + if (!line || line[0] == '\0') + { return; } // Echo the command back to the console for clarity and posterity. Serial.printf("-->%s\r\n", line); - if (strcasecmp(line, "help") == 0) { + if (strcasecmp(line, "help") == 0) + { showGpsLogHelp(); - } else if (strcasecmp(line, "stat") == 0) { + } + else if (strcasecmp(line, "stat") == 0) + { gpsLogStat(); - } else if (strcasecmp(line, "list") == 0) { + } + else if (strcasecmp(line, "list") == 0) + { gpsLogList(); - } else if (strcasecmp(line, "read") == 0) { + } + else if (strcasecmp(line, "read") == 0) + { gpsLogRead(); - } else if (strcasecmp(line, "clear") == 0) { + } + else if (strcasecmp(line, "clear") == 0) + { gpsLogClear(); - } else { + } + else + { Serial.println("Unknown command (help for list)"); } } -static void pollSerialConsole() { - while (Serial.available() > 0) { +static void pollSerialConsole() +{ + while (Serial.available() > 0) + { int c = Serial.read(); - if (c < 0) { + if (c < 0) + { continue; } - if (c == '\r' || c == '\n') { - if (g_serialLineLen > 0) { + if (c == '\r' || c == '\n') + { + if (g_serialLineLen > 0) + { g_serialLine[g_serialLineLen] = '\0'; processSerialCommand(g_serialLine); g_serialLineLen = 0; } continue; } - if (g_serialLineLen + 1 < sizeof(g_serialLine)) { + if (g_serialLineLen + 1 < sizeof(g_serialLine)) + { g_serialLine[g_serialLineLen++] = (char)c; - } else { + } + else + { g_serialLineLen = 0; } } } -static void startGpsUart(uint32_t baud, int rxPin, int txPin) { +static void startGpsUart(uint32_t baud, int rxPin, int txPin) +{ g_gpsSerial.end(); delay(20); g_gpsSerial.setRxBufferSize(1024); @@ -887,15 +1054,19 @@ static void startGpsUart(uint32_t baud, int rxPin, int txPin) { g_gpsTxPin = txPin; } -static bool collectGpsTraffic(uint32_t windowMs, bool updateSd) { +static bool collectGpsTraffic(uint32_t windowMs, bool updateSd) +{ uint32_t start = millis(); bool sawBytes = false; - while ((uint32_t)(millis() - start) < windowMs) { - if (g_gpsSerial.available() > 0) { + while ((uint32_t)(millis() - start) < windowMs) + { + if (g_gpsSerial.available() > 0) + { sawBytes = true; } pollGpsSerial(); - if (updateSd) { + if (updateSd) + { g_sd.update(); } delay(2); @@ -903,10 +1074,12 @@ static bool collectGpsTraffic(uint32_t windowMs, bool updateSd) { return sawBytes || g_gps.sawAnySentence; } -static bool probeGpsAtBaud(uint32_t baud, int rxPin, int txPin) { +static bool probeGpsAtBaud(uint32_t baud, int rxPin, int txPin) +{ startGpsUart(baud, rxPin, txPin); logf("Probing GPS at %lu baud on RX=%d TX=%d...", (unsigned long)baud, rxPin, txPin); - if (collectGpsTraffic(700, true)) { + if (collectGpsTraffic(700, true)) + { return true; } @@ -921,23 +1094,28 @@ static bool probeGpsAtBaud(uint32_t baud, int rxPin, int txPin) { return collectGpsTraffic(1200, true); } -static void initialGpsProbe() { +static void initialGpsProbe() +{ const uint32_t bauds[] = {GPS_BAUD, 115200, 38400, 57600, 19200}; int pinCandidates[2][2] = { {GPS_RX_PIN, GPS_TX_PIN}, - {34, 12}, // Legacy T-Beam UBLOX mapping. + {34, 12}, // Legacy T-Beam UBLOX mapping. }; size_t pinCount = 1; if (kExpectedGpsModule == GpsModuleKind::UBLOX && - !(GPS_RX_PIN == 34 && GPS_TX_PIN == 12)) { + !(GPS_RX_PIN == 34 && GPS_TX_PIN == 12)) + { pinCount = 2; } - for (size_t p = 0; p < pinCount; ++p) { + for (size_t p = 0; p < pinCount; ++p) + { int rxPin = pinCandidates[p][0]; int txPin = pinCandidates[p][1]; - for (size_t i = 0; i < sizeof(bauds) / sizeof(bauds[0]); ++i) { - if (probeGpsAtBaud(bauds[i], rxPin, txPin)) { + for (size_t i = 0; i < sizeof(bauds) / sizeof(bauds[0]); ++i) + { + if (probeGpsAtBaud(bauds[i], rxPin, txPin)) + { logf("GPS traffic detected at %lu baud on RX=%d TX=%d", (unsigned long)g_gpsBaud, g_gpsRxPin, g_gpsTxPin); return; @@ -947,24 +1125,30 @@ static void initialGpsProbe() { logf("No GPS traffic detected during startup probe"); } -static uint32_t startupProbeWindowMs() { +static uint32_t startupProbeWindowMs() +{ return (kExpectedGpsModule == GpsModuleKind::UBLOX) ? 45000U : 20000U; } -static GpsModuleKind activeGpsModule() { - if (g_gps.module != GpsModuleKind::UNKNOWN) { +static GpsModuleKind activeGpsModule() +{ + if (g_gps.module != GpsModuleKind::UNKNOWN) + { return g_gps.module; } return kExpectedGpsModule; } -static uint8_t bestSatelliteCount() { +static uint8_t bestSatelliteCount() +{ uint32_t now = millis(); - if ((uint32_t)(now - g_gps.satsUsedWindowMs) > kSatelliteWindowMs) { + if ((uint32_t)(now - g_gps.satsUsedWindowMs) > kSatelliteWindowMs) + { g_gps.satsUsedWindowMax = g_gps.satsUsed; } - if ((uint32_t)(now - g_gps.satsInViewWindowMs) > kSatelliteWindowMs) { + if ((uint32_t)(now - g_gps.satsInViewWindowMs) > kSatelliteWindowMs) + { g_gps.satsInViewWindowMax = g_gps.satsInView; } @@ -973,33 +1157,41 @@ static uint8_t bestSatelliteCount() { return (used > inView) ? used : inView; } -static uint8_t displayedSatsUsed() { - if ((uint32_t)(millis() - g_gps.satsUsedWindowMs) > kFixFreshMs) { +static uint8_t displayedSatsUsed() +{ + if ((uint32_t)(millis() - g_gps.satsUsedWindowMs) > kFixFreshMs) + { return 0; } return g_gps.satsUsed; } -static uint8_t displayedSatsInView() { - if ((uint32_t)(millis() - g_gps.satsInViewWindowMs) > kFixFreshMs) { +static uint8_t displayedSatsInView() +{ + if ((uint32_t)(millis() - g_gps.satsInViewWindowMs) > kFixFreshMs) + { return 0; } return g_gps.satsInView; } -static bool displayHasFreshUtc() { +static bool displayHasFreshUtc() +{ return g_gps.hasValidUtc && (uint32_t)(millis() - g_gps.utcFixMs) <= kFixFreshMs; } -static bool isUnsupportedGpsMode() { +static bool isUnsupportedGpsMode() +{ GpsModuleKind active = activeGpsModule(); - if (kExpectedGpsModule == GpsModuleKind::UNKNOWN || active == GpsModuleKind::UNKNOWN) { + if (kExpectedGpsModule == GpsModuleKind::UNKNOWN || active == GpsModuleKind::UNKNOWN) + { return false; } return active != kExpectedGpsModule; } -static void reportStatusToSerial() { +static void reportStatusToSerial() +{ uint8_t satsUsed = displayedSatsUsed(); uint8_t satsView = displayedSatsInView(); logf("GPS module active: %s", gpsModuleToString(activeGpsModule()).c_str()); @@ -1013,8 +1205,10 @@ static void reportStatusToSerial() { appendGpsSnapshot("status"); } -static void maybeAnnounceGpsTransitions() { - if (isUnsupportedGpsMode()) { +static void maybeAnnounceGpsTransitions() +{ + if (isUnsupportedGpsMode()) + { return; } @@ -1024,7 +1218,8 @@ static void maybeAnnounceGpsTransitions() { bool hasSats = sats > 0; bool hasUtc = displayHasFreshUtc(); - if (!g_satellitesAcquiredAnnounced && !g_prevHadSatellites && hasSats) { + if (!g_satellitesAcquiredAnnounced && !g_prevHadSatellites && hasSats) + { String rtc = formatRtcNow(); char l2[28]; char l3[28]; @@ -1036,7 +1231,8 @@ static void maybeAnnounceGpsTransitions() { g_satellitesAcquiredAnnounced = true; } - if (!g_timeAcquiredAnnounced && !g_prevHadValidUtc && hasUtc) { + if (!g_timeAcquiredAnnounced && !g_prevHadValidUtc && hasUtc) + { char line2[40]; char line3[28]; char line4[28]; @@ -1061,8 +1257,10 @@ static void maybeAnnounceGpsTransitions() { g_prevHadValidUtc = hasUtc; } -static void drawMinuteStatus() { - if (isUnsupportedGpsMode()) { +static void drawMinuteStatus() +{ + if (isUnsupportedGpsMode()) + { oledShowLines("GPS module mismatch", ("Expected: " + gpsModuleToString(kExpectedGpsModule)).c_str(), ("Detected: " + gpsModuleToString(activeGpsModule())).c_str(), @@ -1075,7 +1273,8 @@ static void drawMinuteStatus() { uint8_t satsUsed = displayedSatsUsed(); uint8_t satsView = displayedSatsInView(); - if (displayHasFreshUtc()) { + if (displayHasFreshUtc()) + { char line2[40]; char line3[28]; char line4[28]; @@ -1096,7 +1295,8 @@ static void drawMinuteStatus() { } String rtc = formatRtcNow(); - if (satsUsed > 0 || satsView > 0) { + if (satsUsed > 0 || satsView > 0) + { char line2[28]; char line3[28]; snprintf(line2, sizeof(line2), "Used: %u", (unsigned)satsUsed); @@ -1106,21 +1306,26 @@ static void drawMinuteStatus() { (unsigned)satsUsed, (unsigned)satsView, rtc.c_str()); - } else { + } + else + { oledShowLines("Unable to acquire", "satellites", "Take me outside so I", "can see satellites", rtc.c_str()); logf("Unable to acquire satellites. %s", rtc.c_str()); } } -static bool shouldRefreshDisplay() { +static bool shouldRefreshDisplay() +{ uint32_t now = millis(); - if (g_lastDisplayRefreshMs != 0 && (uint32_t)(now - g_lastDisplayRefreshMs) < kDisplayRefreshMinMs) { + if (g_lastDisplayRefreshMs != 0 && (uint32_t)(now - g_lastDisplayRefreshMs) < kDisplayRefreshMinMs) + { return false; } uint8_t satsUsed = displayedSatsUsed(); uint8_t satsView = displayedSatsInView(); bool hasUtc = displayHasFreshUtc(); - if (!g_haveLastDrawnState) { + if (!g_haveLastDrawnState) + { return true; } return satsUsed != g_lastDrawnSatsUsed || @@ -1128,7 +1333,8 @@ static bool shouldRefreshDisplay() { hasUtc != g_lastDrawnValidUtc; } -static void markDisplayStateDrawn() { +static void markDisplayStateDrawn() +{ g_lastDrawnSatsUsed = displayedSatsUsed(); g_lastDrawnSatsView = displayedSatsInView(); g_lastDrawnValidUtc = displayHasFreshUtc(); @@ -1136,7 +1342,8 @@ static void markDisplayStateDrawn() { g_lastDisplayRefreshMs = millis(); } -void setup() { +void setup() +{ Serial.begin(115200); delay(kSerialDelayMs); @@ -1145,25 +1352,34 @@ void setup() { Serial.println("=================================================="); Serial.printf("Build: %s %s\r\n", kBuildDate, kBuildTime); - if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) { + if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) + { logf("PMU init failed"); } // SPI Flash File System ("SPIFFS") is used for logging GPS diagnostics, - // which may be helpful for analyzing GPS behavior in different - //environments and over time. + // which may be helpful for analyzing GPS behavior in different + // environments and over time. g_spiffsReady = SPIFFS.begin(true); - if (!g_spiffsReady) { + if (!g_spiffsReady) + { logf("SPIFFS mount failed"); - } else if (!ensureGpsLogDirectory()) { + } + else if (!ensureGpsLogDirectory()) + { logf("GPS log directory create/open failed"); - } else { + } + else + { File file = SPIFFS.open(kGpsLogPath, FILE_WRITE); - if (file) { + if (file) + { file.println("Exercise 09 GPS diagnostics"); file.printf("Build: %s %s\r\n", kBuildDate, kBuildTime); file.close(); - } else { + } + else + { logf("GPS log file open failed: %s", kGpsLogPath); } } @@ -1174,10 +1390,11 @@ void setup() { g_oled.begin(); String buildStamp = buildStampShort(); oledShowLines("09_GPS_Time", buildStamp.c_str(), "Booting..."); - // The GPS startup probe may take a while, - //especially for a cold start. Log some + // The GPS startup probe may take a while, + // especially for a cold start. Log some SdWatcherConfig sdCfg{}; - if (!g_sd.begin(sdCfg, nullptr)) { + if (!g_sd.begin(sdCfg, nullptr)) + { logf("SD startup manager begin() failed"); } @@ -1200,24 +1417,28 @@ void setup() { oledShowLines("GPS startup probe", "Checking satellites", "and GPS time..."); uint32_t probeWindowMs = startupProbeWindowMs(); - if (kExpectedGpsModule == GpsModuleKind::UBLOX) { + if (kExpectedGpsModule == GpsModuleKind::UBLOX) + { logf("UBLOX startup window: %lu ms (allowing cold start acquisition)", (unsigned long)probeWindowMs); } uint32_t probeStart = millis(); uint32_t lastProbeUiMs = 0; - while ((uint32_t)(millis() - probeStart) < probeWindowMs) { + while ((uint32_t)(millis() - probeStart) < probeWindowMs) + { pollSerialConsole(); pollGpsSerial(); g_sd.update(); uint32_t now = millis(); - if ((uint32_t)(now - g_lastGpsDiagnosticLogMs) >= kGpsDiagnosticLogMs) { + if ((uint32_t)(now - g_lastGpsDiagnosticLogMs) >= kGpsDiagnosticLogMs) + { g_lastGpsDiagnosticLogMs = now; appendGpsSnapshot("startup_wait"); } - if ((uint32_t)(now - lastProbeUiMs) >= 1000) { + if ((uint32_t)(now - lastProbeUiMs) >= 1000) + { lastProbeUiMs = now; char l3[28]; char l4[30]; @@ -1239,22 +1460,26 @@ void setup() { g_lastGpsDiagnosticLogMs = millis(); } -void loop() { +void loop() +{ pollSerialConsole(); pollGpsSerial(); g_sd.update(); maybeAnnounceGpsTransitions(); uint32_t now = millis(); - if (shouldRefreshDisplay()) { + if (shouldRefreshDisplay()) + { drawMinuteStatus(); markDisplayStateDrawn(); } - if ((uint32_t)(now - g_lastGpsDiagnosticLogMs) >= kGpsDiagnosticLogMs) { + if ((uint32_t)(now - g_lastGpsDiagnosticLogMs) >= kGpsDiagnosticLogMs) + { g_lastGpsDiagnosticLogMs = now; appendGpsSnapshot("periodic"); } - if ((uint32_t)(now - g_lastMinuteReportMs) >= kMinuteMs) { + if ((uint32_t)(now - g_lastMinuteReportMs) >= kMinuteMs) + { g_lastMinuteReportMs = now; drawMinuteStatus(); markDisplayStateDrawn(); diff --git a/lib/tbeam_clock/src/TBeamClock.cpp b/lib/tbeam_clock/src/TBeamClock.cpp index 326401f..98ede4e 100644 --- a/lib/tbeam_clock/src/TBeamClock.cpp +++ b/lib/tbeam_clock/src/TBeamClock.cpp @@ -8,284 +8,333 @@ #define OLED_SCL 18 #endif -namespace tbeam { +namespace tbeam +{ -TBeamClock::TBeamClock(TwoWire& wire) : wire_(wire) {} + TBeamClock::TBeamClock(TwoWire &wire) : wire_(wire) {} -bool TBeamClock::begin(const ClockConfig& config) { - config_ = config; - clearError(); - - if (config_.sda < 0) { - config_.sda = OLED_SDA; - } - if (config_.scl < 0) { - config_.scl = OLED_SCL; - } - - if (config_.beginWire) { - wire_.begin(config_.sda, config_.scl); - } - - DateTime dt{}; - bool lowVoltage = false; - ready_ = readRtc(dt, lowVoltage); - if (!ready_) { - setError("RTC read failed"); - valid_ = false; - return false; - } - - lowVoltage_ = lowVoltage; - lastRtc_ = dt; - valid_ = !lowVoltage && isValidDateTime(dt); - lastEpoch_ = valid_ ? toEpochSeconds(dt) : 0; - if (lowVoltage) { - setError("RTC low-voltage flag set"); - } else if (!valid_) { - setError("RTC date/time invalid"); - } - return true; -} - -void TBeamClock::update() { - DateTime dt{}; - bool lowVoltage = false; - if (!readRtc(dt, lowVoltage)) { - ready_ = false; - valid_ = false; - setError("RTC read failed"); - return; - } - - ready_ = true; - lowVoltage_ = lowVoltage; - lastRtc_ = dt; - valid_ = !lowVoltage && isValidDateTime(dt); - lastEpoch_ = valid_ ? toEpochSeconds(dt) : 0; - if (valid_) { + bool TBeamClock::begin(const ClockConfig &config) + { + config_ = config; clearError(); - } else if (lowVoltage) { - setError("RTC low-voltage flag set"); - } else { - setError("RTC date/time invalid"); - } -} -bool TBeamClock::readRtc(DateTime& out, bool& lowVoltageFlag) const { - wire_.beginTransmission(config_.rtcAddress); - wire_.write(0x02); - if (wire_.endTransmission(false) != 0) { - return false; + if (config_.sda < 0) + { + config_.sda = OLED_SDA; + } + if (config_.scl < 0) + { + config_.scl = OLED_SCL; + } + + if (config_.beginWire) + { + wire_.begin(config_.sda, config_.scl); + } + + DateTime dt{}; + bool lowVoltage = false; + ready_ = readRtc(dt, lowVoltage); + if (!ready_) + { + setError("RTC read failed"); + valid_ = false; + return false; + } + + lowVoltage_ = lowVoltage; + lastRtc_ = dt; + valid_ = !lowVoltage && isValidDateTime(dt); + lastEpoch_ = valid_ ? toEpochSeconds(dt) : 0; + if (lowVoltage) + { + setError("RTC low-voltage flag set"); + } + else if (!valid_) + { + setError("RTC date/time invalid"); + } + return true; } - const uint8_t need = 7; - const uint8_t got = wire_.requestFrom((int)config_.rtcAddress, (int)need); - if (got != need) { - return false; + void TBeamClock::update() + { + DateTime dt{}; + bool lowVoltage = false; + if (!readRtc(dt, lowVoltage)) + { + ready_ = false; + valid_ = false; + setError("RTC read failed"); + return; + } + + ready_ = true; + lowVoltage_ = lowVoltage; + lastRtc_ = dt; + valid_ = !lowVoltage && isValidDateTime(dt); + lastEpoch_ = valid_ ? toEpochSeconds(dt) : 0; + if (valid_) + { + clearError(); + } + else if (lowVoltage) + { + setError("RTC low-voltage flag set"); + } + else + { + setError("RTC date/time invalid"); + } } - const uint8_t sec = wire_.read(); - const uint8_t min = wire_.read(); - const uint8_t hour = wire_.read(); - const uint8_t day = wire_.read(); - const uint8_t weekday = wire_.read(); - const uint8_t month = wire_.read(); - const uint8_t year = wire_.read(); + bool TBeamClock::readRtc(DateTime &out, bool &lowVoltageFlag) const + { + wire_.beginTransmission(config_.rtcAddress); + wire_.write(0x02); + if (wire_.endTransmission(false) != 0) + { + return false; + } - lowVoltageFlag = (sec & 0x80U) != 0; - out.second = fromBcd(sec & 0x7FU); - out.minute = fromBcd(min & 0x7FU); - out.hour = fromBcd(hour & 0x3FU); - out.day = fromBcd(day & 0x3FU); - out.weekday = fromBcd(weekday & 0x07U); - out.month = fromBcd(month & 0x1FU); - const uint8_t yy = fromBcd(year); - out.year = (month & 0x80U) ? (1900U + yy) : (2000U + yy); - return true; -} + const uint8_t need = 7; + const uint8_t got = wire_.requestFrom((int)config_.rtcAddress, (int)need); + if (got != need) + { + return false; + } -bool TBeamClock::readValidRtc(DateTime& out, int64_t* epochOut) const { - bool lowVoltage = false; - if (!readRtc(out, lowVoltage) || lowVoltage || !isValidDateTime(out)) { - return false; - } - if (epochOut) { - *epochOut = toEpochSeconds(out); - } - return true; -} + const uint8_t sec = wire_.read(); + const uint8_t min = wire_.read(); + const uint8_t hour = wire_.read(); + const uint8_t day = wire_.read(); + const uint8_t weekday = wire_.read(); + const uint8_t month = wire_.read(); + const uint8_t year = wire_.read(); -bool TBeamClock::writeRtc(const DateTime& dt) const { - if (!isValidDateTime(dt)) { - return false; + lowVoltageFlag = (sec & 0x80U) != 0; + out.second = fromBcd(sec & 0x7FU); + out.minute = fromBcd(min & 0x7FU); + out.hour = fromBcd(hour & 0x3FU); + out.day = fromBcd(day & 0x3FU); + out.weekday = fromBcd(weekday & 0x07U); + out.month = fromBcd(month & 0x1FU); + const uint8_t yy = fromBcd(year); + out.year = (month & 0x80U) ? (1900U + yy) : (2000U + yy); + return true; } - wire_.beginTransmission(config_.rtcAddress); - wire_.write(0x02); - wire_.write(toBcd(dt.second) & 0x7FU); - wire_.write(toBcd(dt.minute) & 0x7FU); - wire_.write(toBcd(dt.hour) & 0x3FU); - wire_.write(toBcd(dt.day) & 0x3FU); - wire_.write(toBcd(dt.weekday) & 0x07U); - - uint8_t monthReg = toBcd(dt.month) & 0x1FU; - if (dt.year < 2000U) { - monthReg |= 0x80U; - } - wire_.write(monthReg); - wire_.write(toBcd((uint8_t)(dt.year % 100U))); - return wire_.endTransmission() == 0; -} - -bool TBeamClock::isValidDateTime(const DateTime& dt) { - if (dt.year < 2000U || dt.year > 2099U) return false; - if (dt.month < 1U || dt.month > 12U) return false; - if (dt.day < 1U || dt.day > daysInMonth(dt.year, dt.month)) return false; - if (dt.hour > 23U || dt.minute > 59U || dt.second > 59U) return false; - return true; -} - -int64_t TBeamClock::toEpochSeconds(const DateTime& dt) { - const int64_t days = daysFromCivil((int)dt.year, dt.month, dt.day); - return days * 86400LL + (int64_t)dt.hour * 3600LL + (int64_t)dt.minute * 60LL + (int64_t)dt.second; -} - -bool TBeamClock::fromEpochSeconds(int64_t seconds, DateTime& out) { - if (seconds < 0) { - return false; + bool TBeamClock::readValidRtc(DateTime &out, int64_t *epochOut) const + { + bool lowVoltage = false; + if (!readRtc(out, lowVoltage) || lowVoltage || !isValidDateTime(out)) + { + return false; + } + if (epochOut) + { + *epochOut = toEpochSeconds(out); + } + return true; } - int64_t days = seconds / 86400LL; - int64_t remainder = seconds % 86400LL; - if (remainder < 0) { - remainder += 86400LL; - days -= 1; + bool TBeamClock::writeRtc(const DateTime &dt) const + { + if (!isValidDateTime(dt)) + { + return false; + } + + wire_.beginTransmission(config_.rtcAddress); + wire_.write(0x02); + wire_.write(toBcd(dt.second) & 0x7FU); + wire_.write(toBcd(dt.minute) & 0x7FU); + wire_.write(toBcd(dt.hour) & 0x3FU); + wire_.write(toBcd(dt.day) & 0x3FU); + wire_.write(toBcd(dt.weekday) & 0x07U); + + uint8_t monthReg = toBcd(dt.month) & 0x1FU; + if (dt.year < 2000U) + { + monthReg |= 0x80U; + } + wire_.write(monthReg); + wire_.write(toBcd((uint8_t)(dt.year % 100U))); + return wire_.endTransmission() == 0; } - out.hour = (uint8_t)(remainder / 3600LL); - remainder %= 3600LL; - out.minute = (uint8_t)(remainder / 60LL); - out.second = (uint8_t)(remainder % 60LL); - - days += 719468; - const int era = (days >= 0 ? days : days - 146096) / 146097; - const unsigned doe = (unsigned)(days - era * 146097); - const unsigned yoe = (doe - doe / 1460U + doe / 36524U - doe / 146096U) / 365U; - int year = (int)yoe + era * 400; - const unsigned doy = doe - (365U * yoe + yoe / 4U - yoe / 100U); - const unsigned mp = (5U * doy + 2U) / 153U; - const unsigned day = doy - (153U * mp + 2U) / 5U + 1U; - const unsigned month = mp + (mp < 10U ? 3U : (unsigned)-9); - year += (month <= 2U); - - out.year = (uint16_t)year; - out.month = (uint8_t)month; - out.day = (uint8_t)day; - out.weekday = 0; - return isValidDateTime(out); -} - -void TBeamClock::formatIsoUtc(const DateTime& dt, char* out, size_t outSize) { - snprintf(out, - outSize, - "%04u-%02u-%02uT%02u:%02u:%02uZ", - (unsigned)dt.year, - (unsigned)dt.month, - (unsigned)dt.day, - (unsigned)dt.hour, - (unsigned)dt.minute, - (unsigned)dt.second); -} - -void TBeamClock::formatCompactUtc(const DateTime& dt, char* out, size_t outSize) { - snprintf(out, - outSize, - "%04u%02u%02u_%02u%02u%02u", - (unsigned)dt.year, - (unsigned)dt.month, - (unsigned)dt.day, - (unsigned)dt.hour, - (unsigned)dt.minute, - (unsigned)dt.second); -} - -void TBeamClock::makeRunId(const DateTime& dt, const char* boardId, char* out, size_t outSize) { - snprintf(out, - outSize, - "%04u%02u%02u_%02u%02u%02u_%s", - (unsigned)dt.year, - (unsigned)dt.month, - (unsigned)dt.day, - (unsigned)dt.hour, - (unsigned)dt.minute, - (unsigned)dt.second, - boardId ? boardId : "NODE"); -} - -bool TBeamClock::parseDateTime(const char* text, DateTime& out) { - if (!text) { - return false; - } - int y = 0; - int mo = 0; - int d = 0; - int h = 0; - int mi = 0; - int s = 0; - if (sscanf(text, "%d-%d-%d %d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6 && - sscanf(text, "%d-%d-%dT%d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6) { - return false; + bool TBeamClock::isValidDateTime(const DateTime &dt) + { + if (dt.year < 2000U || dt.year > 2099U) + return false; + if (dt.month < 1U || dt.month > 12U) + return false; + if (dt.day < 1U || dt.day > daysInMonth(dt.year, dt.month)) + return false; + if (dt.hour > 23U || dt.minute > 59U || dt.second > 59U) + return false; + return true; } - out.year = (uint16_t)y; - out.month = (uint8_t)mo; - out.day = (uint8_t)d; - out.hour = (uint8_t)h; - out.minute = (uint8_t)mi; - out.second = (uint8_t)s; - out.weekday = 0; - return isValidDateTime(out); -} - -uint8_t TBeamClock::toBcd(uint8_t value) { - return (uint8_t)(((value / 10U) << 4U) | (value % 10U)); -} - -uint8_t TBeamClock::fromBcd(uint8_t value) { - return (uint8_t)(((value >> 4U) * 10U) + (value & 0x0FU)); -} - -bool TBeamClock::isLeapYear(uint16_t year) { - return ((year % 4U) == 0U && (year % 100U) != 0U) || ((year % 400U) == 0U); -} - -uint8_t TBeamClock::daysInMonth(uint16_t year, uint8_t month) { - static const uint8_t kDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - if (month == 2U) { - return (uint8_t)(isLeapYear(year) ? 29U : 28U); + int64_t TBeamClock::toEpochSeconds(const DateTime &dt) + { + const int64_t days = daysFromCivil((int)dt.year, dt.month, dt.day); + return days * 86400LL + (int64_t)dt.hour * 3600LL + (int64_t)dt.minute * 60LL + (int64_t)dt.second; } - if (month >= 1U && month <= 12U) { - return kDays[month - 1U]; + + bool TBeamClock::fromEpochSeconds(int64_t seconds, DateTime &out) + { + if (seconds < 0) + { + return false; + } + + int64_t days = seconds / 86400LL; + int64_t remainder = seconds % 86400LL; + if (remainder < 0) + { + remainder += 86400LL; + days -= 1; + } + + out.hour = (uint8_t)(remainder / 3600LL); + remainder %= 3600LL; + out.minute = (uint8_t)(remainder / 60LL); + out.second = (uint8_t)(remainder % 60LL); + + days += 719468; + const int era = (days >= 0 ? days : days - 146096) / 146097; + const unsigned doe = (unsigned)(days - era * 146097); + const unsigned yoe = (doe - doe / 1460U + doe / 36524U - doe / 146096U) / 365U; + int year = (int)yoe + era * 400; + const unsigned doy = doe - (365U * yoe + yoe / 4U - yoe / 100U); + const unsigned mp = (5U * doy + 2U) / 153U; + const unsigned day = doy - (153U * mp + 2U) / 5U + 1U; + const unsigned month = mp + (mp < 10U ? 3U : (unsigned)-9); + year += (month <= 2U); + + out.year = (uint16_t)year; + out.month = (uint8_t)month; + out.day = (uint8_t)day; + out.weekday = 0; + return isValidDateTime(out); } - return 0; -} -int64_t TBeamClock::daysFromCivil(int year, unsigned month, unsigned day) { - year -= (month <= 2U); - const int era = (year >= 0 ? year : year - 399) / 400; - const unsigned yoe = (unsigned)(year - era * 400); - const unsigned doy = (153U * (month + (month > 2U ? (unsigned)-3 : 9U)) + 2U) / 5U + day - 1U; - const unsigned doe = yoe * 365U + yoe / 4U - yoe / 100U + doy; - return era * 146097 + (int)doe - 719468; -} + void TBeamClock::formatIsoUtc(const DateTime &dt, char *out, size_t outSize) + { + snprintf(out, + outSize, + "%04u-%02u-%02uT%02u:%02u:%02uZ", + (unsigned)dt.year, + (unsigned)dt.month, + (unsigned)dt.day, + (unsigned)dt.hour, + (unsigned)dt.minute, + (unsigned)dt.second); + } -void TBeamClock::setError(const char* message) const { - strlcpy(lastError_, message ? message : "", sizeof(lastError_)); -} + void TBeamClock::formatCompactUtc(const DateTime &dt, char *out, size_t outSize) + { + snprintf(out, + outSize, + "%04u%02u%02u_%02u%02u%02u", + (unsigned)dt.year, + (unsigned)dt.month, + (unsigned)dt.day, + (unsigned)dt.hour, + (unsigned)dt.minute, + (unsigned)dt.second); + } -void TBeamClock::clearError() const { - lastError_[0] = '\0'; -} + void TBeamClock::makeRunId(const DateTime &dt, const char *boardId, char *out, size_t outSize) + { + snprintf(out, + outSize, + "%04u%02u%02u_%02u%02u%02u_%s", + (unsigned)dt.year, + (unsigned)dt.month, + (unsigned)dt.day, + (unsigned)dt.hour, + (unsigned)dt.minute, + (unsigned)dt.second, + boardId ? boardId : "NODE"); + } -} // namespace tbeam + bool TBeamClock::parseDateTime(const char *text, DateTime &out) + { + if (!text) + { + return false; + } + int y = 0; + int mo = 0; + int d = 0; + int h = 0; + int mi = 0; + int s = 0; + if (sscanf(text, "%d-%d-%d %d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6 && + sscanf(text, "%d-%d-%dT%d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6) + { + return false; + } + + out.year = (uint16_t)y; + out.month = (uint8_t)mo; + out.day = (uint8_t)d; + out.hour = (uint8_t)h; + out.minute = (uint8_t)mi; + out.second = (uint8_t)s; + out.weekday = 0; + return isValidDateTime(out); + } + + uint8_t TBeamClock::toBcd(uint8_t value) + { + return (uint8_t)(((value / 10U) << 4U) | (value % 10U)); + } + + uint8_t TBeamClock::fromBcd(uint8_t value) + { + return (uint8_t)(((value >> 4U) * 10U) + (value & 0x0FU)); + } + + bool TBeamClock::isLeapYear(uint16_t year) + { + return ((year % 4U) == 0U && (year % 100U) != 0U) || ((year % 400U) == 0U); + } + + uint8_t TBeamClock::daysInMonth(uint16_t year, uint8_t month) + { + static const uint8_t kDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month == 2U) + { + return (uint8_t)(isLeapYear(year) ? 29U : 28U); + } + if (month >= 1U && month <= 12U) + { + return kDays[month - 1U]; + } + return 0; + } + + int64_t TBeamClock::daysFromCivil(int year, unsigned month, unsigned day) + { + year -= (month <= 2U); + const int era = (year >= 0 ? year : year - 399) / 400; + const unsigned yoe = (unsigned)(year - era * 400); + const unsigned doy = (153U * (month + (month > 2U ? (unsigned)-3 : 9U)) + 2U) / 5U + day - 1U; + const unsigned doe = yoe * 365U + yoe / 4U - yoe / 100U + doy; + return era * 146097 + (int)doe - 719468; + } + + void TBeamClock::setError(const char *message) const + { + strlcpy(lastError_, message ? message : "", sizeof(lastError_)); + } + + void TBeamClock::clearError() const + { + lastError_[0] = '\0'; + } + +} // namespace tbeam diff --git a/lib/tbeam_clock/src/TBeamClock.h b/lib/tbeam_clock/src/TBeamClock.h index ad85b88..35c7683 100644 --- a/lib/tbeam_clock/src/TBeamClock.h +++ b/lib/tbeam_clock/src/TBeamClock.h @@ -3,69 +3,73 @@ #include #include -namespace tbeam { +namespace tbeam +{ -struct DateTime { - uint16_t year = 0; - uint8_t month = 0; - uint8_t day = 0; - uint8_t hour = 0; - uint8_t minute = 0; - uint8_t second = 0; - uint8_t weekday = 0; -}; + struct DateTime + { + uint16_t year = 0; + uint8_t month = 0; + uint8_t day = 0; + uint8_t hour = 0; + uint8_t minute = 0; + uint8_t second = 0; + uint8_t weekday = 0; + }; -struct ClockConfig { - uint8_t rtcAddress = 0x51; - int sda = -1; - int scl = -1; - bool beginWire = true; -}; + struct ClockConfig + { + uint8_t rtcAddress = 0x51; + int sda = -1; + int scl = -1; + bool beginWire = true; + }; -class TBeamClock { - public: - explicit TBeamClock(TwoWire& wire = Wire1); + class TBeamClock + { + public: + explicit TBeamClock(TwoWire &wire = Wire1); - bool begin(const ClockConfig& config = ClockConfig{}); - void update(); + bool begin(const ClockConfig &config = ClockConfig{}); + void update(); - bool readRtc(DateTime& out, bool& lowVoltageFlag) const; - bool readValidRtc(DateTime& out, int64_t* epochOut = nullptr) const; - bool writeRtc(const DateTime& dt) const; + bool readRtc(DateTime &out, bool &lowVoltageFlag) const; + bool readValidRtc(DateTime &out, int64_t *epochOut = nullptr) const; + bool writeRtc(const DateTime &dt) const; - bool ready() const { return ready_; } - bool valid() const { return valid_; } - bool lowVoltage() const { return lowVoltage_; } - const DateTime& lastRtc() const { return lastRtc_; } - int64_t lastEpoch() const { return lastEpoch_; } - const char* lastError() const { return lastError_; } + bool ready() const { return ready_; } + bool valid() const { return valid_; } + bool lowVoltage() const { return lowVoltage_; } + const DateTime &lastRtc() const { return lastRtc_; } + int64_t lastEpoch() const { return lastEpoch_; } + const char *lastError() const { return lastError_; } - static bool isValidDateTime(const DateTime& dt); - static int64_t toEpochSeconds(const DateTime& dt); - static bool fromEpochSeconds(int64_t seconds, DateTime& out); - static void formatIsoUtc(const DateTime& dt, char* out, size_t outSize); - static void formatCompactUtc(const DateTime& dt, char* out, size_t outSize); - static void makeRunId(const DateTime& dt, const char* boardId, char* out, size_t outSize); - static bool parseDateTime(const char* text, DateTime& out); + static bool isValidDateTime(const DateTime &dt); + static int64_t toEpochSeconds(const DateTime &dt); + static bool fromEpochSeconds(int64_t seconds, DateTime &out); + static void formatIsoUtc(const DateTime &dt, char *out, size_t outSize); + static void formatCompactUtc(const DateTime &dt, char *out, size_t outSize); + static void makeRunId(const DateTime &dt, const char *boardId, char *out, size_t outSize); + static bool parseDateTime(const char *text, DateTime &out); - private: - static uint8_t toBcd(uint8_t value); - static uint8_t fromBcd(uint8_t value); - static bool isLeapYear(uint16_t year); - static uint8_t daysInMonth(uint16_t year, uint8_t month); - static int64_t daysFromCivil(int year, unsigned month, unsigned day); + private: + static uint8_t toBcd(uint8_t value); + static uint8_t fromBcd(uint8_t value); + static bool isLeapYear(uint16_t year); + static uint8_t daysInMonth(uint16_t year, uint8_t month); + static int64_t daysFromCivil(int year, unsigned month, unsigned day); - void setError(const char* message) const; - void clearError() const; + void setError(const char *message) const; + void clearError() const; - TwoWire& wire_; - ClockConfig config_{}; - bool ready_ = false; - bool valid_ = false; - bool lowVoltage_ = false; - DateTime lastRtc_{}; - int64_t lastEpoch_ = 0; - mutable char lastError_[128] = {}; -}; + TwoWire &wire_; + ClockConfig config_{}; + bool ready_ = false; + bool valid_ = false; + bool lowVoltage_ = false; + DateTime lastRtc_{}; + int64_t lastEpoch_ = 0; + mutable char lastError_[128] = {}; + }; -} // namespace tbeam +} // namespace tbeam diff --git a/lib/tbeam_logger/src/TBeamLogger.cpp b/lib/tbeam_logger/src/TBeamLogger.cpp index 0e3c104..f7bbf21 100644 --- a/lib/tbeam_logger/src/TBeamLogger.cpp +++ b/lib/tbeam_logger/src/TBeamLogger.cpp @@ -1,78 +1,98 @@ #include "TBeamLogger.h" -namespace tbeam { +namespace tbeam +{ -bool TBeamLogger::begin(Print& serial, TBeamStorage* storage, const LoggerConfig& config) { - serial_ = &serial; - storage_ = storage; - config_ = config; - lastFlushMs_ = millis(); - return true; -} - -void TBeamLogger::update() { - if (!config_.autoFlush || !storage_) { - return; + bool TBeamLogger::begin(Print &serial, TBeamStorage *storage, const LoggerConfig &config) + { + serial_ = &serial; + storage_ = storage; + config_ = config; + lastFlushMs_ = millis(); + return true; } - const uint32_t now = millis(); - if ((uint32_t)(now - lastFlushMs_) >= config_.flushIntervalMs) { - storage_->flush(); - lastFlushMs_ = now; + + void TBeamLogger::update() + { + if (!config_.autoFlush || !storage_) + { + return; + } + const uint32_t now = millis(); + if ((uint32_t)(now - lastFlushMs_) >= config_.flushIntervalMs) + { + storage_->flush(); + lastFlushMs_ = now; + } } -} -bool TBeamLogger::openLog(const char* path) { - return storage_ && storage_->openLog(path); -} - -bool TBeamLogger::openUniqueLog(const char* prefix, const char* extension) { - if (!storage_) { - return false; + bool TBeamLogger::openLog(const char *path) + { + return storage_ && storage_->openLog(path); } - char path[128]; - if (!storage_->makeUniqueLogPath(prefix, extension, path, sizeof(path))) { - return false; + + bool TBeamLogger::openUniqueLog(const char *prefix, const char *extension) + { + if (!storage_) + { + return false; + } + char path[128]; + if (!storage_->makeUniqueLogPath(prefix, extension, path, sizeof(path))) + { + return false; + } + return storage_->openLog(path); } - return storage_->openLog(path); -} -const char* TBeamLogger::currentLogPath() const { - return storage_ ? storage_->currentLogPath() : ""; -} - -bool TBeamLogger::storageReady() const { - return storage_ && storage_->ready() && storage_->isLogOpen(); -} - -void TBeamLogger::flush() { - if (storage_) { - storage_->flush(); + const char *TBeamLogger::currentLogPath() const + { + return storage_ ? storage_->currentLogPath() : ""; } - if (serial_) { - serial_->flush(); - } -} -void TBeamLogger::closeLog() { - if (storage_) { - storage_->closeLog(); + bool TBeamLogger::storageReady() const + { + return storage_ && storage_->ready() && storage_->isLogOpen(); } -} -size_t TBeamLogger::write(uint8_t value) { - return write(&value, 1); -} - -size_t TBeamLogger::write(const uint8_t* buffer, size_t size) { - size_t serialWrote = 0; - size_t storageWrote = 0; - if (config_.echoSerial && serial_) { - serialWrote = serial_->write(buffer, size); + void TBeamLogger::flush() + { + if (storage_) + { + storage_->flush(); + } + if (serial_) + { + serial_->flush(); + } } - if (config_.echoStorage && storage_ && storage_->isLogOpen()) { - storageWrote = storage_->write(buffer, size); - } - return storageWrote > 0 ? storageWrote : serialWrote; -} -} // namespace tbeam + void TBeamLogger::closeLog() + { + if (storage_) + { + storage_->closeLog(); + } + } + + size_t TBeamLogger::write(uint8_t value) + { + return write(&value, 1); + } + + size_t TBeamLogger::write(const uint8_t *buffer, size_t size) + { + size_t serialWrote = 0; + size_t storageWrote = 0; + if (config_.echoSerial && serial_) + { + serialWrote = serial_->write(buffer, size); + } + if (config_.echoStorage && storage_ && storage_->isLogOpen()) + { + storageWrote = storage_->write(buffer, size); + } + return storageWrote > 0 ? storageWrote : serialWrote; + } + +} // namespace tbeam From 04afd13532986d9fee4fdbc9f1d04e32da9924c8 Mon Sep 17 00:00:00 2001 From: John Poole Date: Fri, 24 Apr 2026 16:31:03 -0700 Subject: [PATCH 2/4] works --- .../24_nvs/lib/tbeam_display/library.json | 14 + .../lib/tbeam_display/src/TBeamDisplay.cpp | 204 ++++++++++++ .../lib/tbeam_display/src/TBeamDisplay.h | 70 ++++ exercises/24_nvs/platformio.ini | 77 +++++ exercises/24_nvs/scripts/set_build_epoch.py | 13 + exercises/24_nvs/src/main.cpp | 307 ++++++++++++++++++ 6 files changed, 685 insertions(+) create mode 100644 exercises/24_nvs/lib/tbeam_display/library.json create mode 100644 exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.cpp create mode 100644 exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.h create mode 100644 exercises/24_nvs/platformio.ini create mode 100644 exercises/24_nvs/scripts/set_build_epoch.py create mode 100644 exercises/24_nvs/src/main.cpp diff --git a/exercises/24_nvs/lib/tbeam_display/library.json b/exercises/24_nvs/lib/tbeam_display/library.json new file mode 100644 index 0000000..70a14f6 --- /dev/null +++ b/exercises/24_nvs/lib/tbeam_display/library.json @@ -0,0 +1,14 @@ +{ + "name": "tbeam_display", + "version": "0.1.0", + "description": "Reusable SH1106 OLED display service for LilyGO T-Beam Supreme exercises.", + "frameworks": "arduino", + "platforms": "espressif32", + "dependencies": [ + { + "name": "U8g2", + "owner": "olikraus", + "version": "^2.36.4" + } + ] +} diff --git a/exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.cpp b/exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.cpp new file mode 100644 index 0000000..427a060 --- /dev/null +++ b/exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.cpp @@ -0,0 +1,204 @@ +#include "TBeamDisplay.h" + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif + +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif + +#ifndef OLED_ADDR +#define OLED_ADDR 0x3C +#endif + +namespace tbeam { + +TBeamDisplay::TBeamDisplay(TwoWire& wire) : wire_(wire) {} + +bool TBeamDisplay::begin(const DisplayConfig& config) { + config_ = config; + clearError(); + + if (config_.sda < 0) { + config_.sda = OLED_SDA; + } + if (config_.scl < 0) { + config_.scl = OLED_SCL; + } + if (config_.address == 0) { + config_.address = OLED_ADDR; + } + + if (config_.beginWire) { + wire_.begin(config_.sda, config_.scl); + } + + oled_.setI2CAddress(config_.address << 1); + if (!oled_.begin()) { + ready_ = false; + setError("OLED begin failed"); + return false; + } + + ready_ = true; + setPowerSave(config_.powerSave); + setFont(DisplayFont::NORMAL); + clear(); + return true; +} + +void TBeamDisplay::update() { +} + +void TBeamDisplay::clear() { + if (!ready_) { + return; + } + oled_.clearBuffer(); + oled_.sendBuffer(); +} + +void TBeamDisplay::clearBuffer() { + for (uint8_t i = 0; i < kMaxLines; ++i) { + lines_[i][0] = '\0'; + } +} + +void TBeamDisplay::setPowerSave(bool enabled) { + if (!ready_) { + return; + } + oled_.setPowerSave(enabled ? 1 : 0); + powerSave_ = enabled; +} + +void TBeamDisplay::setFont(DisplayFont font) { + font_ = font; + if (ready_) { + oled_.setFont(fontFor(font_)); + } +} + +void TBeamDisplay::showLines(const char* l1, + const char* l2, + const char* l3, + const char* l4, + const char* l5, + const char* l6) { + setLine(0, l1); + setLine(1, l2); + setLine(2, l3); + setLine(3, l4); + setLine(4, l5); + setLine(5, l6); + renderLines(); +} + +void TBeamDisplay::setLine(uint8_t index, const char* text) { + if (index >= kMaxLines) { + return; + } + strlcpy(lines_[index], text ? text : "", sizeof(lines_[index])); +} + +void TBeamDisplay::renderLines(uint8_t lineCount) { + if (!ready_) { + return; + } + + if (lineCount > kMaxLines) { + lineCount = kMaxLines; + } + + oled_.clearBuffer(); + oled_.setFont(fontFor(font_)); + + uint8_t yStart = 12; + uint8_t yStep = 12; + if (font_ == DisplayFont::SMALL) { + yStart = 10; + yStep = 10; + } else if (font_ == DisplayFont::LARGE) { + yStart = 15; + yStep = 16; + if (lineCount > 4) { + lineCount = 4; + } + } + + for (uint8_t i = 0; i < lineCount; ++i) { + if (lines_[i][0] == '\0') { + continue; + } + oled_.drawUTF8(0, yStart + (i * yStep), lines_[i]); + } + oled_.sendBuffer(); +} + +void TBeamDisplay::appendLine(const char* text) { + for (uint8_t i = 0; i < kMaxLines - 1; ++i) { + strlcpy(lines_[i], lines_[i + 1], sizeof(lines_[i])); + } + setLine(kMaxLines - 1, text); + renderLines(); +} + +void TBeamDisplay::showBoot(const char* title, const char* subtitle, const char* detail) { + setFont(DisplayFont::NORMAL); + showLines(title ? title : "T-Beam", subtitle, detail); +} + +void TBeamDisplay::showStatus(const char* title, const char* left, const char* right, const char* footer) { + if (!ready_) { + return; + } + + oled_.clearBuffer(); + oled_.setFont(u8g2_font_6x10_tf); + if (title) { + oled_.drawUTF8(0, 10, title); + } + oled_.drawHLine(0, 13, 128); + + oled_.setFont(u8g2_font_7x14B_tf); + if (left) { + oled_.drawUTF8(0, 34, left); + } + if (right) { + const int width = oled_.getUTF8Width(right); + int x = 128 - width; + if (x < 0) { + x = 0; + } + oled_.drawUTF8(x, 34, right); + } + + oled_.setFont(u8g2_font_6x10_tf); + if (footer) { + oled_.drawUTF8(0, 60, footer); + } + oled_.sendBuffer(); +} + +void TBeamDisplay::setError(const char* message) { + strlcpy(lastError_, message ? message : "", sizeof(lastError_)); +} + +void TBeamDisplay::clearError() { + lastError_[0] = '\0'; +} + +const uint8_t* TBeamDisplay::fontFor(DisplayFont font) const { + switch (font) { + case DisplayFont::SMALL: + return u8g2_font_5x8_tf; + case DisplayFont::LARGE: + return u8g2_font_7x14B_tf; + case DisplayFont::NORMAL: + default: + return u8g2_font_6x10_tf; + } +} + +} // namespace tbeam diff --git a/exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.h b/exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.h new file mode 100644 index 0000000..bfc5f8f --- /dev/null +++ b/exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +namespace tbeam { + +struct DisplayConfig { + int sda = -1; + int scl = -1; + uint8_t address = 0x3C; + bool beginWire = true; + bool powerSave = false; +}; + +enum class DisplayFont : uint8_t { + SMALL = 0, + NORMAL, + LARGE +}; + +class TBeamDisplay { + public: + static constexpr uint8_t kMaxLines = 6; + static constexpr uint8_t kLineBytes = 32; + + explicit TBeamDisplay(TwoWire& wire = Wire); + + bool begin(const DisplayConfig& config = DisplayConfig{}); + void update(); + + bool ready() const { return ready_; } + bool powerSave() const { return powerSave_; } + const char* lastError() const { return lastError_; } + + void clear(); + void clearBuffer(); + void setPowerSave(bool enabled); + void setFont(DisplayFont font); + void showLines(const char* l1, + const char* l2 = nullptr, + const char* l3 = nullptr, + const char* l4 = nullptr, + const char* l5 = nullptr, + const char* l6 = nullptr); + void setLine(uint8_t index, const char* text); + void renderLines(uint8_t lineCount = kMaxLines); + void appendLine(const char* text); + void showBoot(const char* title, const char* subtitle = nullptr, const char* detail = nullptr); + void showStatus(const char* title, const char* left, const char* right = nullptr, const char* footer = nullptr); + + U8G2& raw() { return oled_; } + + private: + void setError(const char* message); + void clearError(); + const uint8_t* fontFor(DisplayFont font) const; + + TwoWire& wire_; + DisplayConfig config_{}; + U8G2_SH1106_128X64_NONAME_F_HW_I2C oled_{U8G2_R0, U8X8_PIN_NONE}; + bool ready_ = false; + bool powerSave_ = false; + DisplayFont font_ = DisplayFont::NORMAL; + char lines_[kMaxLines][kLineBytes] = {}; + char lastError_[96] = {}; +}; + +} // namespace tbeam diff --git a/exercises/24_nvs/platformio.ini b/exercises/24_nvs/platformio.ini new file mode 100644 index 0000000..b60f92b --- /dev/null +++ b/exercises/24_nvs/platformio.ini @@ -0,0 +1,77 @@ +; 20260423 Codex +; Exercise 24_nvs + +[platformio] +default_envs = cy + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +board_build.partitions = default_8MB.csv +monitor_speed = 115200 +extra_scripts = pre:scripts/set_build_epoch.py +lib_extra_dirs = + ../lib +lib_deps = + Wire + olikraus/U8g2@^2.36.4 + +build_flags = + -I ../lib/tbeam_display/src + -I ../../shared/boards + -D BOARD_MODEL=BOARD_TBEAM_S_V1 + -D OLED_SDA=17 + -D OLED_SCL=18 + -D OLED_ADDR=0x3C + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"AMY\" + -D NODE_LABEL=\"Amy\" + +[env:bob] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"BOB\" + -D NODE_LABEL=\"Bob\" + +[env:cy] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"CY\" + -D NODE_LABEL=\"Cy\" + +[env:dan] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"DAN\" + -D NODE_LABEL=\"Dan\" + +[env:ed] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"ED\" + -D NODE_LABEL=\"Ed\" + +[env:flo] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"FLO\" + -D NODE_LABEL=\"Flo\" + +[env:guy] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"GUY\" + -D NODE_LABEL=\"Guy\" diff --git a/exercises/24_nvs/scripts/set_build_epoch.py b/exercises/24_nvs/scripts/set_build_epoch.py new file mode 100644 index 0000000..033becd --- /dev/null +++ b/exercises/24_nvs/scripts/set_build_epoch.py @@ -0,0 +1,13 @@ +import time +Import("env") + +epoch = int(time.time()) +utc_tag = time.strftime("%Y%m%d_%H%M%S", time.gmtime(epoch)) + +env.Append( + CPPDEFINES=[ + ("BUILD_EPOCH", str(epoch)), + ("FW_BUILD_EPOCH", str(epoch)), + ("FW_BUILD_UTC", '\"%s\"' % utc_tag), + ] +) diff --git a/exercises/24_nvs/src/main.cpp b/exercises/24_nvs/src/main.cpp new file mode 100644 index 0000000..b0603d1 --- /dev/null +++ b/exercises/24_nvs/src/main.cpp @@ -0,0 +1,307 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "TBeamDisplay.h" + +namespace { + +#ifndef BOARD_ID +#define BOARD_ID "CY" +#endif + +#ifndef NODE_LABEL +#define NODE_LABEL "Cy" +#endif + +#ifndef BUILD_EPOCH +#define BUILD_EPOCH 0 +#endif + +using tbeam::DisplayConfig; +using tbeam::DisplayFont; +using tbeam::TBeamDisplay; + +static constexpr const char* kExerciseTitle = "Exercise 24"; +static constexpr const char* kExerciseSubtitle = "NVS Persistence Demo"; +static constexpr const char* kExerciseVersion = "Version 1"; +static constexpr const char* kNamespace = "mag"; +static constexpr const char* kBlobKey = "magcal_blob"; +static constexpr const char* kKeyX = "mag_x"; +static constexpr const char* kKeyY = "mag_y"; +static constexpr const char* kKeyZ = "mag_z"; +static constexpr const char* kKeyEpoch = "mag_epoch"; +static constexpr uint32_t kSplashMs = 15000; +static constexpr uint32_t kInitMessageMs = 2000; + +struct __attribute__((packed)) MagCal { + int16_t x; + int16_t y; + int16_t z; + uint64_t epoch; + uint16_t version; +}; + +static_assert(sizeof(MagCal) == 16, "MagCal layout changed"); + +MagCal magcalibration = {-654, 1129, 464, BUILD_EPOCH, 1}; + +enum class ValueState : uint8_t { + Missing = 0, + NullValue, + Valid, +}; + +struct DisplayField { + ValueState state = ValueState::Missing; + int16_t value = 0; +}; + +struct EpochField { + ValueState state = ValueState::Missing; + uint64_t value = 0; +}; + +struct CalibrationView { + DisplayField x{}; + DisplayField y{}; + DisplayField z{}; + EpochField epoch{}; +}; + +Preferences g_preferences; +TBeamDisplay g_display; +char g_displayLines[4][64] = {}; +uint32_t g_scrollStartMs = 0; + +bool formatEpochUtc(uint64_t epoch, char* out, size_t outSize) { + if (out == nullptr || outSize == 0) { + return false; + } + + if (epoch > static_cast(INT32_MAX)) { + return false; + } + + time_t raw = static_cast(epoch); + struct tm tmUtc; + if (gmtime_r(&raw, &tmUtc) == nullptr) { + return false; + } + + return strftime(out, outSize, "%Y%m%d_%H%M%S", &tmUtc) > 0; +} + +bool isInvalidCalibration(const MagCal& cal) { + if (cal.version == 0 || cal.epoch == 0) { + return true; + } + return cal.x == 0 && cal.y == 0 && cal.z == 0 && cal.epoch == 0 && cal.version == 0; +} + +void assignMissing(CalibrationView& view) { + view = CalibrationView{}; +} + +void assignNull(CalibrationView& view) { + view.x.state = ValueState::NullValue; + view.y.state = ValueState::NullValue; + view.z.state = ValueState::NullValue; + view.epoch.state = ValueState::NullValue; +} + +void assignValid(const MagCal& cal, CalibrationView& view) { + view.x.state = ValueState::Valid; + view.x.value = cal.x; + view.y.state = ValueState::Valid; + view.y.value = cal.y; + view.z.state = ValueState::Valid; + view.z.value = cal.z; + view.epoch.state = ValueState::Valid; + view.epoch.value = cal.epoch; +} + +void formatField(const DisplayField& field, char* out, size_t outSize) { + if (field.state == ValueState::Missing) { + strlcpy(out, "not found", outSize); + return; + } + if (field.state == ValueState::NullValue) { + strlcpy(out, "NULL", outSize); + return; + } + snprintf(out, outSize, "%d", field.value); +} + +void formatEpochField(const EpochField& field, char* out, size_t outSize) { + if (field.state == ValueState::Missing) { + strlcpy(out, "not found", outSize); + return; + } + if (field.state == ValueState::NullValue) { + strlcpy(out, "NULL", outSize); + return; + } + if (!formatEpochUtc(field.value, out, outSize)) { + strlcpy(out, "NULL", outSize); + } +} + +bool writeCalibration(const MagCal& cal) { + const size_t written = g_preferences.putBytes(kBlobKey, &cal, sizeof(cal)); + if (written != sizeof(cal)) { + return false; + } + + const size_t xWritten = g_preferences.putShort(kKeyX, cal.x); + const size_t yWritten = g_preferences.putShort(kKeyY, cal.y); + const size_t zWritten = g_preferences.putShort(kKeyZ, cal.z); + const size_t epochWritten = g_preferences.putULong64(kKeyEpoch, cal.epoch); + return xWritten > 0 && yWritten > 0 && zWritten > 0 && epochWritten > 0; +} + +CalibrationView loadCalibration(bool& initializedDefaults) { + CalibrationView view; + assignMissing(view); + initializedDefaults = false; + + const size_t len = g_preferences.getBytesLength(kBlobKey); + if (len == sizeof(MagCal)) { + MagCal cal{}; + const size_t read = g_preferences.getBytes(kBlobKey, &cal, sizeof(cal)); + if (read == sizeof(cal)) { + if (isInvalidCalibration(cal)) { + assignNull(view); + } else { + assignValid(cal, view); + } + return view; + } + } + + if (writeCalibration(magcalibration)) { + initializedDefaults = true; + assignValid(magcalibration, view); + } + return view; +} + +void showSplash() { + if (!g_display.ready()) { + return; + } + + g_display.setFont(DisplayFont::NORMAL); + g_display.showLines(kExerciseTitle, kExerciseSubtitle, kExerciseVersion); + delay(kSplashMs); +} + +void showMessage(const char* line1, const char* line2 = nullptr) { + if (!g_display.ready()) { + return; + } + + g_display.setFont(DisplayFont::NORMAL); + g_display.showLines(line1, line2); +} + +void showCalibration(const CalibrationView& view) { + char xValue[24]; + char yValue[24]; + char zValue[24]; + char epochValue[24]; + + formatField(view.x, xValue, sizeof(xValue)); + formatField(view.y, yValue, sizeof(yValue)); + formatField(view.z, zValue, sizeof(zValue)); + formatEpochField(view.epoch, epochValue, sizeof(epochValue)); + + snprintf(g_displayLines[0], sizeof(g_displayLines[0]), "magnet calibration.x = %s", xValue); + snprintf(g_displayLines[1], sizeof(g_displayLines[1]), "magnet calibration.y = %s", yValue); + snprintf(g_displayLines[2], sizeof(g_displayLines[2]), "magnet calibration.z = %s", zValue); + snprintf(g_displayLines[3], sizeof(g_displayLines[3]), "magnet calibration.date = %s", epochValue); + g_scrollStartMs = millis(); +} + +void renderCalibrationScreen() { + if (!g_display.ready()) { + return; + } + + U8G2& oled = g_display.raw(); + oled.clearBuffer(); + oled.setFont(u8g2_font_4x6_tf); + + const uint32_t elapsed = millis() - g_scrollStartMs; + const int16_t scrollStep = static_cast(elapsed / 175U); + const uint8_t yPositions[4] = {8, 22, 36, 50}; + + for (uint8_t i = 0; i < 4; ++i) { + const int width = oled.getUTF8Width(g_displayLines[i]); + int16_t x = 0; + if (width > 128) { + const int travel = width - 128 + 8; + x = -static_cast(scrollStep % travel); + } + oled.drawUTF8(x, yPositions[i], g_displayLines[i]); + } + + oled.sendBuffer(); +} + +void logCalibration(const CalibrationView& view) { + char xValue[24]; + char yValue[24]; + char zValue[24]; + char epochValue[24]; + + formatField(view.x, xValue, sizeof(xValue)); + formatField(view.y, yValue, sizeof(yValue)); + formatField(view.z, zValue, sizeof(zValue)); + formatEpochField(view.epoch, epochValue, sizeof(epochValue)); + + Serial.printf("magnet calibration.x = %s\n", xValue); + Serial.printf("magnet calibration.y = %s\n", yValue); + Serial.printf("magnet calibration.z = %s\n", zValue); + Serial.printf("magnet calibration.date = %s\n", epochValue); +} + +} // namespace + +void setup() { + Serial.begin(115200); + delay(200); + + DisplayConfig displayConfig; + displayConfig.powerSave = false; + g_display.begin(displayConfig); + + showSplash(); + + if (!g_preferences.begin(kNamespace, false)) { + showMessage("NVS open failed", kNamespace); + Serial.println("Failed to open Preferences namespace"); + return; + } + + bool initializedDefaults = false; + const CalibrationView view = loadCalibration(initializedDefaults); + + if (initializedDefaults) { + showMessage("Calibration initialized"); + delay(kInitMessageMs); + } + + showCalibration(view); + renderCalibrationScreen(); + logCalibration(view); +} + +void loop() { + renderCalibrationScreen(); + delay(250); +} From 8aff7daa11dc36041ad87080a8409da16713cc41 Mon Sep 17 00:00:00 2001 From: John Poole Date: Sat, 25 Apr 2026 04:19:17 -0700 Subject: [PATCH 3/4] Works, but on jp so sluggish as to work intermittently, will try on eos --- exercises/25_motioncal_tbeam/README.md | 57 +++ exercises/25_motioncal_tbeam/platformio.ini | 77 ++++ .../scripts/set_build_epoch.py | 12 + exercises/25_motioncal_tbeam/src/main.cpp | 348 ++++++++++++++++++ 4 files changed, 494 insertions(+) create mode 100644 exercises/25_motioncal_tbeam/README.md create mode 100644 exercises/25_motioncal_tbeam/platformio.ini create mode 100644 exercises/25_motioncal_tbeam/scripts/set_build_epoch.py create mode 100644 exercises/25_motioncal_tbeam/src/main.cpp diff --git a/exercises/25_motioncal_tbeam/README.md b/exercises/25_motioncal_tbeam/README.md new file mode 100644 index 0000000..164c700 --- /dev/null +++ b/exercises/25_motioncal_tbeam/README.md @@ -0,0 +1,57 @@ +# Exercise 25: MotionCal T-Beam Bridge + +Streams the T-Beam Supreme QMC6310 magnetometer in the ASCII format accepted by +Paul Stoffregen's MotionCal desktop tool. + +https://github.com/PaulStoffregen/MotionCal.git (fetch) + +MotionCal expects: + +```text +Raw:accel_x,accel_y,accel_z,gyro_x,gyro_y,gyro_z,mag_x,mag_y,mag_z +``` + +This exercise only has a magnetometer, so it sends a stationary accelerometer +placeholder and zero gyro: + +```text +Raw:0,0,8192,0,0,0,mag_x,mag_y,mag_z +``` + +The magnetic values are converted from SensorLib Gauss readings into MotionCal's +integer units where 1 count is 0.1 microtesla. + +## Build + +```sh +cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/25_motioncal_tbeam +source /home/jlpoole/rnsenv/bin/activate +pio run +``` + +## Upload + +```sh +source /home/jlpoole/rnsenv/bin/activate +pio run -t upload +``` + +Use a specific board environment if needed: + +```sh +pio run -e guy -t upload +``` + +## MotionCal + +Build and run MotionCal as before: + +```sh +cd /usr/local/src/MotionCal +make WXCONFIG=wx-config LDFLAGS="-lglut -lGLU -lGL -lm" +GDK_BACKEND=x11 ./MotionCal +``` + +Select the T-Beam USB serial port in MotionCal. The firmware also accepts +MotionCal's 68-byte calibration packet and echoes `Cal1:` and `Cal2:` lines so +MotionCal can confirm the send. diff --git a/exercises/25_motioncal_tbeam/platformio.ini b/exercises/25_motioncal_tbeam/platformio.ini new file mode 100644 index 0000000..f004814 --- /dev/null +++ b/exercises/25_motioncal_tbeam/platformio.ini @@ -0,0 +1,77 @@ +; 20260424 Codex +; Exercise 25_motioncal_tbeam + +[platformio] +default_envs = cy + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +board_build.partitions = default_8MB.csv +monitor_speed = 115200 +extra_scripts = pre:scripts/set_build_epoch.py +lib_deps = + Wire + olikraus/U8g2@^2.36.4 + lewisxhe/XPowersLib@0.3.3 + +build_flags = + -I ../../shared/boards + -I ../../external/microReticulum_Firmware + -I ../../../../LilyGo-LoRa-Series/lib/SensorLib/src + -D BOARD_MODEL=BOARD_TBEAM_S_V1 + -D OLED_SDA=17 + -D OLED_SCL=18 + -D OLED_ADDR=0x3C + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"AMY\" + -D NODE_LABEL=\"Amy\" + +[env:bob] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"BOB\" + -D NODE_LABEL=\"Bob\" + +[env:cy] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"CY\" + -D NODE_LABEL=\"Cy\" + +[env:dan] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"DAN\" + -D NODE_LABEL=\"Dan\" + +[env:ed] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"ED\" + -D NODE_LABEL=\"Ed\" + +[env:flo] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"FLO\" + -D NODE_LABEL=\"Flo\" + +[env:guy] +extends = env +build_flags = + ${env.build_flags} + -D BOARD_ID=\"GUY\" + -D NODE_LABEL=\"Guy\" diff --git a/exercises/25_motioncal_tbeam/scripts/set_build_epoch.py b/exercises/25_motioncal_tbeam/scripts/set_build_epoch.py new file mode 100644 index 0000000..40ef7ca --- /dev/null +++ b/exercises/25_motioncal_tbeam/scripts/set_build_epoch.py @@ -0,0 +1,12 @@ +import time +Import("env") + +epoch = int(time.time()) +utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch)) + +env.Append( + CPPDEFINES=[ + ("FW_BUILD_EPOCH", str(epoch)), + ("FW_BUILD_UTC", '"%s"' % utc_tag), + ] +) diff --git a/exercises/25_motioncal_tbeam/src/main.cpp b/exercises/25_motioncal_tbeam/src/main.cpp new file mode 100644 index 0000000..60de5ee --- /dev/null +++ b/exercises/25_motioncal_tbeam/src/main.cpp @@ -0,0 +1,348 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "SensorQMC6310.hpp" +#include "tbeam_supreme_adapter.h" + +namespace { + +#ifndef BOARD_ID +#define BOARD_ID "CY" +#endif + +#ifndef NODE_LABEL +#define NODE_LABEL "Cy" +#endif + +#ifndef FW_BUILD_UTC +#define FW_BUILD_UTC unknown +#endif + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif + +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif + +#ifndef OLED_ADDR +#define OLED_ADDR 0x3C +#endif + +#define STR_INNER(x) #x +#define STR(x) STR_INNER(x) + +static constexpr const char* kExerciseName = "Exercise 25"; +static constexpr const char* kBoardId = BOARD_ID; +static constexpr const char* kNodeLabel = NODE_LABEL; +static constexpr const char* kBuildUtc = STR(FW_BUILD_UTC); +static constexpr uint32_t kSampleIntervalMs = 40; +static constexpr uint32_t kDisplayIntervalMs = 250; +static constexpr uint8_t kMagCandidateCount = 3; +static constexpr uint8_t kMagCandidates[kMagCandidateCount] = {0x1C, 0x3C, 0x2C}; +static constexpr uint16_t kCalibrationPacketSize = 68; +static constexpr uint16_t kCalibrationSignature = 0x5475; +static constexpr size_t kFloatCount = 16; + +XPowersLibInterface* g_pmu = nullptr; +U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); +SensorQMC6310 g_qmc; + +bool g_displayReady = false; +bool g_magReady = false; +uint8_t g_magAddress = 0; +uint8_t g_magChipId = 0; +char g_magLabel[16] = "UNKNOWN"; +uint32_t g_lastSampleMs = 0; +uint32_t g_lastDisplayMs = 0; +uint32_t g_sampleCount = 0; +int16_t g_lastMagCounts[3] = {0, 0, 0}; +float g_lastMagUt[3] = {0.0f, 0.0f, 0.0f}; + +uint8_t g_calPacket[kCalibrationPacketSize]; +uint16_t g_calPacketLen = 0; + +uint16_t crc16Update(uint16_t crc, uint8_t data) { + crc ^= data; + for (uint8_t i = 0; i < 8; ++i) { + if ((crc & 1U) != 0U) { + crc = (crc >> 1U) ^ 0xA001U; + } else { + crc >>= 1U; + } + } + return crc; +} + +float readFloatLe(const uint8_t* p) { + union { + uint32_t n; + float f; + } value; + value.n = ((uint32_t)p[0]) | + ((uint32_t)p[1] << 8U) | + ((uint32_t)p[2] << 16U) | + ((uint32_t)p[3] << 24U); + return value.f; +} + +int16_t clampToInt16(long value) { + if (value > INT16_MAX) return INT16_MAX; + if (value < INT16_MIN) return INT16_MIN; + return (int16_t)value; +} + +int16_t microteslaToMotionCalCounts(float uT) { + return clampToInt16(lroundf(uT * 10.0f)); +} + +void drawLines(const char* l1, + const char* l2 = nullptr, + const char* l3 = nullptr, + const char* l4 = nullptr, + const char* l5 = nullptr) { + if (!g_displayReady) { + return; + } + + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_6x12_tf); + if (l1) g_oled.drawUTF8(0, 12, l1); + if (l2) g_oled.drawUTF8(0, 24, l2); + if (l3) g_oled.drawUTF8(0, 36, l3); + if (l4) g_oled.drawUTF8(0, 48, l4); + if (l5) g_oled.drawUTF8(0, 60, l5); + g_oled.sendBuffer(); +} + +void initDisplay() { + Wire.begin(OLED_SDA, OLED_SCL); + Wire.setClock(400000); + g_oled.setI2CAddress(OLED_ADDR << 1); + g_oled.setBusClock(400000); + g_oled.begin(); + g_oled.setPowerSave(0); + g_displayReady = true; + drawLines(kExerciseName, "MotionCal Bridge", kBoardId, "starting..."); +} + +bool probeI2cAddr(TwoWire& wire, uint8_t addr) { + wire.beginTransmission(addr); + return wire.endTransmission() == 0; +} + +bool detectMagnetometer() { + for (uint8_t i = 0; i < kMagCandidateCount; ++i) { + const uint8_t addr = kMagCandidates[i]; + if (!probeI2cAddr(Wire, addr)) { + continue; + } + + g_magAddress = addr; + if (addr == 0x1C) { + strlcpy(g_magLabel, "QMC6310U", sizeof(g_magLabel)); + } else if (addr == 0x3C) { + strlcpy(g_magLabel, "QMC6310N", sizeof(g_magLabel)); + } else if (addr == 0x2C) { + strlcpy(g_magLabel, "QMC5883P", sizeof(g_magLabel)); + } else { + strlcpy(g_magLabel, "QST-MAG", sizeof(g_magLabel)); + } + return true; + } + return false; +} + +bool initMagnetometer() { + if (!detectMagnetometer()) { + return false; + } + + if (!g_qmc.begin(Wire, g_magAddress, tbeam_supreme::i2cSda(), tbeam_supreme::i2cScl())) { + return false; + } + + g_magChipId = g_qmc.getChipID(); + return g_qmc.configMagnetometer( + OperationMode::CONTINUOUS_MEASUREMENT, + MagFullScaleRange::FS_2G, + 100.0f, + MagOverSampleRatio::OSR_4, + MagDownSampleRatio::DSR_1); +} + +void printBootSummary() { + Serial.printf("exercise=%s MotionCal T-Beam bridge\r\n", kExerciseName); + Serial.printf("board_id=%s node_label=%s build=%s\r\n", kBoardId, kNodeLabel, kBuildUtc); + Serial.printf("serial_format=Raw:accel_x,accel_y,accel_z,gyro_x,gyro_y,gyro_z,mag_x,mag_y,mag_z\r\n"); + Serial.printf("mag_units=MotionCal integer counts, 1 count = 0.1 uT\r\n"); +} + +void streamMotionCalRaw(const MagnetometerData& data) { + const float xUt = data.magnetic_field.x * 100.0f; + const float yUt = data.magnetic_field.y * 100.0f; + const float zUt = data.magnetic_field.z * 100.0f; + + g_lastMagUt[0] = xUt; + g_lastMagUt[1] = yUt; + g_lastMagUt[2] = zUt; + g_lastMagCounts[0] = microteslaToMotionCalCounts(xUt); + g_lastMagCounts[1] = microteslaToMotionCalCounts(yUt); + g_lastMagCounts[2] = microteslaToMotionCalCounts(zUt); + ++g_sampleCount; + + Serial.printf("Raw:0,0,8192,0,0,0,%d,%d,%d\r\n", + (int)g_lastMagCounts[0], + (int)g_lastMagCounts[1], + (int)g_lastMagCounts[2]); +} + +void updateDisplay() { + char line3[28]; + char line4[28]; + char line5[28]; + + snprintf(line3, sizeof(line3), "%s 0x%02X id 0x%02X", g_magLabel, g_magAddress, g_magChipId); + snprintf(line4, sizeof(line4), "%6.1f %6.1f", g_lastMagUt[0], g_lastMagUt[1]); + snprintf(line5, sizeof(line5), "Z:%6.1f N:%lu", g_lastMagUt[2], (unsigned long)g_sampleCount); + drawLines(kExerciseName, "MotionCal Raw stream", line3, line4, line5); +} + +void printCalibrationEcho(const float* values) { + Serial.printf("Cal1:%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f\r\n", + values[0], values[1], values[2], + values[3], values[4], values[5], + values[6], values[7], values[8], values[9]); + Serial.printf("Cal2:%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f\r\n", + values[10], values[13], values[14], + values[13], values[11], values[15], + values[14], values[15], values[12]); +} + +void handleCalibrationPacket(const uint8_t* packet) { + const uint16_t signature = (uint16_t)packet[0] | ((uint16_t)packet[1] << 8U); + if (signature != kCalibrationSignature) { + return; + } + + uint16_t crc = 0xFFFF; + for (uint16_t i = 0; i < kCalibrationPacketSize - 2; ++i) { + crc = crc16Update(crc, packet[i]); + } + const uint16_t got = (uint16_t)packet[66] | ((uint16_t)packet[67] << 8U); + if (crc != got) { + Serial.printf("motioncal_calibration_crc=bad expected=0x%04X got=0x%04X\r\n", crc, got); + return; + } + + float values[kFloatCount]; + const uint8_t* p = packet + 2; + for (size_t i = 0; i < kFloatCount; ++i) { + values[i] = readFloatLe(p); + p += 4; + } + + Serial.printf("motioncal_calibration=received hard_iron_uT=%.3f,%.3f,%.3f field_uT=%.3f\r\n", + values[6], values[7], values[8], values[9]); + printCalibrationEcho(values); +} + +void pollSerialInput() { + while (Serial.available() > 0) { + const int c = Serial.read(); + if (c < 0) { + return; + } + + if (g_calPacketLen == 0 && (uint8_t)c != 0x75U) { + continue; + } + + g_calPacket[g_calPacketLen++] = (uint8_t)c; + if (g_calPacketLen == 2 && g_calPacket[1] != 0x54U) { + g_calPacketLen = (g_calPacket[1] == 0x75U) ? 1U : 0U; + if (g_calPacketLen == 1U) { + g_calPacket[0] = 0x75U; + } + continue; + } + + if (g_calPacketLen >= kCalibrationPacketSize) { + handleCalibrationPacket(g_calPacket); + g_calPacketLen = 0; + } + } +} + +void appSetup() { + Serial.begin(115200); + const uint32_t serialWaitStart = millis(); + while (!Serial && (millis() - serialWaitStart) < 4000) { + delay(10); + } + delay(300); + + printBootSummary(); + initDisplay(); + + if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) { + Serial.println("pmu_init=failed"); + drawLines(kExerciseName, "PMU init failed", kBoardId, "see serial"); + return; + } + Serial.println("pmu_init=ok"); + + g_magReady = initMagnetometer(); + if (!g_magReady) { + Serial.println("magnetometer_init=failed"); + drawLines(kExerciseName, "MAG init failed", kBoardId, "see serial"); + return; + } + + Serial.printf("magnetometer_init=ok label=%s addr=0x%02X chip=0x%02X\r\n", + g_magLabel, g_magAddress, g_magChipId); + drawLines(kExerciseName, "MotionCal Bridge", g_magLabel, "streaming Raw..."); + g_lastSampleMs = millis(); + g_lastDisplayMs = millis(); +} + +void appLoop() { + pollSerialInput(); + + if (!g_magReady) { + delay(100); + return; + } + + const uint32_t now = millis(); + if ((uint32_t)(now - g_lastSampleMs) >= kSampleIntervalMs) { + g_lastSampleMs = now; + MagnetometerData data; + if (g_qmc.readData(data) && !data.overflow) { + streamMotionCalRaw(data); + } + } + + if ((uint32_t)(now - g_lastDisplayMs) >= kDisplayIntervalMs) { + g_lastDisplayMs = now; + updateDisplay(); + } + + delay(1); +} + +} // namespace + +void setup() { + appSetup(); +} + +void loop() { + appLoop(); +} From fab25e1a72e8446e78ff5d2f003fccd3da5dc160 Mon Sep 17 00:00:00 2001 From: John Poole Date: Sat, 25 Apr 2026 04:20:14 -0700 Subject: [PATCH 4/4] Commencing DOxygen documentation --- exercises/01_lora_ascii_pingpong/src/main.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/exercises/01_lora_ascii_pingpong/src/main.cpp b/exercises/01_lora_ascii_pingpong/src/main.cpp index b13fd1e..d7b2450 100644 --- a/exercises/01_lora_ascii_pingpong/src/main.cpp +++ b/exercises/01_lora_ascii_pingpong/src/main.cpp @@ -2,6 +2,18 @@ // $Id$ // $HeadURL$ +/* + Exercise 01: LoRa ASCII ping-pong (serial only) + + This is a simple "ping-pong" test of LoRa communication between two nodes, using the Serial console for output. + The nodes will periodically transmit a message containing their label and an iteration count, and print any received messages along with RSSI and SNR. + + To run this test, set up two devices with the same code but different NODE_LABELs (e.g., "A" and "B"), and ensure they are within range of each other. You should see them exchanging messages in the Serial console. + + Note: This exercise assumes you have already set up the hardware and wiring correctly, as per the instructions in the README. Make sure to adjust the pins in platformio.ini if needed. + + +*/ #include #include #include