works
This commit is contained in:
parent
6fdbf1d258
commit
04afd13532
6 changed files with 685 additions and 0 deletions
14
exercises/24_nvs/lib/tbeam_display/library.json
Normal file
14
exercises/24_nvs/lib/tbeam_display/library.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
204
exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.cpp
Normal file
204
exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.cpp
Normal file
|
|
@ -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
|
||||||
70
exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.h
Normal file
70
exercises/24_nvs/lib/tbeam_display/src/TBeamDisplay.h
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <U8g2lib.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
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
|
||||||
77
exercises/24_nvs/platformio.ini
Normal file
77
exercises/24_nvs/platformio.ini
Normal file
|
|
@ -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\"
|
||||||
13
exercises/24_nvs/scripts/set_build_epoch.py
Normal file
13
exercises/24_nvs/scripts/set_build_epoch.py
Normal file
|
|
@ -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),
|
||||||
|
]
|
||||||
|
)
|
||||||
307
exercises/24_nvs/src/main.cpp
Normal file
307
exercises/24_nvs/src/main.cpp
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <U8g2lib.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#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<uint64_t>(INT32_MAX)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t raw = static_cast<time_t>(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<int16_t>(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<int16_t>(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);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue