safety -- still not sure what is happening, but I needed to have logging on SD card and web access since running on a USB tether placed the unit close to a maelstrom of magnetic interferences

This commit is contained in:
John Poole 2026-04-19 10:21:50 -07:00
commit 329143db49
2 changed files with 114 additions and 23 deletions

View file

@ -77,6 +77,8 @@ build_flags =
-D NODE_SLOT_INDEX=2
-D LOG_AP_IP_OCTET=25
-D GNSS_CHIP_NAME=\"L76K\"
; defaults to "display" mode, but for this exercise we want to calibrate the compass, so we'll set the mode to "calibrate" which will display the raw magnetic heading and allow us to determine the declination experimentally.
;-D COMPASS_MODE=\"calibrate\"
[env:dan]
extends = env

View file

@ -46,6 +46,10 @@ namespace {
#define MAG_DECLINATION_DEG 0.0f
#endif
#ifndef COMPASS_MODE
#define COMPASS_MODE "display"
#endif
#define STR_INNER(x) #x
#define STR(x) STR_INNER(x)
@ -53,6 +57,7 @@ static constexpr const char* kBoardId = BOARD_ID;
static constexpr const char* kNodeLabel = NODE_LABEL;
static constexpr const char* kBuild = STR(FW_BUILD_UTC);
static constexpr const char* kExerciseName = "Exercise 22";
static constexpr const char* kCompassMode = COMPASS_MODE;
static constexpr uint32_t kSampleIntervalMs = 200;
static constexpr uint32_t kDisplayIntervalMs = 200;
static constexpr uint32_t kUiSplashMs = 1400;
@ -61,10 +66,19 @@ static constexpr uint8_t kMagCandidateCount = 3;
static constexpr uint8_t kMagCandidates[kMagCandidateCount] = {0x1C, 0x3C, 0x0D};
static constexpr float kDeclinationDeg = MAG_DECLINATION_DEG;
static constexpr float kDegPerRad = 57.29577951308232f;
static constexpr float kMagOffsetRawX = -7993.0f;
static constexpr float kMagOffsetRawY = 4608.0f;
static constexpr float kMagOffsetRawZ = 3456.0f;
//static constexpr float kMagOffsetRawX = -7993.0f;
//static constexpr float kMagOffsetRawY = 4608.0f;
//static constexpr float kMagOffsetRawZ = 3456.0f;
// Above are CODEX, below are ChatGPT, v. 2
static constexpr float kMagOffsetRawX = -8089.0f;
static constexpr float kMagOffsetRawY = 4606.0f;
static constexpr float kMagOffsetRawZ = 3428.0f;
static constexpr float kMagSoft00 = 0.002173745f;
static constexpr float kMagSoft01 = 0.000184341f;
static constexpr float kMagSoft10 = 0.000184341f;
static constexpr float kMagSoft11 = 0.002412928f;
struct ClockDateTime {
uint16_t year = 0;
@ -86,6 +100,9 @@ struct MagSample {
float x_uT = 0.0f;
float y_uT = 0.0f;
float z_uT = 0.0f;
float corrX = 0.0f;
float corrY = 0.0f;
float corrZ = 0.0f;
float field_uT = 0.0f;
float headingMagDeg = 0.0f;
float headingTrueDeg = 0.0f;
@ -118,6 +135,14 @@ uint32_t g_lastSampleMs = 0;
uint32_t g_lastDisplayMs = 0;
uint32_t g_lastHeartbeatMs = 0;
bool isCalibrateMode() {
return strcmp(kCompassMode, "calibrate") == 0;
}
bool isDisplayMode() {
return strcmp(kCompassMode, "display") == 0;
}
uint8_t toBcd(uint8_t value) {
return (uint8_t)(((value / 10U) << 4U) | (value % 10U));
}
@ -559,6 +584,13 @@ float normalizeHeadingDeg(float heading) {
return heading;
}
const char* headingToCompassPoint(float headingDeg) {
static constexpr const char* kPoints[8] = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
const float normalized = normalizeHeadingDeg(headingDeg);
const int index = ((int)((normalized + 22.5f) / 45.0f)) % 8;
return kPoints[index];
}
bool captureSample(MagSample& sample) {
if (!g_magReady) {
return false;
@ -576,12 +608,52 @@ bool captureSample(MagSample& sample) {
sample.rawX = g_qmc.getRawX();
sample.rawY = g_qmc.getRawY();
sample.rawZ = g_qmc.getRawZ();
//sample.x_uT = g_qmc.getX();
//sample.y_uT = g_qmc.getY();
//sample.z_uT = g_qmc.getZ();
//sample.field_uT = sqrtf(sample.x_uT * sample.x_uT + sample.y_uT * sample.y_uT + sample.z_uT * sample.z_uT);
//sample.headingMagDeg = normalizeHeadingDeg(atan2f(sample.y_uT, sample.x_uT) * kDegPerRad);
//sample.headingTrueDeg = normalizeHeadingDeg(sample.headingMagDeg + kDeclinationDeg);
// Below is ChatGPT's suggestion for applying declination, but for calibration purposes we may want to keep the raw magnetic heading instead of applying declination, since the point of the exercise is to determine the declination experimentally. We can calculate the true heading in post-processing after we have determined the declination.
//const float rawHeadingDeg =
// normalizeHeadingDeg(atan2f(sample.y_uT, sample.x_uT) * kDegPerRad);
//sample.headingMagDeg = normalizeHeadingDeg(360.0f - rawHeadingDeg);
//sample.headingTrueDeg = normalizeHeadingDeg(sample.headingMagDeg + kDeclinationDeg);
// Preserve the library-scaled values for logging/reference.
sample.x_uT = g_qmc.getX();
sample.y_uT = g_qmc.getY();
sample.z_uT = g_qmc.getZ();
sample.field_uT = sqrtf(sample.x_uT * sample.x_uT + sample.y_uT * sample.y_uT + sample.z_uT * sample.z_uT);
sample.headingMagDeg = normalizeHeadingDeg(atan2f(sample.y_uT, sample.x_uT) * kDegPerRad);
// Apply raw-count hard-iron offsets.
const float mx0 = (float)sample.rawX - kMagOffsetRawX;
const float my0 = (float)sample.rawY - kMagOffsetRawY;
const float mz0 = (float)sample.rawZ - kMagOffsetRawZ;
// Apply 2x2 XY soft-iron correction matrix.
const float mx = kMagSoft00 * mx0 + kMagSoft01 * my0;
const float my = kMagSoft10 * mx0 + kMagSoft11 * my0;
const float mz = mz0;
// Store corrected values into the existing x_uT/y_uT/z_uT fields so the
// rest of the program uses the calibrated vector.
sample.x_uT = mx;
sample.y_uT = my;
sample.z_uT = mz;
sample.field_uT = sqrtf(sample.x_uT * sample.x_uT +
sample.y_uT * sample.y_uT +
sample.z_uT * sample.z_uT);
//const float rawHeadingDeg =
// normalizeHeadingDeg(atan2f(sample.y_uT, sample.x_uT) * kDegPerRad);
const float rawHeadingDeg =
normalizeHeadingDeg(atan2f(my, mx) * kDegPerRad);
sample.headingMagDeg = normalizeHeadingDeg(360.0f - rawHeadingDeg);
sample.headingTrueDeg = normalizeHeadingDeg(sample.headingMagDeg + kDeclinationDeg);
return true;
}
@ -646,10 +718,12 @@ void drawLiveUi() {
time_t now = time(nullptr);
struct tm tmUtc;
char line1[24];
char line2[28];
char line3[28];
char line3[24];
char line4[28];
char line5[28];
char bearingLine[24];
const int bearing = (int)lroundf(normalizeHeadingDeg(g_lastSample.headingTrueDeg));
const char* compassPoint = headingToCompassPoint(g_lastSample.headingTrueDeg);
if (now >= 946684800 && gmtime_r(&now, &tmUtc) != nullptr) {
snprintf(line1, sizeof(line1), "%02d:%02d:%02d UTC", tmUtc.tm_hour, tmUtc.tm_min, tmUtc.tm_sec);
@ -657,17 +731,19 @@ void drawLiveUi() {
snprintf(line1, sizeof(line1), "time invalid");
}
snprintf(line2, sizeof(line2), "X:% 7.2f Y:% 7.2f", g_lastSample.x_uT, g_lastSample.y_uT);
snprintf(line3, sizeof(line3), "Z:% 7.2f F:% 7.2f", g_lastSample.z_uT, g_lastSample.field_uT);
snprintf(line4, sizeof(line4), "HdM:%6.1f T:%6.1f", g_lastSample.headingMagDeg, g_lastSample.headingTrueDeg);
snprintf(line5, sizeof(line5), "%s 0x%02X N:%lu", g_magLabel, g_magAddress, (unsigned long)g_lastSample.seq);
snprintf(bearingLine, sizeof(bearingLine), "Bearing:%3d", bearing);
snprintf(line3, sizeof(line3), "Dir:%s", compassPoint);
snprintf(line4, sizeof(line4), "Mag:%6.1f Tru:%6.1f", g_lastSample.headingMagDeg, g_lastSample.headingTrueDeg);
snprintf(line5, sizeof(line5), "%s N:%lu", g_magLabel, (unsigned long)g_lastSample.seq);
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_6x12_tf);
g_oled.drawUTF8(0, 12, line1);
g_oled.drawUTF8(0, 24, line2);
g_oled.drawUTF8(0, 36, line3);
g_oled.drawUTF8(0, 48, line4);
g_oled.setFont(u8g2_font_7x14B_tf);
g_oled.drawUTF8(0, 30, bearingLine);
g_oled.setFont(u8g2_font_6x12_tf);
g_oled.drawUTF8(0, 42, line3);
g_oled.drawUTF8(0, 54, line4);
g_oled.drawUTF8(0, 60, line5);
g_oled.sendBuffer();
}
@ -677,6 +753,7 @@ void printBootSummary() {
Serial.printf("board_id=%s\n", kBoardId);
Serial.printf("node_label=%s\n", kNodeLabel);
Serial.printf("build=%s\n", kBuild);
Serial.printf("compass_mode=%s\n", kCompassMode);
Serial.printf("pmu_wire_pins=sda:%d scl:%d\n", tbeam_supreme::i2cSda(), tbeam_supreme::i2cScl());
Serial.printf("oled_wire_pins=sda:%d scl:%d addr:0x%02X\n", OLED_SDA, OLED_SCL, OLED_ADDR);
Serial.printf("declination_deg=%.2f\n", kDeclinationDeg);
@ -694,7 +771,7 @@ void appSetup() {
printBootSummary();
initDisplay();
drawLines("Exercise 22", "Magnetometer", "& calibration", kBoardId, "bring-up");
drawLines("Exercise 22", "Magnetometer", kCompassMode, kBoardId, "bring-up");
if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) {
Serial.println("pmu_init=failed");
@ -732,21 +809,30 @@ void appSetup() {
Serial.printf("magnetometer_init=ok label=%s addr=0x%02X chip=0x%02X\n", g_magLabel, g_magAddress, g_magChipId);
Serial.printf("magnetometer_offsets_applied=%.1f,%.1f,%.1f\n", kMagOffsetRawX, kMagOffsetRawY, kMagOffsetRawZ);
if (g_timeValid && g_sdMounted) {
if (isCalibrateMode() && g_timeValid && g_sdMounted) {
if (openLogFile()) {
Serial.printf("log_open=ok path=%s\n", g_logPath);
} else {
Serial.println("log_open=failed");
}
} else {
Serial.printf("log_open=skipped time_valid=%s sd_mounted=%s\n",
Serial.printf("log_open=skipped mode=%s time_valid=%s sd_mounted=%s\n",
kCompassMode,
g_timeValid ? "yes" : "no",
g_sdMounted ? "yes" : "no");
}
if (isCalibrateMode()) {
startWebServer();
} else {
Serial.printf("wifi_ap=skipped mode=%s\n", kCompassMode);
}
drawLines("Exercise 22", "Magnetometer", g_magLabel, "offsets applied", "logging @200ms");
if (isCalibrateMode()) {
drawLines("Exercise 22", "Magnetometer", "calibrate", "logging @200ms", "limit 600");
} else {
drawLines("Exercise 22", "Magnetometer", "display", "bearing live", "no logging");
}
delay(kUiSplashMs);
g_lastSampleMs = millis();
g_lastDisplayMs = millis();
@ -770,17 +856,20 @@ void appLoop() {
g_sdMounted = g_sd.isMounted();
const uint32_t now = millis();
if (sample_count <= sample_limit) {
const bool allowSampling = isDisplayMode() || (sample_count <= sample_limit);
if (allowSampling) {
if ((uint32_t)(now - g_lastSampleMs) >= kSampleIntervalMs) {
g_lastSampleMs = now;
MagSample sample{};
if (captureSample(sample)) {
sample_count++;
g_lastSample = sample;
if (isCalibrateMode()) {
sample_count++;
printSampleToSerial(sample);
appendSampleToLog(sample);
}
}
}
if ((uint32_t)(now - g_lastDisplayMs) >= kDisplayIntervalMs) {
g_lastDisplayMs = now;