Wgranie zmian do repozytorium

This commit is contained in:
2026-05-10 16:46:04 +02:00
commit f171113450
1607 changed files with 254616 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
#include "ADXL345FastSPI.h"
static const char *TAG_FRESH = "ADXLFAST";
bool ADXL345FastSPI::begin(SPIClass *spi, uint32_t spiHz, Rate rate, Range range, uint8_t /*options*/){
spi_ = spi;
spiHz_ = spiHz;
odrHz_ = mapRateToHz(rate);
auto r = mapRange(range);
//spi_->begin(); 28.01.2026
presentCnt_ = 0;
for (uint8_t i = 0; i < count_; ++i) {
bool ok = dev_[i].begin(spi_, cs_[i], spiHz_)
&& dev_[i].setRange(r, true)
&& dev_[i].setODR_Hz(odrHz_);
dev_[i].showRangeFull(String(i));
if (ok) {
dev_[i].enableFIFO(ADXL345FreshSPI::FIFOmode::STREAM, 32); // wariant B
present_[i] = true;
presentCnt_++;
} else {
present_[i] = false;
}
}
return presentCnt_ > 0;
}
bool ADXL345FastSPI::availableAll() {
for (uint8_t i = 0; i < count_; ++i) {
if (!present_[i]) continue;
// else {
// dev_[i].showRangeFull("AVAILABLE ADXL");
// }
if (!dev_[i].available()) return false;
}
return true;
}
uint8_t ADXL345FastSPI::readAlignedOnce(int16_t* x, int16_t* y, int16_t* z, uint32_t* ts_us){
// 1) Czekaj aż każdy ma ≥1 próbkę (DATA_READY => FIFO>0)
while (!availableAll()) {
// micro-spin; w razie potrzeby można dodać yield() / feed watchdog poza hot-path
}
// 2) Zdejmij najstarszą z każdego sensora
uint8_t got = 0;
for (uint8_t i = 0; i < count_; ++i) {
if (!present_[i]) continue;
ADXL345FreshSPI::SampleI16 one[1];
size_t n = dev_[i].readFIFOBurst(one, 1);
if (n == 0) {
ADXL345FreshSPI::SampleI16 s;
if (!dev_[i].readFresh(s, 1)) break;
one[0] = s;
}
if (x) x[i] = one[0].x;
if (y) y[i] = one[0].y;
if (z) z[i] = one[0].z;
if (ts_us) ts_us[i] = one[0].ts_us;
got++;
}
return got;
}
bool ADXL345FastSPI::readNewSample(uint8_t idx, int16_t& x, int16_t& y, int16_t& z, bool& ready)
{
if (idx >= count_ || !present_[idx]) { ready = false; return false; }
ADXL345FreshSPI::SampleI16 one[1];
size_t n = dev_[idx].readFIFOBurst(one, 1);
if (n == 0) {
ADXL345FreshSPI::SampleI16 s;
if (!dev_[idx].readFresh(s, 1)) { ready = false; return false; }
one[0] = s;
}
x = one[0].x; y = one[0].y; z = one[0].z;
ready = true;
return true;
}
uint8_t ADXL345FastSPI::refreshConnectedSensorsCount(){
uint8_t cnt = 0;
for (uint8_t i = 0; i < count_; ++i) {
// ping() czyta DEVID (0xE5) wewnętrznie i zwraca true/false
if (dev_[i].ping()) {
present_[i] = true;
cnt++;
} else {
present_[i] = false;
}
}
presentCnt_ = cnt;
return cnt;
}
/*
Zwraca maskę bitową aktywnych sensorów (np. do szybkiej diagnostyki: który CS nie odpowiada)
Uzycie:
uint8_t mask = adxl.refreshActiveMask();
ESP_LOGI(TAG_MAIN, "Aktywne sensory (bitmask): 0x%02X, count=%u",
mask, adxl.connectedSensorsCount());
// Sprawdzenie konkretnego sensora:
if (mask & (1u << 3)) {
ESP_LOGI(TAG_MAIN, "Sensor #3 online");
} else {
ESP_LOGW(TAG_MAIN, "Sensor #3 offline");
}
*/
uint8_t ADXL345FastSPI::refreshActiveMask(){
uint8_t m = 0;
uint8_t cnt = 0;
for (uint8_t i = 0; i < count_; ++i) {
if (dev_[i].ping()) {
present_[i] = true;
m |= (1u << i);
cnt++;
} else {
present_[i] = false;
}
}
presentCnt_ = cnt;
return m;
}
// Pobiera zakres wybranego akcelerometru
uint8_t ADXL345FastSPI::getRange(uint8_t accel){
return dev_[accel].getADXLRange();
}
bool ADXL345FastSPI::getFullRes(uint8_t accel){
return dev_[accel].getADXLFullRes();
}

View File

@@ -0,0 +1,242 @@
#include "ADXL345FreshSPI.h"
static const char *TAG_FRESH = "ADXLFRESH";
static inline int16_t u8pair_to_i16(uint8_t lo, uint8_t hi) {
return (int16_t)((hi << 8) | lo);
}
bool ADXL345FreshSPI::begin(SPIClass* s, uint8_t csPin, uint32_t clockHz) {
spi = s; cs = csPin; spiHz = clockHz;
pinMode(cs, OUTPUT); digitalWrite(cs, HIGH);
//spi->begin(); // tymczas
delay(1);
if (!ping()) return false;
// 1. Wymuś STANDBY przed jakąkolwiek konfiguracją
write8(ADXL345_REG_POWER_CTL, 0x00);
delay(1);
// 2. Skonfiguruj format danych (Range i Full_Res)
if (!setRange(Range::G16, true)) { // było if (!setRange(Range::G2, true)) return false;
ESP_LOGI(TAG_FRESH, "Range G16 ERROR!");
return false;
}
// 3. Skonfiguruj ODR
if (!setODR_Hz(100.0f)) return false; // domyślnie 100 Hz
// 4. Dopiero teraz włącz pomiar
if (!configurePowerMeasure()) return false;
showRangeFull();
return true;
}
bool ADXL345FreshSPI::ping() {
uint8_t id=0; if (!read8(ADXL345_REG_DEVID, id)) return false;
return id == 0xE5;
}
bool ADXL345FreshSPI::configurePowerMeasure() {
return write8(ADXL345_REG_POWER_CTL, ADXL345_POWER_MEASURE);
}
bool ADXL345FreshSPI::setRange(Range r, bool fullRes_) {
fullRes = fullRes_;
// Wymuś STANDBY (MEASURE=0) kluczowe dla zmiany RANGE/FULL_RES
write8(ADXL345_REG_POWER_CTL, 0x00);
delayMicroseconds(5);
uint8_t fmt = 0;
if (!read8(ADXL345_REG_DATA_FORMAT, fmt)) return false;
fmt &= ~ADXL345_DATA_FORMAT_RANGE_MASK; // bity 1:0
fmt |= (uint8_t)r;
if (fullRes) fmt |= ADXL345_DATA_FORMAT_FULL_RES; // bit 3
else fmt &= ~ADXL345_DATA_FORMAT_FULL_RES;
if (!write8(ADXL345_REG_DATA_FORMAT, fmt)) return false;
// Kontrola: odczyt po zapisie
//uint8_t verify = 0;
//read8(ADXL345_REG_DATA_FORMAT, verify);
// tu możesz logować verify
// Wróć do MEASURE
write8(ADXL345_REG_POWER_CTL, ADXL345_POWER_MEASURE);
scale_g_per_lsb = fullRes ? 0.0039f : (1.0f/256.0f) * (2 << (uint8_t)r);
return true;
}
uint8_t ADXL345FreshSPI::odrCodeFromHz(float hz) {
struct { uint8_t code; float f; } map[] = {
{0x06, 6.25f},{0x07,12.5f},{0x08,25.f},{0x09,50.f},{0x0A,100.f},
{0x0B,200.f},{0x0C,400.f},{0x0D,800.f},{0x0E,1600.f},{0x0F,3200.f}
};
uint8_t best=0x0A; float bestErr=1e9f;
for (auto &e: map){ float err=fabsf(e.f-hz); if (err<bestErr){bestErr=err; best=e.code;}}
return best;
}
bool ADXL345FreshSPI::setODR_Hz(float hz) {
return write8(ADXL345_REG_BW_RATE, odrCodeFromHz(hz));
}
bool ADXL345FreshSPI::enableFIFO(FIFOmode mode, uint8_t triggerLevel) {
triggerLevel = constrain(triggerLevel, (uint8_t)1, (uint8_t)32);
uint8_t m = ADXL345_FIFO_BYPASS;
switch(mode){
case FIFOmode::BYPASS: m = ADXL345_FIFO_BYPASS; break;
case FIFOmode::FIFO: m = ADXL345_FIFO_FIFO; break;
case FIFOmode::STREAM: m = ADXL345_FIFO_STREAM; break;
case FIFOmode::TRIGGER: m = ADXL345_FIFO_TRIGGER;break;
}
return write8(ADXL345_REG_FIFO_CTL, (uint8_t)(m | ((triggerLevel-1) & 0x1F)));
}
bool ADXL345FreshSPI::enableDataReadyInterrupt(bool enable) {
uint8_t ie=0; if (!read8(ADXL345_REG_INT_ENABLE, ie)) return false;
if (enable) ie |= ADXL345_DATA_READY_BIT;
else ie &= ~ADXL345_DATA_READY_BIT;
return write8(ADXL345_REG_INT_ENABLE, ie);
}
bool ADXL345FreshSPI::available() {
uint8_t src=0; if (!read8(ADXL345_REG_INT_SOURCE, src)) return false;
return (src & ADXL345_DATA_READY_BIT) != 0;
}
bool ADXL345FreshSPI::readFresh(SampleI16& out, uint32_t timeout_ms) {
uint32_t start = millis();
while (!available()) {
if ((millis() - start) > timeout_ms) return false;
delayMicroseconds(200);
}
uint8_t buf[6];
if (!readMulti(ADXL345_REG_DATAX0, buf, 6)) return false;
out.x = u8pair_to_i16(buf[0], buf[1]);
out.y = u8pair_to_i16(buf[2], buf[3]);
out.z = u8pair_to_i16(buf[4], buf[5]);
out.ts_us = micros();
return true;
}
bool ADXL345FreshSPI::readFresh(SampleSI& out, uint32_t timeout_ms) {
SampleI16 raw;
if (!readFresh(raw, timeout_ms)) return false;
countsToSI(raw, out);
return true;
}
size_t ADXL345FreshSPI::readFIFOBurst(SampleI16* buf, size_t maxCount) {
if (!buf || maxCount==0) return 0;
uint8_t status=0; if (!read8(ADXL345_REG_FIFO_STATUS, status)) return 0;
uint8_t entries = status & 0x3F; // 0..32
size_t n = min<size_t>(entries, maxCount);
for (size_t i=0;i<n;i++){
uint8_t d[6];
if (!readMulti(ADXL345_REG_DATAX0, d, 6)) return i;
buf[i].x = u8pair_to_i16(d[0], d[1]);
buf[i].y = u8pair_to_i16(d[2], d[3]);
buf[i].z = u8pair_to_i16(d[4], d[5]);
buf[i].ts_us = micros();
}
return n;
}
size_t ADXL345FreshSPI::readFIFOBurst(SampleSI* buf, size_t maxCount) {
if (!buf || maxCount==0) return 0;
size_t n = 0;
while (n < maxCount) {
uint8_t status=0; if (!read8(ADXL345_REG_FIFO_STATUS, status)) break;
uint8_t entries = status & 0x3F;
if (entries == 0) break;
uint8_t d[6];
uint32_t t = micros();
if (!readMulti(ADXL345_REG_DATAX0, d, 6)) break;
SampleI16 s;
s.x = u8pair_to_i16(d[0], d[1]);
s.y = u8pair_to_i16(d[2], d[3]);
s.z = u8pair_to_i16(d[4], d[5]);
s.ts_us = t;
countsToSI(s, buf[n]);
n++;
}
return n;
}
void ADXL345FreshSPI::countsToSI(const SampleI16& in, SampleSI& out) {
out.ax_g = in.x * scale_g_per_lsb;
out.ay_g = in.y * scale_g_per_lsb;
out.az_g = in.z * scale_g_per_lsb;
out.ax_ms2 = out.ax_g * g_ms2;
out.ay_ms2 = out.ay_g * g_ms2;
out.az_ms2 = out.az_g * g_ms2;
out.ts_us = in.ts_us;
}
// ---------- Low-level SPI -----------
bool ADXL345FreshSPI::write8(uint8_t reg, uint8_t val) {
spi->beginTransaction(SPISettings(spiHz, MSBFIRST, SPI_MODE3));
spiSelect();
spi->transfer(reg & 0x3F); // write, single
spi->transfer(val);
spiDeselect();
spi->endTransaction();
return true;
}
bool ADXL345FreshSPI::read8(uint8_t reg, uint8_t& val) {
spi->beginTransaction(SPISettings(spiHz, MSBFIRST, SPI_MODE3));
spiSelect();
spi->transfer(0x80 | (reg & 0x3F)); // read, single
val = spi->transfer(0x00);
spiDeselect();
spi->endTransaction();
return true;
}
bool ADXL345FreshSPI::readMulti(uint8_t reg, uint8_t *dst, size_t n) {
if (n==0) return true;
spi->beginTransaction(SPISettings(spiHz, MSBFIRST, SPI_MODE3));
spiSelect();
spi->transfer(0xC0 | (reg & 0x3F)); // read, multi (MB=1, R/W=1)
for (size_t i=0;i<n;i++) dst[i] = spi->transfer(0x00);
spiDeselect();
spi->endTransaction();
return true;
}
void ADXL345FreshSPI::spiSelect() { digitalWrite(cs, LOW); }
void ADXL345FreshSPI::spiDeselect() { digitalWrite(cs, HIGH); }
uint8_t ADXL345FreshSPI::getADXLRange() {
uint8_t format = 0;
if (!read8(0x31, format)) return 255; // albo 0
switch (format & 0x03) {
case 0: return 2;
case 1: return 4;
case 2: return 8;
case 3: return 16;
}
return 255;
}
bool ADXL345FreshSPI::getADXLFullRes() {
uint8_t format = 0;
if (!read8(0x31, format)) return false;
//bool ok = read8(0x31, format);
//ESP_LOGI(TAG_FRESH, "read8 ok=%d DATA_FORMAT=0x%02X", ok, format);
return (format & 0x08) != 0;
}
void ADXL345FreshSPI::showRangeFull(String txt) {
uint8_t format = getADXLRange();
bool full_res = getADXLFullRes();
ESP_LOGI(TAG_FRESH, "%s ADXL345: RANGE=%dG FULL_RES=%d", txt, format, full_res);
}

View File

@@ -0,0 +1,111 @@
#include "APIClient.h"
#include <WiFiClient.h>
#include <base64.h>
static const char* TAG_API = "API";
APIClient::APIClient() {
}
bool APIClient::uploadMeasurement(const String& filePath) {
if (WiFi.status() != WL_CONNECTED) {
ESP_LOGE(TAG_API, "No WiFi connection.");
return false;
}
File file = SD.open(filePath, FILE_READ);
if (!file) {
ESP_LOGE(TAG_API, "Failed to open file %s", filePath.c_str());
return false;
}
String filename = filePath;
int slashIndex = filename.lastIndexOf('/');
if (slashIndex >= 0) {
filename = filename.substring(slashIndex + 1);
}
String boundary = "----WebKitFormBoundaryESP32WMT";
String head = "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\n"
+ "Content-Type: application/octet-stream\r\n\r\n";
String tail = "\r\n--" + boundary + "--\r\n";
size_t fileSize = file.size();
size_t totalLength = head.length() + fileSize + tail.length();
WiFiClient client;
String host = String(config.restURL);
int port = config.restPort;
if (host.indexOf("/accels/api1") >= 0) {
host = "http://62.93.60.19";
port = 5004;
}
host.replace("http://", "");
host.replace("https://", "");
ESP_LOGI(TAG_API, "Connecting to %s:%d for upload", host.c_str(), port);
if (!client.connect(host.c_str(), port)) {
ESP_LOGE(TAG_API, "Connection failed to host %s:%d", host.c_str(), port);
file.close();
return false;
}
ESP_LOGI(TAG_API, "Uploading %s (%d bytes)", filePath.c_str(), fileSize);
client.println("POST /api/v1/measurements/measurements/upload HTTP/1.1");
client.println("Host: " + host + ":" + String(port));
String auth = String(config.restUser) + ":" + String(config.restPass);
String authBase64 = base64::encode(auth);
client.println("Authorization: Basic " + authBase64);
client.println("Content-Length: " + String(totalLength));
client.println("Content-Type: multipart/form-data; boundary=" + boundary);
client.println("Connection: close");
client.println();
client.print(head);
uint8_t buffer[2048];
while (file.available()) {
size_t len = file.read(buffer, sizeof(buffer));
client.write(buffer, len);
Watchdog::feed();
}
client.print(tail);
file.close();
int httpCode = 0;
String responseLine;
unsigned long timeout = millis();
while (client.connected() && millis() - timeout < 15000) {
if (client.available()) {
responseLine = client.readStringUntil('\n');
responseLine.trim();
if (responseLine.startsWith("HTTP/1.1 ")) {
httpCode = responseLine.substring(9, 12).toInt();
}
if (responseLine.length() == 0) break;
}
Watchdog::feed();
}
String responseBody = "";
while(client.available()){
responseBody += client.readString();
}
client.stop();
if (httpCode == 201) {
ESP_LOGI(TAG_API, "Upload successful: 201");
return true;
} else if (httpCode == 401) {
ESP_LOGE(TAG_API, "Unauthorized 401: Wrong serial or password!");
} else {
ESP_LOGE(TAG_API, "Upload failed with code %d. Response: %s", httpCode, responseBody.c_str());
}
return false;
}

View File

@@ -0,0 +1,184 @@
#include <Config.h>
Config config;
ConfigManager::ConfigManager() {}
// EEPROM initialize
void ConfigManager::begin() {
ESP_LOGI(TAG_CONF, "Begin config");
EEPROM.begin(EEPROM_SIZE);
if (isEEPROMEmpty()) {
ESP_LOGI(TAG_CONF, "EEPROM Empty");
resetToDefaults();
generateApiKey(config.apiKey, sizeof(config.apiKey));
saveConfig();
readConfig();
} else {
//Logger::getInstance().log(LOG_INFO, "READ Config");
readConfig();
}
}
// Check is EEPROM is empty
bool ConfigManager::isEEPROMEmpty() {
if (EEPROM.read(0) != 251){
ESP_LOGI(TAG_CONF, "EEPROM is new!");
return true;
} else {
return false;
}
}
// Read configuration from EEPROM
void ConfigManager::readConfig() {
ESP_LOGI(TAG_CONF, "Read config from EEPROM");
EEPROM.get(1, config);
}
// Save config to EEPROM
void ConfigManager::saveConfig() {
ESP_LOGI(TAG_CONF, "SAVE CONFIG");
EEPROM.put(1, config);
EEPROM.write(0, 251);
if (EEPROM.commit()) {
ESP_LOGI(TAG_CONF, "Config saved");
} else {
ESP_LOGE(TAG_CONF, "Error save config");
}
EEPROM.end();
}
void ConfigManager::generateApiKey(uint8_t *buf, size_t len) {
for (size_t i = 0; i < len; i += 4) {
uint32_t r = esp_random(); // losowe 32 bity z TRNG ESP32
size_t chunk = (len - i >= 4) ? 4 : (len - i); // ostatnia iteracja może być < 4 bajtów
memcpy(buf + i, &r, chunk);
}
}
// Factory reset EEPROM
void ConfigManager::resetToDefaults() {
ESP_LOGI(TAG_CONF, "EEPROM RESET FACTORY");
EEPROM.begin(EEPROM_SIZE);
for (int i = 0; i < EEPROM_SIZE; i++) {
EEPROM.write(i, 0);
}
EEPROM.write(0, 0);
strcpy(config.ssid, "politechnika");
strcpy(config.password, "");
strcpy(config.hostname, "WMT001");
strcpy(config.place, "WMT Stalowa Wola");
config.dhcp = 1;
strcpy(config.ip, "192.168.0.10");
strcpy(config.subnet, "255.255.255.0");
strcpy(config.gateway, "192.168.0.1");
strcpy(config.dns, "8.8.8.8");
strcpy(config.user, "admin");
strcpy(config.pass, "admin");
strcpy(config.ntp, "pl.pool.ntp.org");
config.connect = 0; // urządzenie połączone z siecią lub 0 offline
config.measure = 1; // włącz automatyczny pomiar co x sekunt (pause)
config.duration = 5; // czas trwania pomiaru 5 sekund
config.pause = 10000; // odstęp pomiędzy pomiarami w ms
//strcpy(config.ntp, "0.pl.pool.ntp.org");
strcpy(config.restURL, "http://62.93.60.19");
config.restPort = 5004;
strcpy(config.restUser, "wmt");
strcpy(config.restPass, "Zaq12wsx");
strcpy(config.S0, "ACCEL1");
strcpy(config.S1, "ACCEL2");
strcpy(config.S2, "ACCEL3");
strcpy(config.S3, "ACCEL4");
strcpy(config.S4, "ACCEL5");
strcpy(config.S5, "ACCEL6");
strcpy(config.S6, "ACCEL7");
strcpy(config.S7, "ACCEL8");
EEPROM.put(1, config);
EEPROM.write(0, 251);
saveConfig();
readConfig();
isRebootRequired = true;
}
void ConfigManager::showConfig(){
ESP_LOGI(TAG_CONF, "Config size: %d bytes max %d", sizeof(config), EEPROM_SIZE);
String ii;
if(config.connect)
ii = "online";
else
ii = "offline";
ESP_LOGI(TAG_CONF, "Mode: %s", ii.c_str());
if(config.measure)
ii = "auto";
else
ii = "manual";
ESP_LOGI(TAG_CONF, "MEASURE: %s", ii.c_str());
ii = "PLACE: " + String(config.place);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "HOSTNAME: " + String(config.hostname);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "WIFI SSID: " + String(config.ssid);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "WIFI PASS: " + String(config.password);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
if(config.dhcp)
ii = "yes";
else
ii = "no";
ESP_LOGI(TAG_CONF, "DHCP: %s", ii.c_str());
ii = "IP: " + String(config.ip);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "MASK: " + String(config.subnet);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "GATEWAY: " + String(config.gateway);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "DNS: " + String(config.dns);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "USER: " + String(config.user);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "USER PASS: " + String(config.pass);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
if(config.connect) {
ii = "URL: " + String(config.restURL);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "PORT: " + String(config.restPort);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "USER: " + String(config.restUser);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "PASS: " + String(config.restPass);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
}
for (size_t i = 0; i < 32; i++) {
if (config.apiKey[i] < 0x10) Serial.print("0");
Serial.print(config.apiKey[i], HEX);
}
Serial.println();
ii = "Delay: " + String(config.pause) + "ms";
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "S1: " + String(config.S0);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "S2: " + String(config.S1);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "S3: " + String(config.S2);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "S4: " + String(config.S3);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "S5: " + String(config.S4);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "S6: " + String(config.S5);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "S7: " + String(config.S6);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
ii = "S8: " + String(config.S7);
ESP_LOGI(TAG_CONF, "%s", ii.c_str());
}

View File

@@ -0,0 +1,279 @@
#include <Arduino.h>
#include "Display.h"
static const char *DISP = "display";
Display::Display(RTC_DS3231 &rtc, uint8_t address, uint8_t columns, uint8_t rows):rtc_(rtc) {
_lcd = new LiquidCrystal_I2C(address, columns, rows);
_address = address;
_columns = columns;
_rows = rows;
}
bool Display::begin(TwoWire *wire) {
wire->begin();
wire->beginTransmission(_address);
if (wire->endTransmission() != 0) {
return false; // brak odpowiedzi — error
}
// Inicjalizacja LCD
_lcd->begin(_columns, _rows);
_lcd->backlight();
_lcd->clear();
delay(100);
return true;
}
void Display::initMeasure(bool measure, bool run, bool runmes, uint16_t pause, uint8_t duration,
bool connect, long counter, String gdate, String gtime) {
clear();
textCenter(0, "** Measure params **");
String conting = measure ? "yes":"no";
String st2 = "Continous:";
String allt = st2 + conting;
textCenter(1, allt.c_str());
String type = "Sample:" + String(duration) + "s Pause:" + String(pause/1000)+"s";
textCenter(2, type.c_str());
textCenter(3, String("Time:" + gtime).c_str());
textStatus("initialisation");
}
void Display::clear() {
_lcd->clear();
}
void Display::textCenter(uint8_t line, const char *txt){
if (line >= _rows) {
ESP_LOGE(DISP, "Line >= max rows!");
return;
}
_lcd->setCursor(0, line);
int len = strlen(txt);
if (len < _columns) {
int pad = (_columns - len) / 2;
for (int i = 0; i < pad; i++) {
_lcd->print(" ");
}
}
_lcd->print(txt);
}
void Display::text(uint8_t line, const char *text) {
if (line >= _rows) {
ESP_LOGE(DISP, "Line >= max rows!");
return;
}
_lcd->setCursor(0, line);
_lcd->print(text);
}
void Display::welcomeScreen() {
ESP_LOGI(DISP, "Info screen");
clear();
textCenter(0, "** Vibra Dude **");
textCenter(1, "WMT Stalowa Wola");
String firm = String(VERSION);
firm.trim();
textCenter(2, firm.c_str());
//_lcd->print(VERSION);
}
void Display::mainScreen(){
ESP_LOGI(DISP, "Main screen");
clear();
textCenter(0, "** Vibra Dude **");
textCenter(1, "WMT Stalowa Wola");
//String firm = String(VERSION).trim();
//textCenter(2, firm.c_str());
//_lcd->print(VERSION);
ESP_LOGI(DISP, "Finish main screen");
}
void Display::clearRow(uint16_t line){
_lcd->setCursor(0, line);
_lcd->print(" ");
_lcd->setCursor(0, line);
}
void Display::textStatus(const char *text){
clearRow(3);
_lcd->setCursor(0, 3);
_lcd->print(text);
}
void Display::updateNetwork(String ip, bool connected) {
// przykładowa prosta implementacja:
// clear();
// text(0, connected ? "NET: OK" : "NET: OFF");
// _lcd->setCursor(0, 1);
// _lcd->print(ip);
}
void Display::updateBarWiFi(long signal, bool connected) {
// tu możesz wykorzystać np. init_bargraph / draw_horizontal_graph
// (void)signal;
// (void)connected;
}
void Display::textStyle(const uint8_t st, String text) {
// miejsce na różne style tekstu
//(void)st;
// clear();
//text(0, text.c_str());
}
void Display::message(String text) {
// clear();
//text(0, text.c_str());
}
void Display::print(String text) {
_lcd->print(text);
}
void Display::println(String text) {
_lcd->print(text);
_lcd->print("\n");
}
void Display::setCursor(int16_t x, int16_t y) {
_lcd->setCursor((uint8_t)x, (uint8_t)y);
}
void Display::showAccel(float a, float b, float c) {
clear();
// _lcd->setCursor(0, 0);
// _lcd->print("Ax: ");
// _lcd->print(a, 2);
// _lcd->setCursor(0, 1);
// _lcd->print("Ay: ");
// _lcd->print(b, 2);
// _lcd->setCursor(0, 2);
// _lcd->print("Az: ");
// _lcd->print(c, 2);
}
void Display::displayOffline(bool measure, uint8_t count, float freeSpace, long licznik, bool refresh){
if(refresh){
oyear, omonth, oday, ohour, omin, osec, ospace, oadxlcnt = 100;
_lcd->clear();
_lcd->setCursor(0, 0);
}
DateTime now = rtc_.now();
uint16_t year = now.year();
uint8_t month = now.month();
uint8_t day = now.day();
uint8_t hour = now.hour();
uint8_t min = now.minute();
uint8_t sec = now.second();
// DEBUG
//ESP_LOGI(DISP, "RTC: %d:%d:%d %d:%d:%d", now.day(), now.month(), now.year(), now.hour(), now.minute(), now.second());
//ESP_LOGI(DISP, "RTC: %d:%d:%d %d:%d:%d", oday, omonth, oyear, ohour, omin, osec);
if ((hour != ohour) || (refresh)) {
ohour = hour;
_lcd->setCursor(0, 0);
if (hour < 10) _lcd->print('0');
_lcd->print(hour);
_lcd->setCursor(2, 0);
_lcd->print(':');
}
if ((min != omin) || (refresh)) {
omin = min;
_lcd->setCursor(3, 0);
if (min < 10) _lcd->print('0');
_lcd->print(min);
_lcd->print(':');
}
if ((sec != osec) || (refresh)) {
_lcd->setCursor(6, 0);
osec = sec;
if (sec < 10) _lcd->print('0');
_lcd->print(sec);
}
if ((day != oday || month != omonth || year != oyear) || (refresh)){
_lcd->setCursor(10, 0);
if (day < 10) _lcd->print('0');
_lcd->print(day);
_lcd->print('.');
if (month < 10) _lcd->print('0');
_lcd->print(month);
_lcd->print('.');
_lcd->print(year);
oday = day;
omonth = month;
oyear = year;
if(!config.measure)
textStatus("OK: Start MEASURE");
else
textStatus("Wait for NEXT");
}
if((freeSpace != ospace) || (refresh)){
ospace = freeSpace;
_lcd->setCursor(0, 1);
_lcd->print("FREE:");
_lcd->print(freeSpace);
_lcd->print("GB");
}
if((count != oadxlcnt) || (omode != config.measure) || (refresh)){
oadxlcnt = count;
omode = config.measure;
_lcd->setCursor(0, 2);
_lcd->print("ADXL:");
_lcd->print(count);
_lcd->print(" MODE:");
_lcd->print(config.measure ? "AUTO ":"MANUAL ");
}
}
// Podczas pomiaru
void Display::displayOnOffM(bool measure, String myDir, String myFile) {
clear();
_lcd->setCursor(0, 0);
_lcd->print(measure ? "MEASURE STARTED" : "MEASURE STOPPED");
_lcd->setCursor(0, 1);
_lcd->print("DIR: " + myDir);
//_lcd->print(myDir);
//_lcd->setCursor(0, 2);
//_lcd->print("FILE:");
//_lcd->setCursor(0, 3);
_lcd->setCursor(0, 2);
_lcd->print(myFile);
}
/* Podsumowanie po pomiarze: ramki, sampling, etc. */
void Display::displaySampleRateSummary(uint32_t reccount, uint32_t captureSeconds, float khz, String filename){
clear();
_lcd->setCursor(0, 0);
if (captureSeconds == 0) {
_lcd->print("Sampling time 0!");
delay(1000);
return;
}
float fs = (float)reccount / (float)captureSeconds; // Hz (ramki/s)
float fs_kHz = fs / 1000.0f;
_lcd->setCursor(0,0);
_lcd->print("Frames:");
_lcd->print((unsigned)reccount);
_lcd->setCursor(0,1);
_lcd->print("Time:");
_lcd->print((unsigned)captureSeconds);
_lcd->print("s.");
_lcd->setCursor(0,2);
_lcd->print("Rate:");
_lcd->print(fs_kHz);
_lcd->print("kHz");
_lcd->setCursor(0,3);
_lcd->print(filename);
}

View File

@@ -0,0 +1,16 @@
#include <Logger.h>
const char *TAG_MAIN = "MAIN";
const char *TAG_DISPLAY = "DISPLAY";
const char *TAG_WIFI = "WiFi";
const char *TAG_ADXL = "ADXL345";
const char *TAG_CONF = "CONFIG";
void init_log_levels() {
esp_log_level_set(TAG_MAIN, ESP_LOG_INFO);
esp_log_level_set(TAG_DISPLAY, ESP_LOG_INFO);
esp_log_level_set(TAG_WIFI, ESP_LOG_INFO);
esp_log_level_set(TAG_ADXL, ESP_LOG_INFO);
esp_log_level_set(TAG_CONF, ESP_LOG_INFO);
}

View File

@@ -0,0 +1,706 @@
#include <Measure.h>
static const char *TAG_CAPTURE = "CAPTURE";
// Był na sztywno przydzielony bufor 128 KB. Zmieniam tak, aby zaalokować 4 MB
// lub 6 MB (trzeba zostawić resztę na potrzeby systemu).
DataCapture::DataCapture(ADXL345FastSPI &adxl, Display &display,
RTC_DS3231 &rtc, fs::FS &storage, size_t bufferSize)
: adxl_(adxl), display_(display), rtc_(rtc), _fs(storage) {
// Ustawiamy duży bufor dla PSRAM (np. 4 MB = 4194304 bajty)
// 4 * 1024 * 1024 / 12 bajtów = ~349 525 próbek
// Możesz to przekazać jako parametr lub wpisać na sztywno:
if (bufferSize < 4 * 1024 * 1024)
bufferSize = 4 * 1024 * 1024;
bufferSize_ = bufferSize;
#if defined(ESP32)
// ps_malloc jest kluczowy dla PSRAM. Wymuszamy alokację w zewnętrznym PSRAM
buffer_ = (uint8_t *)ps_malloc(bufferSize_);
#endif
if (!buffer_)
buffer_ = (uint8_t *)malloc(bufferSize_);
if (!buffer_) {
ESP_LOGE(TAG_CAPTURE, "No mem for buffer (%u B)", (unsigned)bufferSize_);
} else {
ESP_LOGI(TAG_CAPTURE, "Capture buffer: %u B", (unsigned)bufferSize_);
}
}
DataCapture::~DataCapture() {
if (buffer_) {
free(buffer_);
buffer_ = nullptr;
}
}
void DataCapture::stop() { measurementActive_ = false; }
// --- ZAPIS BUFORA NA SD ---
bool DataCapture::flushToFile(File &f) {
if (bufferIndex_ == 0)
return true;
Watchdog::feed();
size_t written = f.write(buffer_, bufferIndex_);
if (written != bufferIndex_) {
ESP_LOGE(TAG_CAPTURE, "SD save error (written=%u, expected=%u)",
(unsigned)written, (unsigned)bufferIndex_);
display_.textStatus("SD save error");
bufferIndex_ = 0;
return false;
}
bufferIndex_ = 0;
return true;
}
// CRC8 (zostawione nieużywane domyślnie)
uint8_t DataCapture::crc8(const uint8_t *data, size_t len) {
uint8_t crc = 0x00;
for (size_t i = 0; i < len; ++i) {
crc ^= data[i];
for (uint8_t b = 0; b < 8; ++b) {
if (crc & 0x80)
crc = (crc << 1) ^ 0x07;
else
crc <<= 1;
}
}
return crc;
}
void DataCapture::setTestingIndicator_(bool on, String myDir, String myFile) {
display_.displayOnOffM(on, myDir, myFile);
}
// --- automatyczna nazwa pliku ---
bool DataCapture::captureAuto(uint32_t captureSeconds,
const char * /*baseDirectory*/) {
isExit = false;
Watchdog::feed();
String path = allocateNextFilePath();
if (path.isEmpty()) {
ESP_LOGE(TAG_CAPTURE, "Error generate file name in %s", _baseDir.c_str());
display_.textStatus("Error file");
return false;
}
ESP_LOGI(TAG_CAPTURE, "Autogenerate file: %s", path.c_str());
display_.textStatus(path.c_str());
Watchdog::feed();
return capture(captureSeconds, path.c_str());
}
// --- main measure function ---
// bool DataCapture::capture(uint32_t captureSeconds, const char *filename) {
// Watchdog::feed();
// if (!buffer_) {
// ESP_LOGE(TAG_CAPTURE, "No buffer - cancel.");
// display_.textStatus("Buffer error");
// return false;
// }
// ESP_LOGI(TAG_CAPTURE, "Capture %u sec. -> %s", (unsigned)captureSeconds,
// filename);
// File dataFile = _fs.open(filename, FILE_WRITE);
// if (!dataFile) {
// ESP_LOGE(TAG_CAPTURE, "Can't open file: %s", filename);
// char buf[100];
// snprintf(buf, sizeof(buf), "Can't open: %s", filename);
// display_.textStatus(buf);
// //s2etTestingIndicator_(false, _baseDir, filename);
// return false;
// }
// // Bufory RAMKI (po 1 próbce z każdego sensora)
// static const uint8_t MAXN = 7; // zgodnie z projektem
// int16_t X[MAXN]{}, Y[MAXN]{}, Z[MAXN]{};
// uint32_t TS[MAXN]{};
// Watchdog::feed();
// const uint32_t tStart_us = micros();
// const uint32_t captureDuration_us = captureSeconds * 1000000UL;
// // Czas startu (UTC) — tylko w nagłówku!
// const DateTime now = rtc_.now();
// const int32_t unix_start = now.unixtime();
// // Nagłówek
// FileHeader hdr;
// memcpy(hdr.magic, "WMT", 3);
// hdr.version = 1;
// hdr.headerSize = sizeof(FileHeader);
// hdr.sampleSize = sizeof(Sample);
// hdr.timestamp = unix_start;
// hdr.reccount = 0;
// if (dataFile.write(reinterpret_cast<const uint8_t*>(&hdr), sizeof(hdr)) !=
// sizeof(hdr)) {
// ESP_LOGE(TAG_CAPTURE, "Header write failed");
// display_.textStatus("Header SD failed");
// dataFile.close();
// return false;
// }
// measurementActive_ = true;
// bufferIndex_ = 0;
// setTestingIndicator_(true, _baseDir, filename);
// uint32_t frames = 0; // liczba zebranych „ramek”
// const uint8_t presentCnt = adxl_.size(); // wykryte sensory (wg begin)
// // --- Pętla akwizycji wyrównanych ramek ---
// while (measurementActive_) {
// if(isEscape()) break; // wyjście z pętli gdy OK przerwie
// uint32_t now_us = micros();
// if ((now_us - tStart_us) >= captureDuration_us) break;
// // 1) Czekamy aż KAŻDY obecny sensor ma >= 1 próbkę w FIFO (DATA_READY)
// while (!adxl_.availableAll()) {
// if(isEscape()) break; //????? sprawdź kHz pomiaru!
// // krótki spin; unikamy delay(1), aby nie zrywać 3.2 kHz
// // (opcjonalnie yield(); jeśli system wymaga)
// }
// // 2) Zdejmij po 1 NAJSTARSZEJ próbce z każdego sensora (STREAM FIFO)
// const uint8_t got = adxl_.readAlignedOnce(X, Y, Z, TS);
// if (got != presentCnt) {
// // rzadki przypadek niepełna ramka; pomiń
// continue;
// }
// // 3) Wspólny timestamp ramki: minimalny z TS[i]
// uint32_t tmin = UINT32_MAX;
// for (uint8_t i = 0; i < MAXN; ++i) {
// if (!adxl_.isPresent(i)) continue;
// if (TS[i] < tmin) tmin = TS[i];
// }
// const uint32_t frame_offset_us = tmin - tStart_us;
// // 4) Zapis 7 rekordów Sample do bufora
// for (uint8_t i = 0; i < MAXN; ++i) {
// if (!adxl_.isPresent(i)) continue;
// // Konstruuj rekord bezpośrednio w buforze (bez memcpy)
// if (bufferIndex_ + sizeof(Sample) > bufferSize_) {
// if (!flushToFile(dataFile)) { measurementActive_ = false; break; }
// }
// Sample *dst = reinterpret_cast<Sample*>(buffer_ + bufferIndex_);
// dst->offset = frame_offset_us;
// dst->sensor_id = i;
// dst->x = X[i]; dst->y = Y[i]; dst->z = Z[i];
// dst->ready = true;
// bufferIndex_ += sizeof(Sample);
// }
// if (!measurementActive_) break;
// frames++;
// // Watchdog rzadziej, żeby nie zwiększać jittera
// if ((frames & 0xFF) == 0) Watchdog::feed();
// } // while
// // Statystyki
// ESP_LOGI(TAG_CAPTURE, "Frames: %u, sensors: %u", (unsigned)frames,
// (unsigned)presentCnt); printSamplingRate(frames, captureSeconds); //
// liczymy ramki/sek.
// // Domknij bufor
// if (bufferIndex_ > 0) {
// if (!flushToFile(dataFile)) {
// Watchdog::feed();
// ESP_LOGE(TAG_CAPTURE, "Finish save error.");
// display_.textStatus("Save SD error");
// delay(1000);
// }
// }
// ESP_LOGI(TAG_CAPTURE, "Saved to SD successful");
// display_.textStatus("Saved SD OK");
// delay(700);
// // Uzupełnij nagłówek o liczbę rekordów (Sample)
// hdr.reccount = frames * presentCnt;
// dataFile.seek(0);
// dataFile.write(reinterpret_cast<const uint8_t*>(&hdr), sizeof(hdr));
// dataFile.flush();
// dataFile.close();
// //setTestingIndicator_(false, _baseDir, filename);
// bool ok = measurementActive_;
// measurementActive_ = false;
// if (ok) {
// ESP_LOGI(TAG_CAPTURE, "Successful");
// display_.textStatus("Successfull");
// Watchdog::feed();
// return true;
// } else {
// ESP_LOGE(TAG_CAPTURE, "Measurement aborted");
// display_.textStatus("Aborted");
// Watchdog::feed();
// return false;f
// }
// }
// --- main measure function --- V2
bool DataCapture::capture(uint32_t captureSeconds, const char *filename) {
Watchdog::feed();
if (!buffer_) {
ESP_LOGE(TAG_CAPTURE, "No buffer - cancel.");
display_.textStatus("Buffer error");
return false;
}
// Obliczamy ile danych maksymalnie może wejść do bufora
const uint8_t presentCnt = adxl_.size();
const size_t frameSize = presentCnt * sizeof(Sample);
ESP_LOGI(TAG_CAPTURE, "Start RAM capture: %u sec.", (unsigned)captureSeconds);
display_.textStatus("Sampling...");
static const uint8_t MAXN = 7;
int16_t X[MAXN]{}, Y[MAXN]{}, Z[MAXN]{};
uint32_t TS[MAXN]{};
measurementActive_ = true;
bufferIndex_ = 0;
uint32_t frames = 0;
const uint32_t tStart_us = micros();
const uint32_t captureDuration_us = captureSeconds * 1000000UL;
const DateTime now_start = rtc_.now(); // Czas dla nagłówka
// --- KRYTYCZNA PĘTLA POMIAROWA (ZERO SD) ---
while (measurementActive_) {
if (isEscape())
break;
uint32_t now_us = micros();
if ((now_us - tStart_us) >= captureDuration_us)
break;
// Sprawdzenie czy mamy miejsce w PSRAM na kolejną pełną ramkę
if (bufferIndex_ + frameSize > bufferSize_) {
ESP_LOGW(TAG_CAPTURE, "PSRAM Buffer Full! Stopping.");
break;
}
// Czekamy na dane z sensora
while (!adxl_.availableAll()) {
if (isEscape())
break;
yield(); // Oddaj czas systemowi
}
const uint8_t got = adxl_.readAlignedOnce(X, Y, Z, TS);
if (got != presentCnt)
continue;
// Wspólny timestamp ramki
uint32_t tmin = UINT32_MAX;
for (uint8_t i = 0; i < MAXN; ++i) {
if (!adxl_.isPresent(i))
continue;
if (TS[i] < tmin)
tmin = TS[i];
}
const uint32_t frame_offset_us = tmin - tStart_us;
// Zapis do PSRAM
for (uint8_t i = 0; i < MAXN; ++i) {
if (!adxl_.isPresent(i))
continue;
Sample *dst = reinterpret_cast<Sample *>(buffer_ + bufferIndex_);
dst->offset = frame_offset_us;
dst->sensor_id = i;
dst->x = X[i];
dst->y = Y[i];
dst->z = Z[i];
dst->ready = true;
bufferIndex_ += sizeof(Sample);
}
frames++;
if ((frames & 0x3FF) == 0)
Watchdog::feed(); // Rzadziej, by nie siać jittera
} // --- KONIEC PĘTLI POMIAROWEJ ---
measurementActive_ = false;
ESP_LOGI(TAG_CAPTURE,
"Capture finished. Samples in RAM: %u. Writing to SD...",
(unsigned)frames);
display_.textStatus("Saving to SD...");
// Dopiero teraz otwieramy plik na SD
File dataFile = _fs.open(filename, FILE_WRITE);
if (!dataFile) {
ESP_LOGE(TAG_CAPTURE, "Can't open SD file!");
display_.textStatus("SD Open Error");
return false;
}
// Przygotowanie nagłówka
FileHeader hdr;
memcpy(hdr.magic, "WMT", 3);
hdr.version = 1;
hdr.headerSize = sizeof(FileHeader);
hdr.sampleSize = sizeof(Sample);
hdr.timestamp = now_start.unixtime();
hdr.reccount = frames * presentCnt;
// Zapis nagłówka
dataFile.write(reinterpret_cast<const uint8_t *>(&hdr), sizeof(hdr));
// Zapis całego bufora PSRAM jednym ciągiem
size_t written = dataFile.write(buffer_, bufferIndex_);
if (written == bufferIndex_) {
ESP_LOGI(TAG_CAPTURE, "SD Save Successful: %u bytes", (unsigned)written);
// display_.textStatus("Save OK");
display_.textStatus(basenameFromPath(filename));
delay(2000); // Tutaj zrob to inaczej
} else {
ESP_LOGE(TAG_CAPTURE, "SD Write Error!");
display_.textStatus("SD Write Err");
}
dataFile.close();
printSamplingRate(frames, captureSeconds, filename);
return (written == bufferIndex_);
}
// --- Narzędzia SD i pierdoły ---
String DataCapture::unixToDateTime(uint32_t ts) {
DateTime dt(ts);
char buf[25];
snprintf(buf, sizeof(buf), "%04u-%02u-%02u %02u:%02u:%02u", dt.year(),
dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second());
return String(buf);
}
void DataCapture::readHeaderAndPrint(const char *path) {
Watchdog::feed();
File f = SD.open(path, FILE_READ);
if (!f) {
ESP_LOGE(TAG_CAPTURE, "Error open file %s", path);
return;
}
FileHeader hdr;
size_t rd = f.read((uint8_t *)&hdr, sizeof(hdr));
f.close();
if (rd != sizeof(hdr)) {
ESP_LOGE(TAG_CAPTURE, "Header error");
return;
}
if (memcmp(hdr.magic, "WMT", 3) != 0) {
ESP_LOGE(TAG_CAPTURE, "File signature error");
return;
}
ESP_LOGI(TAG_CAPTURE, "===== WMT file header =====");
ESP_LOGI(TAG_CAPTURE, "Signature: %.3s", hdr.magic);
ESP_LOGI(TAG_CAPTURE, "Version: %u", hdr.version);
ESP_LOGI(TAG_CAPTURE, "Header size:%u B", hdr.headerSize);
ESP_LOGI(TAG_CAPTURE, "Sample size:%u B", hdr.sampleSize);
ESP_LOGI(TAG_CAPTURE, "Timestamp: %d", hdr.timestamp);
ESP_LOGI(TAG_CAPTURE, "Reccount: %u", hdr.reccount);
ESP_LOGI(TAG_CAPTURE, "===========================");
}
void DataCapture::printSamplingRate(uint32_t reccount, uint32_t captureSeconds,
String filename) {
Watchdog::feed();
if (captureSeconds == 0) {
ESP_LOGW(TAG_CAPTURE, "Sampling time = 0!");
return;
}
float fs = (float)reccount / (float)captureSeconds; // Hz (ramki/s)
float fs_kHz = fs / 1000.0f;
ESP_LOGI(TAG_CAPTURE, "Frames: %u", (unsigned)reccount);
ESP_LOGI(TAG_CAPTURE, "Time: %u s", (unsigned)captureSeconds);
ESP_LOGI(TAG_CAPTURE, "Rate: %.3f kHz (frames)", fs_kHz);
display_.displaySampleRateSummary(reccount, captureSeconds, fs_kHz, filename);
display_.textStatus("Summary");
delay(2000);
}
// --- Zarządzanie katalogami/plikiem ---
String DataCapture::allocateNextFilePath() {
Watchdog::feed();
uint32_t highestDir = findHighestNumericDir();
if (highestDir == 0) {
highestDir = 1;
if (!ensureDir(highestDir))
return String();
}
uint32_t count = 0, highestIdx = 0;
scanDirForWmt(highestDir, count, highestIdx);
uint32_t targetDir = highestDir;
uint32_t nextIdx = 0;
if (count > _maxFilesPerDir) {
targetDir = highestDir + 1;
if (!ensureDir(targetDir))
return String();
nextIdx = 1;
} else {
nextIdx = (highestIdx == 0) ? 1 : (highestIdx + 1);
}
return joinPath(dirPath(targetDir), makeIndexedName(nextIdx));
}
String DataCapture::generateNextFilename() {
Watchdog::feed();
uint32_t highestDir = findHighestNumericDir();
if (highestDir == 0)
return String();
uint32_t count = 0, highestIdx = 0;
scanDirForWmt(highestDir, count, highestIdx);
ESP_LOGI(TAG_CAPTURE, "highestDir %u, count %u, highestIdx %u",
(unsigned)highestDir, (unsigned)count, (unsigned)highestIdx);
if (highestIdx == 0)
return String();
return joinPath(dirPath(highestDir), makeIndexedName(highestIdx));
}
FileInfo DataCapture::getLastFileInfo() {
FileInfo info{String(), 0u, false};
Watchdog::feed();
String lastPath = generateNextFilename();
if (lastPath.isEmpty())
return info;
if (_fs.exists(lastPath)) {
File f = _fs.open(lastPath, FILE_READ);
if (f) {
info.exists = true;
info.path = lastPath;
info.size = f.size();
f.close();
}
}
return info;
}
bool DataCapture::deleteAllOnSD() {
Watchdog::feed();
File root = _fs.open(_baseDir);
if (!root || !root.isDirectory())
return false;
for (File e = root.openNextFile(); e; e = root.openNextFile()) {
Watchdog::feed();
String child = String(e.name());
e.close();
if (!recursiveDelete(child))
return false;
}
root.close();
return true;
}
SpaceInfo DataCapture::freeSpaceMB() {
Watchdog::feed();
SpaceInfo si{0.0, "UNKNOWN"};
#if defined(ESP32)
uint64_t total = SD.totalBytes();
uint64_t used = SD.usedBytes();
if (total >= used) {
uint64_t freeB = total - used;
const double oneGB = 1024.0 * 1024.0 * 1024.0;
const double oneMB = 1024.0 * 1024.0;
if (freeB >= (uint64_t)oneGB) {
si.value = (double)freeB / oneGB;
si.unit = "GB";
} else {
si.value = (double)freeB / oneMB;
si.unit = "MB";
}
}
#endif
return si;
}
float DataCapture::freeSpaceFloat(bool *isGB) {
Watchdog::feed();
SpaceInfo si = freeSpaceMB();
if (!si.unit || strcmp(si.unit, "UNKNOWN") == 0) {
if (isGB)
*isGB = false;
return -1.0f;
}
if (isGB)
*isGB = (strcmp(si.unit, "GB") == 0);
return static_cast<float>(si.value);
}
// --- helpery pomocnicze ---
bool DataCapture::isAllDigits(const char *s) {
if (!s || !*s)
return false;
while (*s) {
if (*s < '0' || *s > '9')
return false;
++s;
}
return true;
}
uint32_t DataCapture::toUint(const char *s) {
uint32_t v = 0;
while (*s) {
v = v * 10u + uint32_t(*s - '0');
++s;
}
return v;
}
String DataCapture::dirPath(uint32_t dirNum) const {
return joinPath(_baseDir, String(dirNum));
}
const char *DataCapture::basenameFromPath(const char *full) {
const char *slash = strrchr(full, '/');
return slash ? slash + 1 : full;
}
bool DataCapture::isWmtWithDigits(const char *name, uint32_t &idxOut) const {
size_t nlen = strlen(name), extLen = _ext.length();
if (nlen != (size_t)_digits + extLen)
return false;
if (strncmp(name + _digits, _ext.c_str(), extLen) != 0)
return false;
for (uint8_t i = 0; i < _digits; ++i)
if (name[i] < '0' || name[i] > '9')
return false;
char buf[16];
memcpy(buf, name, _digits);
buf[_digits] = '\0';
idxOut = (uint32_t)strtoul(buf, nullptr, 10);
return true;
}
String DataCapture::makeIndexedName(uint32_t idx) const {
char fmt[8];
snprintf(fmt, sizeof(fmt), "%%0%ulu", (unsigned long)_digits);
char num[24];
snprintf(num, sizeof(num), fmt, (unsigned long)idx);
return String(num) + _ext;
}
String DataCapture::joinPath(const String &a, const String &b) {
if (a.endsWith("/"))
return a + b;
return a + "/" + b;
}
bool DataCapture::ensureDir(uint32_t dirNum) {
String path = dirPath(dirNum);
if (_fs.exists(path)) {
File f = _fs.open(path);
bool ok = f && f.isDirectory();
if (f)
f.close();
return ok;
}
return _fs.mkdir(path);
}
uint32_t DataCapture::findHighestNumericDir() {
File root = _fs.open(_baseDir);
if (!root || !root.isDirectory())
return 0;
uint32_t maxDir = 0;
for (File f = root.openNextFile(); f; f = root.openNextFile()) {
if (f.isDirectory()) {
const char *nm = basenameFromPath(f.name());
if (isAllDigits(nm)) {
uint32_t v = toUint(nm);
if (v > maxDir)
maxDir = v;
}
}
}
return maxDir;
}
void DataCapture::scanDirForWmt(uint32_t dirNum, uint32_t &count,
uint32_t &highestIdx) {
Watchdog::feed();
count = 0;
highestIdx = 0;
String path = dirPath(dirNum);
File dir = _fs.open(path);
if (!dir || !dir.isDirectory())
return;
for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {
if (f.isDirectory()) {
Watchdog::feed();
f.close();
continue;
}
const char *base = basenameFromPath(f.name());
uint32_t idx = 0;
if (isWmtWithDigits(base, idx)) {
count++;
if (idx > highestIdx)
highestIdx = idx;
}
f.close();
}
}
bool DataCapture::recursiveDelete(const String &path) {
File e = _fs.open(path);
if (!e)
return false;
if (!e.isDirectory()) {
e.close();
return _fs.remove(path);
}
// katalog
for (File c = e.openNextFile(); c; c = e.openNextFile()) {
String child = String(c.name());
c.close();
if (!recursiveDelete(child)) {
e.close();
return false;
}
}
e.close();
if (path == _baseDir)
return true; // nie usuwamy katalogu bazowego
return _fs.rmdir(path);
}
void DataCapture::printLastFileInfoSerial() {
Watchdog::feed();
const FileInfo info = getLastFileInfo();
if (!info.exists) {
ESP_LOGE(TAG_CAPTURE, "No .wmt file exists. Last file not exists");
return;
}
const double kb = static_cast<double>(info.size) / 1024.0;
const double mb = kb / 1024.0;
#if defined(ESP32)
// ESP32 wspiera Serial.printf z %llu
ESP_LOGI(TAG_CAPTURE, "Last file: %s", info.path.c_str());
ESP_LOGI(TAG_CAPTURE, "Size: %llu B (%.2f KB, %.2f MB)",
static_cast<unsigned long long>(info.size), kb, mb);
char buf[100];
snprintf(buf, sizeof(buf), "Size:%.2f KB", kb);
display_.textStatus(buf);
#else
// Wariant zachowawczy dla platform bez %llu; pokazujemy rozmiar w MB/KB.
ESP_LOGI(TAG_CAPTURE, "Last file info: "));
ESP_LOGI(TAG_CAPTURE, info.path);
ESP_LOGI(TAG_CAPTURE, "Size: "));
ESP_LOGI(TAG_CAPTURE, mb);
// ESP_LOGI(TAG_CAPTURE, " MB"));
// Serial.print(F(" ("));
// Serial.print(kb, 2);
// Serial.println(F(" KB)"));
#endif
}

View File

@@ -0,0 +1,273 @@
#include <Network.h>
#include <SD.h>
static const char *WIFI = "wifi";
extern ConfigManager configManager;
WiFiManager::WiFiManager() {}
void WiFiManager::begin() {
ESP_LOGI(WIFI, "Start network");
isAccessPoint = config.connect;
if (!isAccessPoint) {
setupAccessPoint(ssidAP.c_str(), passwordAP.c_str());
} else {
ESP_LOGI(WIFI, "WiFi client");
ReadConnection();
connectToWiFi();
}
getRSSI();
setupMDNS();
}
// Konwersja char na IPAddress
bool WiFiManager::convertCharToIPAddress(const char *str, IPAddress& ip) {
uint8_t octets[4];
int parsed = sscanf(str, "%hhu.%hhu.%hhu.%hhu", &octets[0], &octets[1], &octets[2], &octets[3]);
if (parsed == 4) {
ip = IPAddress(octets[0], octets[1], octets[2], octets[3]);
return true;
}
return false;
}
void WiFiManager::ReadConnection() {
ESP_LOGE(WIFI, "Network config error");
isAccessPoint = config.connect;
useDHCP = config.dhcp;
convertCharToIPAddress(config.ip, local_IP);
convertCharToIPAddress(config.gateway, gateway);
convertCharToIPAddress(config.subnet, subnet);
convertCharToIPAddress(config.dns, dns);
}
void WiFiManager::connectToWiFi() {
ESP_LOGI(WIFI, "WiFi STA mode");
WiFi.mode(WIFI_STA);
WiFi.begin(config.ssid, config.password);
if (!useDHCP) {
if (!WiFi.config(local_IP, gateway, subnet, dns)) {
ESP_LOGE(WIFI, "Network static IP failed");
}
}
ESP_LOGI(WIFI, "WiFi connecting to last SSID: %s", config.ssid);
int retries = 0;
while (WiFi.status() != WL_CONNECTED && retries < 20) {
delay(500);
retries++;
updateLED();
}
if (WiFi.status() != WL_CONNECTED) {
ESP_LOGI(WIFI, "Failed. Reading wifi.txt from SD card...");
File f = SD.open("/wifi.txt");
if (f) {
while (f.available()) {
String line = f.readStringUntil('\n');
line.trim();
if (line.isEmpty()) continue;
int sep = line.indexOf(';');
if (sep > 0) {
String s = line.substring(0, sep);
String p = line.substring(sep + 1);
ESP_LOGI(WIFI, "Trying SSID from list: %s", s.c_str());
WiFi.disconnect();
WiFi.begin(s.c_str(), p.c_str());
int tr = 0;
while (WiFi.status() != WL_CONNECTED && tr < 20) {
delay(500);
tr++;
updateLED();
}
if (WiFi.status() == WL_CONNECTED) {
ESP_LOGI(WIFI, "Connected to %s. Saving to config.", s.c_str());
strncpy(config.ssid, s.c_str(), sizeof(config.ssid)-1);
strncpy(config.password, p.c_str(), sizeof(config.password)-1);
configManager.saveConfig();
break;
}
}
}
f.close();
} else {
ESP_LOGW(WIFI, "wifi.txt not found on SD card.");
}
}
if (WiFi.status() == WL_CONNECTED) {
ESP_LOGI(WIFI, "SSID: %s", config.ssid);
String ipString = "IP: " + WiFi.localIP().toString();
ESP_LOGI(WIFI, "%s", ipString.c_str());
String gatewayInfo = "GATEWAY: " + WiFi.gatewayIP().toString();
ESP_LOGI(WIFI, "%s", gatewayInfo.c_str());
String dnsInfo = "DNS: " + WiFi.dnsIP().toString();
ESP_LOGI(WIFI, "%s", dnsInfo.c_str());
} else {
String infoWiFi = "WIFI CONNECTION ERROR: ALL FAILED";
ESP_LOGI(WIFI, "%s", infoWiFi.c_str());
}
updateLED();
}
void WiFiManager::setupAccessPoint(const char *newSSID, const char *newPassword) {
ESP_LOGI(WIFI, "Start AP mode");
// Wyłączenie zapisywania konfiguracji do flash
//WiFi.persistent(false);
// Usunięcie zapisanej konfiguracji trybu stacji
//WiFi.disconnect(true);
//delay(1000);
//WiFi.eraseAP();
//WiFi.enableAP(false);
//WiFi.enableAP(true);
WiFi.mode(WIFI_AP);
WiFi.softAPsetHostname(config.hostname);
WiFi.softAP(newSSID, newPassword);
delay(1000);
String ssi = "AP SSID: " + String(WiFi.softAPSSID());
ESP_LOGI(WIFI, "%s", ssi.c_str());
String sspass = "AP PASS: " + String(newPassword);
ESP_LOGI(WIFI, "%s", sspass.c_str());
IPAddress IP = WiFi.softAPIP();
String ipString = "AP IP: " + IP.toString();
ESP_LOGI(WIFI, "%s", ipString.c_str());
setupMDNS();
getRSSI();
}
bool WiFiManager::isWiFiOK(){
if (WiFi.status() != WL_CONNECTED)
return false;
else
return true;
}
void WiFiManager::checkWiFiConnection() {
if (!isAccessPoint) {
getRSSI();
if (WiFi.status() != WL_CONNECTED) {
String infoWiFi = "WiFi reconnecting: " + String(config.ssid);
ESP_LOGI(WIFI, "%s", infoWiFi.c_str());
WiFi.reconnect();
int retries = 0;
while (WiFi.status() != WL_CONNECTED && retries < 20) {
delay(500);
retries++;
updateLED();
}
if (WiFi.status() == WL_CONNECTED) {
//String infoWiFi = "WiFi connected: " + String(config.ssid);
//ESP_LOGI(WIFI, "%s", infoWiFi.c_str());
//String ipString = "IP: " + WiFi.localIP().toString();
//ESP_LOGI(WIFI, "%s", ipString.c_str());
getRSSI();
setupMDNS();
} else {
String infoWiFi = "WiFi reconnect error: " + String(config.ssid);
ESP_LOGI(WIFI, "%s", infoWiFi.c_str());
getRSSI();
}
}
updateLED();
}
}
int WiFiManager::rssiToPercent(int rssi) {
if (rssi <= -100) {
return 0;
} else
if (rssi >= -50) {
return 100;
}
// Dla wartości pomiędzy -100 a -50 dBm stosujemy prostą liniową skalę
else {
return 2 * (rssi + 100); // Przykładowa liniowa zależność
}
}
void WiFiManager::updateLED() {
getRSSI();
if (WiFi.status() == WL_CONNECTED) {
int8_t rssi = WiFi.RSSI();
if (rssi > -70) {
;
//digitalWrite(LED_PIN, HIGH); // Silny sygnał
} else {
String signalInfo = "WIFI WEAK SIGNAL: " + String(rssi) + " " + rssiToPercent(rssi) + "%";
ESP_LOGW(WIFI, "%s", signalInfo.c_str());
}
}
}
int8_t WiFiManager::getRSSI() {
if (WiFi.status() == WL_CONNECTED) {
rssi = WiFi.RSSI();
return rssi;
}
return 0;
}
void WiFiManager::setupMDNS() {
ESP_LOGI(WIFI, "mDNS start");
if (!MDNS.begin(config.hostname)) {
ESP_LOGE(WIFI, "mDNS error");
} else {
String mdnsstr = "MDNS: http://" + String(config.hostname) + ".local";
ESP_LOGI(WIFI, "%s", mdnsstr.c_str());
}
}
bool WiFiManager::performOTAUpdate(bool allowInsecureTLS, std::function<void(int,int)> progressCb){
// 1) Pobierz i sprawdź URL
String url = String(config.updateUrl); // z Config.h globalny 'config'
url.trim();
if (url.isEmpty()) {
ESP_LOGE(WIFI, "[OTA] Pusty config.updateUrl przerwano.");
return false;
}
ESP_LOGI(WIFI, "[OTA] URL: %s", url.c_str());
// 2) Wymagamy aktywnego Wi-Fi w trybie klienta
if (WiFi.status() != WL_CONNECTED) {
ESP_LOGE(WIFI, "[OTA] Brak połączenia Wi-Fi przerwano.");
return false;
}
// 3) Callback postępu (opcjonalny)
if (progressCb) {
httpUpdate.onProgress([&](int cur, int total){ progressCb(cur, total); });
}
// 4) Konfiguracja klienta i wywołanie aktualizacji
httpUpdate.rebootOnUpdate(true); // po sukcesie reboot
t_httpUpdate_return ret;
if (url.startsWith("https://")) {
WiFiClientSecure client;
if (allowInsecureTLS) {
client.setInsecure(); // UWAGA: testy/dev; w produkcji lepiej setCACert(...)
}
client.setTimeout(15000);
ret = httpUpdate.update(client, url); // HTTPS
} else {
WiFiClient client;
client.setTimeout(15000);
ret = httpUpdate.update(client, url); // HTTP
}
// 5) Obsługa rezultatów
switch (ret) {
case HTTP_UPDATE_OK:
ESP_LOGI(WIFI, "[OTA] Sukces restart nastąpi za chwilę.");
return true; // reboot i tak zaraz nastąpi
case HTTP_UPDATE_NO_UPDATES:
ESP_LOGW(WIFI, "[OTA] Brak nowej wersji (304/Not Modified).");
return false;
case HTTP_UPDATE_FAILED:
default:
ESP_LOGE(WIFI, "[OTA] Błąd (%d): %s", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str());
return false;
}
}

View File

@@ -0,0 +1,172 @@
#include <Settings.h>
// Lokalny tag logów
static const char *TAG_SETTINGS = "SETTINGS";
Settings::Settings(Display &display, RTC_DS3231 &rtc): display_(display), rtc_(rtc){}
Settings::~Settings() {}
void Settings::begin() {
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_OK, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
}
// Zwraca true, jeśli przycisk jest wciśnięty
bool Settings::isPressed(uint8_t btnIndex) {
//ESP_LOGI(TAG_SETTINGS, "BTN check");
switch (btnIndex) {
case 1: return digitalRead(BTN_UP) == LOW;
case 2: return digitalRead(BTN_OK) == LOW;
case 3: return digitalRead(BTN_DOWN) == LOW;
default: return false;
}
}
// Kombinacja 1 i 3 jednocześnie
bool Settings::isBtnReset() {
ESP_LOGI(TAG_SETTINGS, "BTN reset check");
return (digitalRead(BTN_UP) == LOW && digitalRead(BTN_DOWN) == LOW);
}
bool Settings::isSetClock() {
ESP_LOGI(TAG_SETTINGS, "BTN set clock");
return (digitalRead(BTN_OK) == LOW);
}
/*
Ustawienie opcji
*/
void Settings::setConfigDevice() {
ESP_LOGI(TAG_SETTINGS, "Set config device");
display_.clear();
delay(300);
config.duration = editField(config.duration, 1, 20, "Test duration");
uint16_t dur = config.pause/1000;
dur = editField(dur, 1, 20, "Pause between");
config.pause = dur * 1000;
config.measure = editField(config.measure, 0, 1, "Autorun test");
ESP_LOGI(TAG_SETTINGS, "Config finish");
}
void Settings::finishConfigDevice(){
display_.clear();
display_.textCenter(1, "CONFIG UPDATED");
display_.textCenter(2, "RESTARTING");
delay(1000);
display_.clear();
ESP.restart();
}
/* --------------------------------------------------------------------
* Ustawianie czasu RTC:
* Kolejność: rok -> miesiąc -> dzień -> godzina -> minuta -> sekunda
* ------------------------------------------------------------------*/
void Settings::setTimeRTC() {
DateTime now = rtc_.now();
RtcDateTime t;
t.year = now.year();
t.month = now.month();
t.day = now.day();
t.hour = now.hour();
t.minute = now.minute();
t.second = now.second();
ESP_LOGI(TAG_SETTINGS,
"Start RTC edit: %04u-%02u-%02u %02u:%02u:%02u",
t.year, t.month, t.day, t.hour, t.minute, t.second);
display_.clear();
delay(300);
// Kolejne pola
t.year = editField(t.year, 2024, 2099, "YEAR");
t.month = editField(t.month, 1, 12, "MONTH");
// uproszczenie: 131 bez sprawdzania długości miesiąca
t.day = editField(t.day, 1, 31, "DAY");
t.hour = editField(t.hour, 0, 23, "HOUR");
t.minute = editField(t.minute, 0, 59, "MINUTE");
t.second = editField(t.second, 0, 59, "SECOND");
// Zapis do RTC
rtc_.adjust(DateTime(t.year, t.month, t.day, t.hour, t.minute, t.second));
ESP_LOGI(TAG_SETTINGS,
"RTC set to: %04u-%02u-%02u %02u:%02u:%02u",
t.year, t.month, t.day, t.hour, t.minute, t.second);
display_.clear();
display_.textCenter(1, "RTC UPDATED");
display_.textCenter(2, "RESTARTING");
//display_.textCenter(2, "OK TO CONTINUE");
delay(1000);
display_.clear();
ESP.restart();
}
// Edycja jednej wartości (rok/miesiąc/dzień/godz/min/sek)
int Settings::editField(int value, int minVal, int maxVal, const char *label) {
bool lastUp = false;
bool lastDown = false;
bool lastOk = false;
constrainValue(value, minVal, maxVal);
printField(label, value);
while (true) {
bool up = readBtnUp();
bool ok = readBtnOk();
bool down = readBtnDown();
// Zmiana przy puszczeniu/wciśnięciu (zbocze narastające)
if (up && !lastUp) {
value++;
if (value > maxVal) value = minVal;
printField(label, value);
}
if (down && !lastDown) {
value--;
if (value < minVal) value = maxVal;
printField(label, value);
}
if (ok && !lastOk) {
ESP_LOGI(TAG_SETTINGS, "[OK] %s = %d", label, value);
// mały debounce na wyjście z pola
delay(200);
return value;
}
lastUp = up;
lastDown = down;
lastOk = ok;
delay(80); //debouncing
}
}
void Settings::constrainValue(int &value, int minVal, int maxVal) {
if (value < minVal) value = minVal;
if (value > maxVal) value = maxVal;
}
// Wyświetlanie aktualnie edytowanego pola na LCD
void Settings::printField(const char *label, int value) {
char buf[21];
display_.clear();
// Linia 0: nazwa pola
snprintf(buf, sizeof(buf), "%s:", label);
display_.text(0, buf);
// Linia 1: wartość
snprintf(buf, sizeof(buf), "%d", value);
display_.text(1, buf);
// Linia 3: podpowiedź
display_.textStatus("UP/DOWN, OK=Next");
ESP_LOGI(TAG_SETTINGS, "%s = %d", label, value);
}

View File

@@ -0,0 +1,48 @@
#include <Tool.h>
static const char *TOOL = "tool";
bool isI2CDevPresent(uint8_t address) {
Wire.beginTransmission(address);
uint8_t error = Wire.endTransmission();
if (error == 0) {
ESP_LOGI(TOOL, "I2C response from 0x%02X", address);
return true;
} else {
if (error == 4) {
ESP_LOGW(TOOL, "I2C unknown error at 0x%02X", address);
} else {
ESP_LOGI(TOOL, "No I2C device at 0x%02X", address);
}
return false;
}
}
void scanI2C() {
byte error, address;
int nDevices = 0;
ESP_LOGI(TOOL, "I2C start scan");
for (address = 1; address < 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
char buf[32];
snprintf(buf, sizeof(buf), "I2C device: 0x%02X", address);
ESP_LOGI(TOOL, "%s", buf);
nDevices++;
Watchdog::feed();
}
else if (error == 4) {
ESP_LOGW(TOOL, "I2C error at address: 0x%02X", address);
}
}
if (nDevices == 0) {
ESP_LOGE(TOOL, "I2C no devices found");
} else {
ESP_LOGI(TOOL, "I2C scan finished. Found %d device(s)", nDevices);
}
Watchdog::feed();
}

View File

@@ -0,0 +1,117 @@
#include "UploadManager.h"
#include "Watchdog.h"
static const char* TAG_UPLOAD = "UPLOAD";
static const char* LOG_FILE = "/uploaded.csv";
UploadManager::UploadManager(APIClient& client, RTC_DS3231& rtc, DataCapture& capture)
: apiClient(client), rtc_(rtc), capture_(capture) {}
String UploadManager::getCurrentTimestamp() {
DateTime now = rtc_.now();
char buf[25];
snprintf(buf, sizeof(buf), "%04u-%02u-%02u %02u:%02u:%02u",
now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
return String(buf);
}
void UploadManager::appendLog(const String& filePath, const String& status) {
File f = SD.open(LOG_FILE, FILE_APPEND);
if (f) {
f.printf("%s,%s,%s\n", getCurrentTimestamp().c_str(), filePath.c_str(), status.c_str());
f.close();
}
}
bool UploadManager::isAlreadyUploaded(const String& filePath) {
File f = SD.open(LOG_FILE, FILE_READ);
if (!f) return false;
bool found = false;
while (f.available()) {
String line = f.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
int firstComma = line.indexOf(',');
if (firstComma < 0) continue;
int secondComma = line.indexOf(',', firstComma + 1);
if (secondComma < 0) continue;
String logPath = line.substring(firstComma + 1, secondComma);
String logStatus = line.substring(secondComma + 1);
if (logPath == filePath) {
if (logStatus == "OK") found = true;
else found = false; // A retry might be needed if last status wasn't OK
}
Watchdog::feed();
}
f.close();
return found;
}
void UploadManager::uploadFile(const String& filePath) {
if (isAlreadyUploaded(filePath)) {
ESP_LOGI(TAG_UPLOAD, "File %s is already uploaded.", filePath.c_str());
return;
}
if (WiFi.status() != WL_CONNECTED) {
ESP_LOGE(TAG_UPLOAD, "No WiFi. Cannot upload %s", filePath.c_str());
appendLog(filePath, "ERROR: No WiFi");
return;
}
bool success = apiClient.uploadMeasurement(filePath);
if (success) {
appendLog(filePath, "OK");
} else {
if (WiFi.status() != WL_CONNECTED) {
appendLog(filePath, "ERROR: WiFi lost during upload");
} else {
appendLog(filePath, "ERROR: Upload Failed");
}
}
}
void UploadManager::processPendingUploads() {
if (WiFi.status() != WL_CONNECTED) return;
ESP_LOGI(TAG_UPLOAD, "Checking for pending uploads...");
uint32_t highestDir = capture_.findHighestNumericDir();
if (highestDir == 0) return;
for (uint32_t d = 1; d <= highestDir; d++) {
String path = capture_.dirPath(d);
File dir = SD.open(path);
if (!dir || !dir.isDirectory()) {
if(dir) dir.close();
continue;
}
for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {
if (f.isDirectory()) { f.close(); continue; }
String childPath = String(f.name());
if (!childPath.startsWith("/")) {
childPath = path + "/" + childPath;
}
if (childPath.endsWith(".wmt")) {
if (!isAlreadyUploaded(childPath)) {
ESP_LOGI(TAG_UPLOAD, "Found pending file: %s", childPath.c_str());
uploadFile(childPath);
delay(1000);
}
}
f.close();
Watchdog::feed();
if (WiFi.status() != WL_CONNECTED) {
dir.close();
return;
}
}
dir.close();
}
ESP_LOGI(TAG_UPLOAD, "Pending uploads check complete.");
}

View File

@@ -0,0 +1,81 @@
#include "Watchdog.h"
// Wykrywanie środowiska
#if defined(ESP_PLATFORM) || defined(ESP32)
#include <esp_task_wdt.h>
#define WDOG_HAS_ESP 1
#else
#define WDOG_HAS_ESP 0
#endif
namespace Watchdog {
static bool s_initialized = false;
bool init(int timeout_seconds, bool panic_on_trigger) {
#if WDOG_HAS_ESP
if (s_initialized) return true;
esp_err_t err = esp_task_wdt_init(timeout_seconds, panic_on_trigger);
if (err == ESP_OK || err == ESP_ERR_INVALID_STATE) {
// ESP_ERR_INVALID_STATE: już zainicjalizowany — traktujemy jako OK
s_initialized = true;
return true;
}
return false;
#else
(void)timeout_seconds; (void)panic_on_trigger;
s_initialized = true; // no-op, aby nie blokować wywołań w kodzie
return true;
#endif
}
bool addThisTask() {
#if WDOG_HAS_ESP
if (!s_initialized) return false;
esp_err_t err = esp_task_wdt_add(nullptr); // nullptr = bieżący task
return (err == ESP_OK || err == ESP_ERR_INVALID_STATE);
#else
return true; // no-op
#endif
}
bool removeThisTask() {
#if WDOG_HAS_ESP
if (!s_initialized) return false;
esp_err_t err = esp_task_wdt_delete(nullptr); // bieżący task
return (err == ESP_OK || err == ESP_ERR_INVALID_STATE);
#else
return true; // no-op
#endif
}
void feed() {
#if WDOG_HAS_ESP
esp_task_wdt_reset();
#else
// no-op
#endif
}
bool setTimeout(int timeout_seconds) {
#if WDOG_HAS_ESP
if (!s_initialized) {
// jeśli ktoś nie zainicjalizował — zrób to teraz
return init(timeout_seconds, true);
}
// W ESP-IDF/Arduino brak prostego API na „live update” — re-init:
esp_err_t err = esp_task_wdt_deinit();
(void)err; // nie każdy port raportuje OK/INVALID_STATE spójnie
s_initialized = false;
return init(timeout_seconds, true);
#else
(void)timeout_seconds;
return true; // no-op
#endif
}
bool isActive() {
return s_initialized;
}
} // namespace Watchdog

View File

@@ -0,0 +1,500 @@
#include <Logger.h>
#include "Watchdog.h"
#include <Arduino.h>
#include <Config.h>
#include <Pinout.h>
#include <Version.h>
#include <Display.h>
#include <SPI.h>
#include <SD.h>
#include "ADXL345FastSPI.h"
#include "RTClib.h"
#include <Wire.h>
#include <Network.h>
#include <Thread.h>
#include <Measure.h>
#include <Tool.h>
#include <Settings.h>
#include "APIClient.h"
#include "UploadManager.h"
#include <time.h>
#define WDT_TIMEOUT 60 // Czas watchdoga do restartu
#define MAX_ADXL345_SENSORS 4 // Maksymalna ilość podłączanych sensorów
SPIClass SPI_ADXL(FSPI); // SPI2 (VSPI)
SPIClass SPI_SD(HSPI); // SPI3 (HSPI)
float x, y, z = 0; // Dane odczytane z akcelerometru
String name;
bool isRebootRequired = false;
bool isAccelExists = false; // Czy istnieje jakiś podłączony do SPI czujnik
bool testingNow = false; // Czy trwa test (gdy tak, trzeba wszystko inne wyłączyć)
bool runMeasure = false; // czy włączyć pomiar?
// Buttony: 5, 6, 7
// piny SPI CS dla ASXL345
const uint8_t csPins[MAX_ADXL345_SENSORS] = {9, 10, 14, 21};
long licznik = 0;
ConfigManager configManager;
RTC_DS3231 rtc;
ADXL345FastSPI adxl(csPins, MAX_ADXL345_SENSORS);
Display display(rtc, 0x27, 20, 4);
Settings settings(display, rtc); // obsługa przycisków
WiFiManager wifi;
DataCapture capture(adxl, display, rtc, SD, 8192); // NEW !!! MEASURE!!!
APIClient apiClient;
UploadManager uploadManager(apiClient, rtc, capture);
Thread wifiTestThread = Thread(); // Cykliczny test WiFi
Thread offlineThread = Thread(); // Jeśli offline
Thread measureThread = Thread(); // Pomiar i zapis na SD
Thread uploadThread = Thread(); // Background upload
//////// PROTOTYPY /////////////////
void setup();
void loop();
void reboot();
void resetBtnClick();
void checkWiFi();
void showOfflineScreen();
void measure();
void setClockRTCBtn();
/* ******************* SETUP() ************************* */
void setup() {
Serial.begin(115200);
delay(500);
// przy USB-CDC warto poczekać chwilę na enumerację (z timeoutem):
unsigned long t0 = millis();
while (!Serial && millis()-t0 < 2000) { delay(10); }
settings.begin();
esp_log_level_set("*", ESP_LOG_INFO); // _ERROR, _WARN, _INFO, _DEBUG, _VERBOSE
delay(500); // było 1000
ESP_LOGI(TAG_MAIN, "----------------------");
ESP_LOGI(TAG_MAIN, "WMT Stalowa Wola A.Chmielowiec & L.Klich");
ESP_LOGI(TAG_MAIN, "Rejestrator parametrow");
ESP_LOGI(TAG_MAIN, "Firmware: %s", VERSION);
ESP_LOGI(TAG_MAIN, "----------------------");
ESP_LOGI(TAG_MAIN, "ESP32 model: %s Rev %d", ESP.getChipModel(), ESP.getChipRevision());
ESP_LOGI(TAG_MAIN, "Chip cores: %d", ESP.getChipCores());
// Inicjalizacja Watchdoga na 5 sek, panic_on_trigger = true
if (Watchdog::init(25, true)) {
ESP_LOGI(TAG_MAIN, "Watchdog init ok.");
} else {
ESP_LOGE(TAG_MAIN, "Watchdog init error.");
}
Wire.begin(PIN_SDA, PIN_SCL, 100000);
scanI2C(); // Skanowanie magistrali I2C na UART (RTC-0x68, LCD-0x27)
configManager.begin(); // konfiguracja EEPROM urządzenia
//configManager.showConfig();
// Test LCD I2C
if (!isI2CDevPresent(0x27)) {
ESP_LOGE(TAG_MAIN, "LCD 0x27 wire error!");
}
ESP_LOGI(TAG_MAIN, "Display init");
if (!display.begin()) {
ESP_LOGE(TAG_MAIN, "Display init failed");
while (true) delay(1000);
}
ESP_LOGI(TAG_MAIN, "Display OK");
display.welcomeScreen();
delay(1000);
// Przycisk reset w przypadku factory reset
if(settings.isBtnReset()) resetBtnClick();
// MCU Info
ESP_LOGI(TAG_MAIN, "MCU info");
display.textStatus(ESP.getChipModel());
delay(500);
char buf[20];
sprintf(buf, "Freq: %d MHz", ESP.getCpuFreqMHz());
display.textStatus(buf);
delay(500);
// Test PSRAM
display.textStatus("PSRAM:");
ESP_LOGI(TAG_MAIN, "PSRAM init");
if (!psramFound()) {
ESP_LOGE(TAG_MAIN, "PSRAM not found");
display.print("FAILED");
while (true) delay(1000);
}
display.print("OK");
delay(500);
// Test RTC
ESP_LOGI(TAG_MAIN, "RTC test");
if (!isI2CDevPresent(0x68)) {
ESP_LOGE(TAG_MAIN, "RTC 0x68 wire error!");
display.textStatus("RTC wire error!");
while (true) delay(1000);
}
display.textStatus("RTC:");
ESP_LOGI(TAG_MAIN, "RTC init");
if (!rtc.begin()) {
display.print("FAILED");
ESP_LOGE(TAG_MAIN, "Can't find RTC");
while (true) delay(1000);
} else {
display.print("OK");
ESP_LOGI(TAG_MAIN, "RTC OK");
if (rtc.lostPower()) {
ESP_LOGE(TAG_MAIN, "RTC power lost! Set clock");
display.textStatus("RTC: SET TIME");
delay(1000);
settings.setTimeRTC();
}
}
// Przycisk ustawianai czasu: przytrzymaj OK przy starcie systemu
if(settings.isSetClock()) setClockRTCBtn();
delay(500);
// Karta SD
ESP_LOGI(TAG_MAIN, "SD Card init");
ESP_LOGI(TAG_MAIN, "SPI2 SD SCK: %d, MISO: %d, MOSI: %d, CS: %d", SD_SCK, SD_MISO, SD_MOSI, SD_CS);
SPI_SD.begin(SD_SCK, SD_MISO, SD_MOSI);
display.textStatus("SD CARD:");
while(!SD.begin(SD_CS, SPI_SD, 4000000)) { // 10 MHz na start; w razie czego 4 MHz (bez tego często SD Failed)
ESP_LOGE(TAG_MAIN, "SD mount failed");
display.print("FAILED");
delay(4000);
display.textStatus("SD CARD:");
delay(500);
}
ESP_LOGI(TAG_MAIN, "SD Card OK");
display.print("OK");
ESP_LOGI(TAG_MAIN, "ADXL345 SPI3 SCK: %d, MISO: %d, MOSI: %d", CLK_ADSX, MISO_ADSX, MOSI_ADSX);
SPI_ADXL.begin(CLK_ADSX, MISO_ADSX, MOSI_ADSX);
// Inicjalizacja ADXL345
ESP_LOGI(TAG_MAIN, "ADXL345 init");
display.textStatus("ADXL345: ");
//if (!adxl.begin(&SPI, 5000000, ADXL345FastSPI::RATE_3200HZ, ADXL345FastSPI::RANGE_16G, 1)) {
if(!adxl.begin(&SPI_ADXL, 5000000, ADXL345FastSPI::RATE_3200HZ, ADXL345FastSPI::RANGE_16G, 1)){
ESP_LOGE(TAG_MAIN, "ADXL345 Error");
display.print("FAILED");
isAccelExists = false;
uint8_t counter = 0;
while(counter<5){ counter++; delay(1000); }
display.clear();
display.textCenter(0, "PLEASE CONNECT");
display.textCenter(1, "SENSOR MODULE");
display.textCenter(2, "ANY KEY RESTART");
display.textCenter(3, "GURU MEDITATION");
while(true){
if(digitalRead(BTN_UP) == LOW) reboot();
if(digitalRead(BTN_OK) == LOW) reboot();
if(digitalRead(BTN_DOWN) == LOW) reboot();
}
} else {
display.print(String(adxl.size()));
ESP_LOGI(TAG_MAIN, "ADXL345 OK: %d COUNT", adxl.size()); // liczba wykrytych
isAccelExists = true;
}
DateTime now = rtc.now();
ESP_LOGI(TAG_MAIN, "RTC TIME: %d:%d:%d %d:%d:%d", now.day(), now.month(), now.year(), now.hour(), now.minute(), now.second());
delay(1000);
if(config.connect){
wifi.begin();
// ---- Synchronizacja RTC z NTP po połączeniu WiFi ----
if (WiFi.status() == WL_CONNECTED) {
ESP_LOGI(TAG_MAIN, "NTP sync: connecting to %s", config.ntp);
// Strefa czasowa Warszawa: CET (UTC+1) z automatycznym przełączaniem na CEST (UTC+2)
configTzTime("CET-1CEST,M3.5.0/2,M10.5.0/3", config.ntp);
display.textStatus("NTP SYNC...");
// Czekamy na synchronizację (max 5s) w sposób nieblokujący dla Watchdoga
unsigned long startSync = millis();
while (millis() - startSync < 5000) {
Watchdog::feed();
yield();
struct tm timeinfo;
if (getLocalTime(&timeinfo, 100)) { // krótki timeout na sprawdzenie
break;
}
delay(100);
}
struct tm timeinfo;
if (getLocalTime(&timeinfo, 500)) { // ostateczne sprawdzenie
DateTime ntpTime(timeinfo.tm_year + 1900, timeinfo.tm_mon + 1,
timeinfo.tm_mday, timeinfo.tm_hour,
timeinfo.tm_min, timeinfo.tm_sec);
DateTime rtcNow = rtc.now();
int32_t diff = (int32_t)ntpTime.unixtime() - (int32_t)rtcNow.unixtime();
ESP_LOGI(TAG_MAIN, "NTP time: %04d-%02d-%02d %02d:%02d:%02d",
ntpTime.year(), ntpTime.month(), ntpTime.day(),
ntpTime.hour(), ntpTime.minute(), ntpTime.second());
ESP_LOGI(TAG_MAIN, "RTC time: %04d-%02d-%02d %02d:%02d:%02d",
rtcNow.year(), rtcNow.month(), rtcNow.day(),
rtcNow.hour(), rtcNow.minute(), rtcNow.second());
ESP_LOGI(TAG_MAIN, "RTC vs NTP difference: %d seconds", abs(diff));
if (abs(diff) > 5) {
rtc.adjust(ntpTime);
ESP_LOGI(TAG_MAIN, "RTC UPDATED from NTP (drift was %d sec)", abs(diff));
display.textStatus("RTC SYNCED");
} else {
ESP_LOGI(TAG_MAIN, "RTC already in sync (diff %d sec)", abs(diff));
display.textStatus("RTC OK");
}
} else {
ESP_LOGE(TAG_MAIN, "NTP: Failed to obtain time from %s", config.ntp);
display.textStatus("NTP FAILED");
}
delay(1000);
}
// ---- Koniec synchronizacji NTP ----
wifiTestThread.onRun(checkWiFi);
wifiTestThread.setInterval(5000); // Test WiFi co 3 sekundy
} else {
offlineThread.onRun(showOfflineScreen);
offlineThread.setInterval(1000); // Jeśli offline, to wyświetl ekran co 1 sek
}
measureThread.onRun(measure);
measureThread.setInterval(config.pause); // Test co X sekund
if(config.connect){
uploadThread.onRun([]() { uploadManager.processPendingUploads(); });
uploadThread.setInterval(60000); // Co 1 minutę sprawdzaj zaległe
}
// Dodanie taska loop do WDT
if (Watchdog::addThisTask()) {
ESP_LOGI(TAG_MAIN, "Added main task to Watchdog");
}
ESP_LOGI(TAG_MAIN, "System ready");
display.clear();
}
// Factory reset
void setClockRTCBtn(){
ESP_LOGI(TAG_MAIN, "SET DATE TIME");
while(settings.isSetClock()){
display.clear();
display.textCenter(0, " PLEASE ");
display.textCenter(1, "release key");
display.textCenter(3, "TO SET DATE TIME");
delay(500);
}
settings.setTimeRTC();
}
// Factory reset
void resetBtnClick(){
uint8_t counter = 10; // ile sekund trzymać przycisk na factory reset
ESP_LOGI(TAG_MAIN, "RESET BTN to 10 sec.");
while(settings.isBtnReset()){
Serial.print(counter); Serial.print(",");
counter--;
delay(1000);
display.clear();
display.textCenter(0,"WARNING!!!");
display.textCenter(1, "Factory reset");
display.textCenter(2, "keep holding for");
display.textStatus(String(counter).c_str());
if (counter <= 1) {
display.clear();
display.textCenter(0, "RESET CONFIG");
display.textCenter(1, "release key");
ESP_LOGI(TAG_MAIN, "RESET CONFIG");
while(settings.isBtnReset()){;}
configManager.resetToDefaults();
display.textStatus("RESTARTING!");
delay(500);
isRebootRequired = true;
} else {
isRebootRequired = true;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
void showError(String err){
Watchdog::feed();
display.clear();
display.println(err);
ESP_LOGE(TAG_MAIN, "%s", err.c_str());
delay(3000);
}
void checkWiFi(){
Watchdog::feed();
bool isConnected = WiFi.isConnected();
String ip = WiFi.localIP().toString();
wifi.checkWiFiConnection();
display.updateBarWiFi(WiFi.RSSI(), isConnected);
display.updateNetwork(ip, isConnected);
}
////// Ekran główny tylko gdy brak pomiaru /////////////
void showOfflineScreen(){
if((!config.connect) && (!testingNow)) {
bool isGB;
Watchdog::feed();
float freeSpace = capture.freeSpaceFloat(&isGB);
display.displayOffline(testingNow, adxl.connectedSensorsCount(), capture.freeSpaceFloat(), licznik, false);
}
}
void reboot() {
display.clear();
display.textCenter(1, "SYSTEM");
display.textCenter(2, "RESTARTING");
#if defined(ARDUINO_RASPBERRY_PI_PICO)
watchdog_enable(1, 1); // 1 ms timeout, restart po upływie
while (true); // czekaj na watchdog reset
#endif
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
ESP_LOGI(TAG_MAIN, "RESTART");
ESP.restart(); // restart ESP
#endif
#if defined(ARDUINO_ARCH_STM32)
NVIC_SystemReset(); // restartuj STM32
#endif
}
// pomiar z Thread START POMIARU TUTAJ
void measure(){
Watchdog::feed();
testingNow = true;
offlineThread.enabled = false;
DateTime now = rtc.now();
ESP_LOGI(TAG_MAIN, "RTC TIME: %d:%d:%d %d:%d:%d", now.day(), now.month(), now.year(), now.hour(), now.minute(), now.second());
char bdate[15]; char btime[15];
snprintf(bdate, sizeof(bdate), "%04d-%02d-%02d", now.year(), now.month(), now.day());
snprintf(btime, sizeof(btime), "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
display.initMeasure(config.measure, testingNow, runMeasure, config.pause, config.duration, config.connect, licznik, bdate, btime);
ESP_LOGI(TAG_MAIN, "MEASURE RUNNING");
//delay(1000);
capture.captureAuto(config.duration, "/");
testingNow = false;
Watchdog::feed();
if(!capture.isExit){
capture.printLastFileInfoSerial();
capture.readHeaderAndPrint(capture.generateNextFilename().c_str());
ESP_LOGI(TAG_MAIN, "MEASURE FINISH");
} else {
ESP_LOGI(TAG_MAIN, "MEASURE INTERRUPT");
//runMeasure = false;
}
if (config.connect && WiFi.status() == WL_CONNECTED) {
ESP_LOGI(TAG_MAIN, "TRIGGER UPLOAD PENDING...");
uploadManager.processPendingUploads();
}
runMeasure = false;
ESP_LOGI(TAG_MAIN, "DISPLAY Offline");
display.displayOffline(testingNow, adxl.connectedSensorsCount(), capture.freeSpaceFloat(), licznik, true);
offlineThread.enabled = true;
}
void toogleMode(){ // tryb ciągłego, pojedynczego pomiaru
config.measure = !config.measure;
configManager.saveConfig();
display.textStatus("Mode changed");
delay(500);
display.textStatus("");
}
void settingsDevice(){
settings.setConfigDevice();
configManager.saveConfig();
settings.finishConfigDevice();
}
///////////// LOOP ////////////////////////////////////////////
void loop() {
if (settings.isPressed(2)){
Watchdog::feed();
if(runMeasure) {
capture.isExit = true;
runMeasure = false;
display.textStatus("Stopped");
delay(3000);
} else runMeasure = true;
if(runMeasure) ESP_LOGI(TAG_MAIN, "BTN MEASURE: START"); else ESP_LOGI(TAG_MAIN, "BTN MEASURE: STOP");
Watchdog::feed();
delay(300);
if (runMeasure) measureThread.run();
}
if(settings.isPressed(3)) settingsDevice(); // DOWN
if(settings.isPressed(1)) toogleMode(); // UP
if(testingNow) {
if((runMeasure) && (settings.isPressed(2))){
runMeasure = false;
display.textStatus("STOPPING. WAIT");
}
return; // jeśli trwa akurat test, nic nie rób
}
if(wifiTestThread.shouldRun() && (config.connect))
wifiTestThread.run();
if(uploadThread.shouldRun() && config.connect)
uploadThread.run();
if(offlineThread.shouldRun() && (!config.connect)){
offlineThread.run();
}
if(!isAccelExists) {
ESP_LOGE(TAG_MAIN, "ADXL module error:halt");
display.clear();
delay(500);
display.textCenter(0, "SENSORS");
display.textCenter(1, "NOT EXISTS");
display.textCenter(2, "SYSTEM STOPPED");
Watchdog::feed();
delay(2000);
} else {
if(measureThread.shouldRun() && (config.measure) && (!runMeasure)){
Watchdog::feed();
ESP_LOGI(TAG_MAIN, "Measure thread run");
measureThread.run();
}
}
if (isRebootRequired) {
ESP_LOGI(TAG_MAIN, "Reboot required");
Watchdog::feed();
delay(1000);
reboot();
}
Watchdog::feed();
delay(20); // 100
licznik ++;
}