Compare commits

...

4 commits

16 changed files with 2146 additions and 638 deletions

View file

@ -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++);

View file

@ -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 <Arduino.h>
#include <SPI.h>
#include <RadioLib.h>

File diff suppressed because it is too large Load diff

View 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"
}
]
}

View 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

View 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

View 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\"

View 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),
]
)

View 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);
}

View file

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

View file

@ -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\"

View file

@ -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),
]
)

View file

@ -0,0 +1,348 @@
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <XPowersLib.h>
#include <math.h>
#include <stdint.h>
#include <string.h>
#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();
}

View file

@ -8,29 +8,35 @@
#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) {
bool TBeamClock::begin(const ClockConfig &config)
{
config_ = config;
clearError();
if (config_.sda < 0) {
if (config_.sda < 0)
{
config_.sda = OLED_SDA;
}
if (config_.scl < 0) {
if (config_.scl < 0)
{
config_.scl = OLED_SCL;
}
if (config_.beginWire) {
if (config_.beginWire)
{
wire_.begin(config_.sda, config_.scl);
}
DateTime dt{};
bool lowVoltage = false;
ready_ = readRtc(dt, lowVoltage);
if (!ready_) {
if (!ready_)
{
setError("RTC read failed");
valid_ = false;
return false;
@ -40,18 +46,23 @@ bool TBeamClock::begin(const ClockConfig& config) {
lastRtc_ = dt;
valid_ = !lowVoltage && isValidDateTime(dt);
lastEpoch_ = valid_ ? toEpochSeconds(dt) : 0;
if (lowVoltage) {
if (lowVoltage)
{
setError("RTC low-voltage flag set");
} else if (!valid_) {
}
else if (!valid_)
{
setError("RTC date/time invalid");
}
return true;
}
}
void TBeamClock::update() {
void TBeamClock::update()
{
DateTime dt{};
bool lowVoltage = false;
if (!readRtc(dt, lowVoltage)) {
if (!readRtc(dt, lowVoltage))
{
ready_ = false;
valid_ = false;
setError("RTC read failed");
@ -63,25 +74,33 @@ void TBeamClock::update() {
lastRtc_ = dt;
valid_ = !lowVoltage && isValidDateTime(dt);
lastEpoch_ = valid_ ? toEpochSeconds(dt) : 0;
if (valid_) {
if (valid_)
{
clearError();
} else if (lowVoltage) {
}
else if (lowVoltage)
{
setError("RTC low-voltage flag set");
} else {
}
else
{
setError("RTC date/time invalid");
}
}
}
bool TBeamClock::readRtc(DateTime& out, bool& lowVoltageFlag) const {
bool TBeamClock::readRtc(DateTime &out, bool &lowVoltageFlag) const
{
wire_.beginTransmission(config_.rtcAddress);
wire_.write(0x02);
if (wire_.endTransmission(false) != 0) {
if (wire_.endTransmission(false) != 0)
{
return false;
}
const uint8_t need = 7;
const uint8_t got = wire_.requestFrom((int)config_.rtcAddress, (int)need);
if (got != need) {
if (got != need)
{
return false;
}
@ -103,21 +122,26 @@ bool TBeamClock::readRtc(DateTime& out, bool& lowVoltageFlag) const {
const uint8_t yy = fromBcd(year);
out.year = (month & 0x80U) ? (1900U + yy) : (2000U + yy);
return true;
}
}
bool TBeamClock::readValidRtc(DateTime& out, int64_t* epochOut) const {
bool TBeamClock::readValidRtc(DateTime &out, int64_t *epochOut) const
{
bool lowVoltage = false;
if (!readRtc(out, lowVoltage) || lowVoltage || !isValidDateTime(out)) {
if (!readRtc(out, lowVoltage) || lowVoltage || !isValidDateTime(out))
{
return false;
}
if (epochOut) {
if (epochOut)
{
*epochOut = toEpochSeconds(out);
}
return true;
}
}
bool TBeamClock::writeRtc(const DateTime& dt) const {
if (!isValidDateTime(dt)) {
bool TBeamClock::writeRtc(const DateTime &dt) const
{
if (!isValidDateTime(dt))
{
return false;
}
@ -130,35 +154,45 @@ bool TBeamClock::writeRtc(const DateTime& dt) const {
wire_.write(toBcd(dt.weekday) & 0x07U);
uint8_t monthReg = toBcd(dt.month) & 0x1FU;
if (dt.year < 2000U) {
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;
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) {
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) {
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) {
if (remainder < 0)
{
remainder += 86400LL;
days -= 1;
}
@ -184,9 +218,10 @@ bool TBeamClock::fromEpochSeconds(int64_t seconds, DateTime& out) {
out.day = (uint8_t)day;
out.weekday = 0;
return isValidDateTime(out);
}
}
void TBeamClock::formatIsoUtc(const DateTime& dt, char* out, size_t outSize) {
void TBeamClock::formatIsoUtc(const DateTime &dt, char *out, size_t outSize)
{
snprintf(out,
outSize,
"%04u-%02u-%02uT%02u:%02u:%02uZ",
@ -196,9 +231,10 @@ void TBeamClock::formatIsoUtc(const DateTime& dt, char* out, size_t outSize) {
(unsigned)dt.hour,
(unsigned)dt.minute,
(unsigned)dt.second);
}
}
void TBeamClock::formatCompactUtc(const DateTime& dt, char* out, size_t outSize) {
void TBeamClock::formatCompactUtc(const DateTime &dt, char *out, size_t outSize)
{
snprintf(out,
outSize,
"%04u%02u%02u_%02u%02u%02u",
@ -208,9 +244,10 @@ void TBeamClock::formatCompactUtc(const DateTime& dt, char* out, size_t outSize)
(unsigned)dt.hour,
(unsigned)dt.minute,
(unsigned)dt.second);
}
}
void TBeamClock::makeRunId(const DateTime& dt, const char* boardId, char* out, size_t outSize) {
void TBeamClock::makeRunId(const DateTime &dt, const char *boardId, char *out, size_t outSize)
{
snprintf(out,
outSize,
"%04u%02u%02u_%02u%02u%02u_%s",
@ -221,10 +258,12 @@ void TBeamClock::makeRunId(const DateTime& dt, const char* boardId, char* out, s
(unsigned)dt.minute,
(unsigned)dt.second,
boardId ? boardId : "NODE");
}
}
bool TBeamClock::parseDateTime(const char* text, DateTime& out) {
if (!text) {
bool TBeamClock::parseDateTime(const char *text, DateTime &out)
{
if (!text)
{
return false;
}
int y = 0;
@ -234,7 +273,8 @@ bool TBeamClock::parseDateTime(const char* text, DateTime& out) {
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) {
sscanf(text, "%d-%d-%dT%d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6)
{
return false;
}
@ -246,46 +286,55 @@ bool TBeamClock::parseDateTime(const char* text, DateTime& out) {
out.second = (uint8_t)s;
out.weekday = 0;
return isValidDateTime(out);
}
}
uint8_t TBeamClock::toBcd(uint8_t value) {
uint8_t TBeamClock::toBcd(uint8_t value)
{
return (uint8_t)(((value / 10U) << 4U) | (value % 10U));
}
}
uint8_t TBeamClock::fromBcd(uint8_t value) {
uint8_t TBeamClock::fromBcd(uint8_t value)
{
return (uint8_t)(((value >> 4U) * 10U) + (value & 0x0FU));
}
}
bool TBeamClock::isLeapYear(uint16_t year) {
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) {
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) {
if (month == 2U)
{
return (uint8_t)(isLeapYear(year) ? 29U : 28U);
}
if (month >= 1U && month <= 12U) {
if (month >= 1U && month <= 12U)
{
return kDays[month - 1U];
}
return 0;
}
}
int64_t TBeamClock::daysFromCivil(int year, unsigned month, unsigned day) {
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 {
void TBeamClock::setError(const char *message) const
{
strlcpy(lastError_, message ? message : "", sizeof(lastError_));
}
}
void TBeamClock::clearError() const {
void TBeamClock::clearError() const
{
lastError_[0] = '\0';
}
}
} // namespace tbeam

View file

@ -3,9 +3,11 @@
#include <Arduino.h>
#include <Wire.h>
namespace tbeam {
namespace tbeam
{
struct DateTime {
struct DateTime
{
uint16_t year = 0;
uint8_t month = 0;
uint8_t day = 0;
@ -13,40 +15,42 @@ struct DateTime {
uint8_t minute = 0;
uint8_t second = 0;
uint8_t weekday = 0;
};
};
struct ClockConfig {
struct ClockConfig
{
uint8_t rtcAddress = 0x51;
int sda = -1;
int scl = -1;
bool beginWire = true;
};
};
class TBeamClock {
class TBeamClock
{
public:
explicit TBeamClock(TwoWire& wire = Wire1);
explicit TBeamClock(TwoWire &wire = Wire1);
bool begin(const ClockConfig& config = ClockConfig{});
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_; }
const DateTime &lastRtc() const { return lastRtc_; }
int64_t lastEpoch() const { return lastEpoch_; }
const char* lastError() const { return lastError_; }
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);
@ -55,10 +59,10 @@ class TBeamClock {
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 setError(const char *message) const;
void clearError() const;
TwoWire& wire_;
TwoWire &wire_;
ClockConfig config_{};
bool ready_ = false;
bool valid_ = false;
@ -66,6 +70,6 @@ class TBeamClock {
DateTime lastRtc_{};
int64_t lastEpoch_ = 0;
mutable char lastError_[128] = {};
};
};
} // namespace tbeam

View file

@ -1,78 +1,98 @@
#include "TBeamLogger.h"
namespace tbeam {
namespace tbeam
{
bool TBeamLogger::begin(Print& serial, TBeamStorage* storage, const LoggerConfig& config) {
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_) {
void TBeamLogger::update()
{
if (!config_.autoFlush || !storage_)
{
return;
}
const uint32_t now = millis();
if ((uint32_t)(now - lastFlushMs_) >= config_.flushIntervalMs) {
if ((uint32_t)(now - lastFlushMs_) >= config_.flushIntervalMs)
{
storage_->flush();
lastFlushMs_ = now;
}
}
}
bool TBeamLogger::openLog(const char* path) {
bool TBeamLogger::openLog(const char *path)
{
return storage_ && storage_->openLog(path);
}
}
bool TBeamLogger::openUniqueLog(const char* prefix, const char* extension) {
if (!storage_) {
bool TBeamLogger::openUniqueLog(const char *prefix, const char *extension)
{
if (!storage_)
{
return false;
}
char path[128];
if (!storage_->makeUniqueLogPath(prefix, extension, path, sizeof(path))) {
if (!storage_->makeUniqueLogPath(prefix, extension, path, sizeof(path)))
{
return false;
}
return storage_->openLog(path);
}
}
const char* TBeamLogger::currentLogPath() const {
const char *TBeamLogger::currentLogPath() const
{
return storage_ ? storage_->currentLogPath() : "";
}
}
bool TBeamLogger::storageReady() const {
bool TBeamLogger::storageReady() const
{
return storage_ && storage_->ready() && storage_->isLogOpen();
}
}
void TBeamLogger::flush() {
if (storage_) {
void TBeamLogger::flush()
{
if (storage_)
{
storage_->flush();
}
if (serial_) {
if (serial_)
{
serial_->flush();
}
}
}
void TBeamLogger::closeLog() {
if (storage_) {
void TBeamLogger::closeLog()
{
if (storage_)
{
storage_->closeLog();
}
}
}
size_t TBeamLogger::write(uint8_t value) {
size_t TBeamLogger::write(uint8_t value)
{
return write(&value, 1);
}
}
size_t TBeamLogger::write(const uint8_t* buffer, size_t size) {
size_t TBeamLogger::write(const uint8_t *buffer, size_t size)
{
size_t serialWrote = 0;
size_t storageWrote = 0;
if (config_.echoSerial && serial_) {
if (config_.echoSerial && serial_)
{
serialWrote = serial_->write(buffer, size);
}
if (config_.echoStorage && storage_ && storage_->isLogOpen()) {
if (config_.echoStorage && storage_ && storage_->isLogOpen())
{
storageWrote = storage_->write(buffer, size);
}
return storageWrote > 0 ? storageWrote : serialWrote;
}
}
} // namespace tbeam