10 Commits

38 changed files with 3723 additions and 39 deletions

View File

@@ -59,7 +59,6 @@ public:
void readHeaderAndPrint(const char *path);
void stop();
bool isActive() const { return measurementActive_; }
String getCurrentCapturePath() const { return currentCapturePath_; }
// SD utils
SpaceInfo freeSpaceMB();
@@ -89,7 +88,6 @@ private:
size_t bufferSize_ = 0;
size_t bufferIndex_ = 0;
bool measurementActive_ = false;
String currentCapturePath_ = "";
bool flushToFile(File &f);
static uint8_t crc8(const uint8_t *data, size_t len);

View File

@@ -9,9 +9,11 @@
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <DNSServer.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <DNSServer.h>
#endif
//#include <Pinout.h>
//#include <IPAddress.h>
@@ -42,6 +44,12 @@ class WiFiManager {
int rssiToPercent(int rssi); // rssi na procenty
int8_t getRSSI();
void handleClient();
void startCaptivePortal();
void handleRoot();
void handleSave();
void handleNotFound();
/**
* Aktualizacja systemu przez internet z adresu config.updateUrl.
* @param allowInsecureTLS true => dla https wyłącz weryfikację certyfikatu.
@@ -61,6 +69,10 @@ class WiFiManager {
private:
bool isAccessPoint = false;
bool captivePortalActive = false;
WebServer server{80};
DNSServer dnsServer;
int expectedCaptchaAnswer = 0;
};
#endif // WIFIMANAGER_H

24
include/Uploader.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef UPLOADER_H
#define UPLOADER_H
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <SD.h>
#include "Config.h"
#include "Display.h"
#include "Watchdog.h"
class Uploader {
public:
Uploader(Display &display);
void processQueue(int maxFiles = 3);
private:
Display &_display;
String loadCACert(const char* path);
bool sendFile(String filePath, String& caCert);
};
#endif

View File

@@ -0,0 +1,79 @@
#pragma once
#include <Arduino.h>
#include <SPI.h>
#include "ADXL345FreshSPI.h"
class ADXL345ArraySPI {
public:
static constexpr uint8_t MAX_SENSORS = 4;
explicit ADXL345ArraySPI(const uint8_t cs_pins[MAX_SENSORS]) {
for (uint8_t i=0;i<MAX_SENSORS;i++){ cs_[i]=cs_pins[i]; }
}
// Inicjalizacja wszystkich (SPI MODE3, STREAM FIFO, full-res, wybrany ODR/Range).
// Zwraca liczbę poprawnie wykrytych czujników.
uint8_t beginAll(SPIClass *spi, uint32_t spiHz, float odr_hz, ADXL345FreshSPI::Range range = ADXL345FreshSPI::Range::G16) {
presentMask_ = 0;
// Zbezpieczenie: inicjalizacja CS na HIGH przed startem.
for (uint8_t i=0;i<MAX_SENSORS;i++){
pinMode(cs_[i], OUTPUT);
digitalWrite(cs_[i], HIGH);
}
delayMicroseconds(5);
for (uint8_t i=0;i<MAX_SENSORS;i++){
if (!acc_[i].begin(spi, cs_[i], spiHz)) continue;
acc_[i].write8(0x2D, 0x00); // POWER_CTL = standby (MEASURE=0)
if (!acc_[i].setRange(range, true)) continue;
acc_[i].write8(0x2D, 0x08); // POWER_CTL = measure
if (!acc_[i].setODR_Hz(odr_hz)) continue;
// Tryb STREAM najbezpieczniejszy dla jitteru CPU
acc_[i].enableFIFO(ADXL345FreshSPI::FIFOmode::STREAM, 32);
presentMask_ |= (1u << i);
}
return countPresent();
}
inline uint8_t size() const { return MAX_SENSORS; }
inline bool isPresent(uint8_t i) const { return (presentMask_ & (1u << i)) != 0; }
inline uint8_t countPresent() const { return __builtin_popcount(presentMask_); }
// Sprawdza, czy KAŻDY obecny sensor ma >=1 nową próbkę (DATA_READY).
bool availableAll() {
for (uint8_t i=0;i<MAX_SENSORS;i++){
if (!isPresent(i)) continue;
if (!acc_[i].available()) return false;
}
return true;
}
// Zdejmuje po 1 NAJSTARSZEJ próbce z FIFO każdego obecnego sensora.
// Zwraca liczbę faktycznie zebranych próbek (== liczbie obecnych sensorów, jeśli sukces).
uint8_t readAlignedOnce(int16_t x[], int16_t y[], int16_t z[], uint32_t ts_us[]) {
uint8_t got = 0;
for (uint8_t i=0;i<MAX_SENSORS;i++){
if (!isPresent(i)) continue;
ADXL345FreshSPI::SampleI16 one[1];
size_t n = acc_[i].readFIFOBurst(one, 1); // NAJSTARSZA próbka z FIFO
if (n == 0) {
// sporadycznie: awaryjnie dociągnij świeżą (rzadkie)
ADXL345FreshSPI::SampleI16 s;
if (!acc_[i].readFresh(s, 1)) return got; // przerwij zbieranie ramki
one[0] = s;
}
x[i] = one[0].x; y[i] = one[0].y; z[i] = one[0].z; ts_us[i] = one[0].ts_us;
got++;
}
return got;
}
ADXL345FreshSPI& at(uint8_t i) { return acc_[i]; }
private:
uint8_t cs_[MAX_SENSORS]{};
ADXL345FreshSPI acc_[MAX_SENSORS];
uint32_t presentMask_ = 0;
};

View File

@@ -0,0 +1,93 @@
#pragma once
#include <Arduino.h>
#include <SPI.h>
#include "ADXL345FreshSPI.h"
#include <Logger.h>
class ADXL345FastSPI {
public:
static constexpr uint8_t MAX_SENSORS = 4; // Maksymalna liczba ADXL345
enum Rate { RATE_100HZ, RATE_200HZ, RATE_400HZ, RATE_800HZ, RATE_1600HZ, RATE_3200HZ };
enum Range { RANGE_2G, RANGE_4G, RANGE_8G, RANGE_16G };
ADXL345FastSPI(const uint8_t* csPins, uint8_t count) : count_(count > MAX_NUM ? MAX_NUM : count){
for (uint8_t i = 0; i < count_; ++i) cs_[i] = csPins[i];
}
/* Pobierz konfigurację zakres akcelerometru */
uint8_t getRange(uint8_t accel);
/* Zwraca czy jest FUL_RES*/
bool getFullRes(uint8_t accel);
// Wersja podstawowa: podaj SPIClass* (np. &SPI)
bool begin(SPIClass *spi, uint32_t spiHz, Rate rate, Range range, uint8_t options = 0);
// Wersja wygodna: deleguje do powyższej, używając globalnego SPI
inline bool begin(uint32_t spiHz, Rate rate, Range range, uint8_t options = 0) {
return begin(&SPI, spiHz, rate, range, options);
}
// Informacje / dostępność
inline uint8_t size() const { return presentCnt_; }
inline bool isPresent(uint8_t i) const { return (i < count_) && present_[i]; }
bool availableAll();
// Wariant B: zdejmuje po 1 NAJSTARSZEJ próbce z FIFO każdego obecnego sensora
// Zwraca liczbę zebranych próbek (== size(), jeśli pełna ramka).
uint8_t readAlignedOnce(int16_t* x, int16_t* y, int16_t* z, uint32_t* ts_us);
// Zgodność: pojedynczy sensor 1 próbka
bool readNewSample(uint8_t idx, int16_t& x, int16_t& y, int16_t& z, bool& ready);
// Zwraca ilośc podłączonych czujników po inicjalizacji - ale nie jest dynamiczna!
inline uint8_t connectedSensorsCount() const { return presentCnt_; }
// dynamiczne odświeżenie liczby działających czujników zawsze sprawdzi na bieżąco i zwróci przy działającym urządzeniu
uint8_t refreshConnectedSensorsCount();
// Maska aktywnych sensorów wg bieżącego stanu (bit i = sensor i)
inline uint8_t activeMask() const {
uint8_t m = 0;
for (uint8_t i = 0; i < count_; ++i) if (present_[i]) m |= (1u << i);
return m;
}
// Odświeża stan (ping) i zwraca zaktualizowaną maskę aktywnych sensorów
uint8_t refreshActiveMask();
private:
static constexpr uint8_t MAX_NUM = MAX_SENSORS;
SPIClass* spi_ = &SPI;
ADXL345FreshSPI dev_[MAX_NUM];
uint8_t cs_[MAX_NUM]{};
bool present_[MAX_NUM]{};
uint8_t count_ = 0;
uint8_t presentCnt_ = 0;
uint32_t spiHz_ = 5000000;
float odrHz_ = 3200.0f;
static float mapRateToHz(Rate r) {
switch (r) {
case RATE_100HZ: return 100.0f;
case RATE_200HZ: return 200.0f;
case RATE_400HZ: return 400.0f;
case RATE_800HZ: return 800.0f;
case RATE_1600HZ: return 1600.0f;
case RATE_3200HZ: return 3200.0f;
default: return 3200.0f;
}
}
static ADXL345FreshSPI::Range mapRange(Range r) {
switch (r) {
case RANGE_2G: return ADXL345FreshSPI::Range::G2;
case RANGE_4G: return ADXL345FreshSPI::Range::G4;
case RANGE_8G: return ADXL345FreshSPI::Range::G8;
case RANGE_16G: return ADXL345FreshSPI::Range::G16;
default: return ADXL345FreshSPI::Range::G16;
}
}
};

View File

@@ -0,0 +1,87 @@
#pragma once
#include <Arduino.h>
#include <SPI.h>
#include "ADXL345Registers.h"
#include <Logger.h>
class ADXL345FreshSPI {
public:
enum class Range { G2=0, G4=1, G8=2, G16=3 };
enum class FIFOmode { BYPASS, FIFO, STREAM, TRIGGER };
struct SampleI16 {
int16_t x, y, z;
uint32_t ts_us;
};
struct SampleSI {
float ax_g, ay_g, az_g;
float ax_ms2, ay_ms2, az_ms2;
uint32_t ts_us;
};
ADXL345FreshSPI() = default;
// --- Init (SPI) ---
// Uwaga: ADXL345 wymaga SPI MODE3, zegar ≤ ~5 MHz.
bool begin(SPIClass *spi, uint8_t csPin, uint32_t clockHz = 5000000);
// --- Konfiguracja ---
bool setODR_Hz(float odr_hz);
bool setRange(Range r, bool fullRes=true);
bool enableFIFO(FIFOmode mode, uint8_t triggerLevel=16);
bool enableDataReadyInterrupt(bool enable=true); // przy pollingu pozostaw false
// --- Status ---
bool ping(); // DEVID == 0xE5 ?
bool available(); // DATA_READY z INT_SOURCE
// --- Odczyty „świeże” (blokujące do timeout_ms) ---
bool readFresh(SampleI16& out, uint32_t timeout_ms = 100);
bool readFresh(SampleSI& out, uint32_t timeout_ms = 100);
// --- Zrzut FIFO (do n elementów) ---
size_t readFIFOBurst(SampleI16* buf, size_t maxCount);
size_t readFIFOBurst(SampleSI* buf, size_t maxCount);
// --- Parametry pomocnicze ---
void setGConstant(float g = 9.80665f) { g_ms2 = g; }
float lsb_per_g() const { return 1.0f / scale_g_per_lsb; }
//bool write8(uint8_t reg, uint8_t val);
//bool read8(uint8_t reg, uint8_t& val);
/*
Zwraca tryb pracy akcelerometru
*/
uint8_t getADXLRange();
/* Zwraca czy jest FULL_RES w akcelerometrze */
bool getADXLFullRes();
void showRangeFull(String txt="");
// niskopoziomowe
bool write8(uint8_t reg, uint8_t val);
bool read8(uint8_t reg, uint8_t& val);
private:
SPIClass* spi = nullptr;
uint8_t cs = 255;
uint32_t spiHz = 5000000;
float scale_g_per_lsb = 0.0039f; // full-res ~3.9 mg/LSB
float g_ms2 = 9.80665f;
bool fullRes = true;
// niskopoziomowe
//bool write8(uint8_t reg, uint8_t val);
//bool read8(uint8_t reg, uint8_t& val);
bool readMulti(uint8_t reg, uint8_t* dst, size_t n);
void spiSelect();
void spiDeselect();
bool configurePowerMeasure();
uint8_t odrCodeFromHz(float hz);
void countsToSI(const SampleI16& in, SampleSI& out);
};

View File

@@ -0,0 +1,27 @@
#pragma once
#define ADXL345_REG_DEVID 0x00
#define ADXL345_REG_BW_RATE 0x2C
#define ADXL345_REG_POWER_CTL 0x2D
#define ADXL345_REG_INT_ENABLE 0x2E
#define ADXL345_REG_INT_MAP 0x2F
#define ADXL345_REG_INT_SOURCE 0x30
#define ADXL345_REG_DATA_FORMAT 0x31
#define ADXL345_REG_DATAX0 0x32
#define ADXL345_REG_DATAX1 0x33
#define ADXL345_REG_DATAY0 0x34
#define ADXL345_REG_DATAY1 0x35
#define ADXL345_REG_DATAZ0 0x36
#define ADXL345_REG_DATAZ1 0x37
#define ADXL345_REG_FIFO_CTL 0x38
#define ADXL345_REG_FIFO_STATUS 0x39
#define ADXL345_POWER_MEASURE 0x08
#define ADXL345_DATA_READY_BIT 0x80 // INT_SOURCE[7]
#define ADXL345_DATA_FORMAT_FULL_RES 0x08
#define ADXL345_DATA_FORMAT_RANGE_MASK 0x03
#define ADXL345_FIFO_BYPASS 0x00
#define ADXL345_FIFO_FIFO 0x40
#define ADXL345_FIFO_STREAM 0x80
#define ADXL345_FIFO_TRIGGER 0xC0

View File

@@ -0,0 +1,19 @@
#ifndef APICLIENT_H
#define APICLIENT_H
#include <Arduino.h>
#include <WiFiClient.h>
#include <HTTPClient.h>
#include <FS.h>
#include <SD.h>
#include "Config.h"
#include "Watchdog.h"
class APIClient {
public:
APIClient();
bool uploadMeasurement(const String& filePath);
};
#endif

View File

@@ -0,0 +1,72 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <Logger.h>
#include <Arduino.h>
#include <EEPROM.h>
#define EEPROM_SIZE 1024
//constexpr size_t EEPROM_SIZE = sizeof(Config) + 32; // z lekkim zapasem
extern bool isRebootRequired;
extern bool isClearLog;
extern bool connected;
extern long countConnect;
extern long countDisconnect;
extern String act_rssi_percent;
extern int8_t rssi;
extern String actDate;
extern String actTime;
struct Config {
bool connect; // czy łączyć z Internetem?
bool measure; // true - pomiary ciągłe, false - nie rób nic (tryb konfiguracji)
char ip[16];
char subnet[16];
char gateway[16];
char dns[16];
char ssid[32];
char ntp[50];
char password[32];
char hostname[32];
char place[100]; // miejsce instalacji
bool dhcp; // czy włączyć DHCP?
char user[10]; // użytkownik konfiguracji
char pass[20]; // hasło użytkownika konfiguracji
char updateUrl[150]; // adres pliku aktualizacji
char restURL[150]; // adres Rest API
int restPort; // Port systemu Api na serwerze
char restUser[30]; // login RestAPI
char restPass[50]; // hasło RestAPi
uint8_t apiKey[32]; // Klucz API KEY
uint32_t pause; // Pomiar co milisekund (ms)
uint8_t duration; // Czas pomiaru w sekundach 1-25
char S0[12]; // nazwy czujników 1-8
char S1[12];
char S2[12];
char S3[12];
char S4[12];
char S5[12];
char S6[12];
char S7[12];
};
// Global config declaration
extern Config config;
static_assert(sizeof(Config) + 1 <= EEPROM_SIZE, "Config struct exceeds EEPROM!");
class ConfigManager {
public:
ConfigManager();
void begin(); // EEPROM initialization
void readConfig(); // Odczyt konfiguracji z EEPROM
void saveConfig(); // Zapis konfiguracji do EEPROM
void resetToDefaults(); // Reset do ustawień domyślnych
void showConfig();
void generateApiKey(uint8_t *buf, size_t len);
private:
bool isEEPROMEmpty();
};
#endif

View File

@@ -0,0 +1,93 @@
#ifndef DISPLAY_H
#define DISPLAY_H
#pragma once
#include "esp_log.h"
#include <Arduino.h>
#include <LiquidCrystal_I2C.h>
#include <Wire.h>
#include <Version.h>
#include <RTClib.h>
#include <Config.h>
#define SCREEN_WIDTH 40
#define SCREEN_HEIGHT 4
#define SCREEN_ADDRESS 0x27
class Display {
public:
Display(RTC_DS3231 &rtc,
uint8_t address = SCREEN_ADDRESS,
uint8_t columns = SCREEN_WIDTH,
uint8_t rows = SCREEN_HEIGHT);
// Zwraca true, jeśli urządzenie na I2C odpowiada
bool begin(TwoWire *wire = &Wire);
/* Ekran startowy urządzenia */
void welcomeScreen();
/* Czyście wiersz */
void clearRow(uint16_t line);
/* Wyświetla wyśrodkowany tekst w danym wierszu */
void textCenter(uint8_t line, const char *txt);
/* Wyświetla tekst w danym wierszu od lewej, bez centrowania */
void text(uint8_t line, const char *text);
/* Status - najniższy wiersz. Czyści przed zapisaniem */
void textStatus(const char *text);
void clear();
/* Ekran po uruchomieniu */
void mainScreen();
/* Ekran jeśli offline miga co 1 sek */
void displayOffline(bool measure, uint8_t count, float freeSpace, long licznik, bool refresh = false);
/* Podsumowanie po pomiarze: ramki, sampling, etc. */
void displaySampleRateSummary(uint32_t reccount, uint32_t captureSeconds, float khz, String filename);
void updateNetwork(String ip, bool connected);
void updateBarWiFi(long signal, bool connected);
void textStyle(const uint8_t st, String text);
void message(String text);
void print(String text);
void println(String text);
void setCursor(int16_t x, int16_t y);
void showAccel(float a, float b, float c);
void displayOnOffM(bool measure, String myDir, String myFile);
void initMeasure(
bool measure, // czy pomiar ciągły?
bool run, // czy uruchomiony?
bool runmes,
uint16_t pause, // Czas (s) pomiędzy próbkami
uint8_t duration, // Czas (s) trwania próbki
bool connect, // Czy połączenie Network
long counter, // główny licznik long
String gdate, // Data aktualna
String gtime); // Czas aktualny
private:
LiquidCrystal_I2C *_lcd;
RTC_DS3231 &rtc_;
uint8_t _columns;
uint8_t _rows;
uint8_t _address;
// Poprzednie
uint16_t oyear;
uint8_t omonth;
uint8_t oday;
uint8_t ohour;
uint8_t omin;
uint8_t osec;
float ospace;
uint8_t oadxlcnt;
bool omode;
};
#endif

View File

@@ -0,0 +1,15 @@
#ifndef LOGGER_H
#define LOGGER_H
#include "esp_log.h"
// Wszystkie tagi logowania w jednym miejscu
extern const char *TAG_MAIN;
extern const char *TAG_DISP;
extern const char *TAG_ADXL;
extern const char *TAG_CONF;
// Funkcja inicjalizacji poziomów logowania
void init_log_levels();
#endif

View File

@@ -0,0 +1,125 @@
#ifndef MEASURE_H
#define MEASURE_H
#include <Arduino.h>
#include <Watchdog.h>
#include <SPI.h>
#include <SD.h>
#include <FS.h>
#include <RTClib.h>
#include <Display.h>
#include "ADXL345FastSPI.h" // <-- zgodnie z main.cpp
#include <Logger.h>
#include <Pinout.h>
// Domyślne parametry akwizycji ADXL345 (mogą być nadpisane w ADXL345FastSPI::begin)
static constexpr uint32_t SPI_HZ = 5000000; // 5 MHz (MODE3)
static constexpr float ODR_HZ = 3200.0f; // maks. ODR
// Zakres ustawiany w main.cpp przez ADXL345FastSPI::begin(..., RANGE_2G, ...)
struct FileInfo {
String path; // np. "/3/00000057.wmt"
uint64_t size; // bajty
bool exists; // true, jeśli ostatni plik istnieje
};
struct SpaceInfo {
double value;
const char *unit; // "GB" | "MB" | "UNKNOWN"
};
class DataCapture {
// --- Nagłówek pliku WMT ---
struct FileHeader {
char magic[3]; // "WMT"
uint16_t version; // 1
uint16_t headerSize; // sizeof(FileHeader)
uint32_t sampleSize; // sizeof(Sample)
uint32_t timestamp; // UNIX startu akwizycji
uint32_t reccount; // liczba rekordów Sample w pliku
} __attribute__((packed));
public:
// --- Rekord próbki ---
struct Sample {
uint32_t offset; // µs od startu akwizycji (wspólny dla ramki)
uint8_t sensor_id; // 0..3
int16_t x, y, z; // surowe ADXL345
bool ready; // 1 = obecna
} __attribute__((packed));
// Konstruktor dopasowany do main.cpp przyjmuje ADXL345FastSPI
DataCapture(ADXL345FastSPI &adxl, Display &display, RTC_DS3231 &rtc, fs::FS &storage, size_t bufferSize = 131072 /* 128 KB */);
~DataCapture();
bool capture(uint32_t captureSeconds, const char *filename);
bool captureAuto(uint32_t captureSeconds, const char *baseDirectory = "/logs");
void printSamplingRate(uint32_t reccount, uint32_t captureSeconds, String filename);
void readHeaderAndPrint(const char *path);
void stop();
bool isActive() const { return measurementActive_; }
String getCurrentCapturePath() const { return currentCapturePath_; }
// SD utils
SpaceInfo freeSpaceMB();
float freeSpaceFloat(bool *isGB = nullptr);
String unixToDateTime(uint32_t ts);
// plik z najwyższym indeksem
FileInfo getLastFileInfo();
bool deleteAllOnSD();
bool isExit = false; // true oznacza przerwanie pomiaru
private:
ADXL345FastSPI &adxl_;
Display &display_;
RTC_DS3231 &rtc_;
fs::FS &_fs;
// Katalogowanie
const String _baseDir = "/";
const String _ext = ".wmt";
const uint8_t _digits = 8;
const uint16_t _maxFilesPerDir = 400;
// Bufor zapisu (PSRAM)
uint8_t *buffer_ = nullptr;
size_t bufferSize_ = 0;
size_t bufferIndex_ = 0;
bool measurementActive_ = false;
String currentCapturePath_ = "";
bool flushToFile(File &f);
static uint8_t crc8(const uint8_t *data, size_t len);
void setTestingIndicator_(bool on, String myDir, String myFile);
bool isEscape(){
bool ispress = (GPIO.in & (1UL << BTN_OK)) == 0;
if(ispress) {
isExit = true;
measurementActive_ = false;
display_.textStatus("Cancelling. Wait!");
}
return ispress;
} // szybkidigitalRead : czy BTN stop???
public:
bool isAllDigits(const char *s);
uint32_t toUint(const char *s);
static const char *basenameFromPath(const char* full);
bool isWmtWithDigits(const char *name, uint32_t &idxOut) const;
String makeIndexedName(uint32_t idx) const;
static String joinPath(const String &a, const String &b);
bool ensureDir(uint32_t dirNum);
uint32_t findHighestNumericDir();
void scanDirForWmt(uint32_t dirNum, uint32_t &count, uint32_t &highestIdx);
bool recursiveDelete(const String &path);
String dirPath(uint32_t dirNum) const;
String allocateNextFilePath();
String generateNextFilename();
void printLastFileInfoSerial();
};
#endif // MEASURE_H

View File

@@ -0,0 +1,66 @@
#ifndef NETWORK_H
#define NETWORK_H
#include "esp_log.h"
#include <Arduino.h>
#include <Config.h>
#ifdef ESP32
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#endif
//#include <Pinout.h>
//#include <IPAddress.h>
//#include <WiFiUdp.h>
//#include <WiFiClientSecureBearSSL.h>
// OTA
#include <HTTPUpdate.h>
#include <WiFiClientSecure.h>
#include <functional>
// OTA
class WiFiManager {
public:
WiFiManager();
void begin();
void connectToWiFi();
void setupAccessPoint(const char *newSSID, const char *newPassword);
void checkWiFiConnection();
void updateLED();
void setupMDNS();
void ReadConnection();
bool isConnected(); // Zwraca stan połączenia
bool convertCharToIPAddress(const char *str, IPAddress& ip);
void ledBlink(); // Z innego projektu - niepotrzebne, bo nie ma LED WiFi...
bool isWiFiOK(); // True, gdy WiFi połączone i false, gdy brak połączenia
int rssiToPercent(int rssi); // rssi na procenty
int8_t getRSSI();
/**
* Aktualizacja systemu przez internet z adresu config.updateUrl.
* @param allowInsecureTLS true => dla https wyłącz weryfikację certyfikatu.
* @param progressCb callback progress (opcjonalnie) (bytes, total).
* @return true, jeśli update zakończony sukcesem (urządzenie się zrestartuje).
*/
bool performOTAUpdate(bool allowInsecureTLS = true, std::function<void(int,int)> progressCb = nullptr);
int8_t rssi = 0;
bool useDHCP = true;
IPAddress local_IP;
IPAddress gateway;
IPAddress subnet;
IPAddress dns;
String ssidAP = "ACCEL666";
String passwordAP = "12345678";
private:
bool isAccessPoint = false;
};
#endif // WIFIMANAGER_H

View File

@@ -0,0 +1,52 @@
#ifndef PINOUT_H
#define PINOUT_H
#if defined(ESP32)
// SPI3 (HSPI) - SD Card nie kolidują z PSRAM
#define SD_SCK 16 // 36 //18
#define SD_MOSI 17 // 35 //17
#define SD_MISO 18 // 37 //16
#define SD_CS 15 // 34 //15 ?? 34
// SPI2 (VSPI) - ADXL345
#define MOSI_ADSX 11 // SDA
#define CLK_ADSX 12 // SCL
#define MISO_ADSX 13 // SDO
//I2C C3
#define PIN_SDA 47 // szary
#define PIN_SCL 48 // niebieski
// Przycisk
#define BTN_UP 5
#define BTN_OK 6
#define BTN_DOWN 7
#endif
/*
CS {5, 6, 7, 10, 14, 21}
SPI2 (VSPI) — preferowane do ADXL345 (wysoka prędkość, stabilność). ASXL345
SPI3 (HSPI) — dowolne piny do SD (niższa prędkość, ale elastyczność). SD
Nie używać ESP32-S3:
integrated SPI flash: 26, 27, 28, 29, 30, 31, 32,
USB: 19, 20, 43, 44
PSRAM 35, 37
*/
#if defined(ARDUINO_RASPBERRY_PI_PICO)
#define CLK_ADSX 18
#define MOSI_ADSX 19
#define MISO_ADSX 16
#define SD_MISO 12
#define SD_CS 13
#define SD_SCK 14
#define SD_MOSI 15
#define I2C_SDA 20
#define I2C_SCL 21
#endif
#endif

View File

@@ -0,0 +1,52 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <Arduino.h>
#include <Pinout.h>
#include <Watchdog.h>
#include <RTClib.h>
#include <Display.h>
#include <Logger.h>
#include <Config.h>
class Settings {
public:
Settings(Display &display, RTC_DS3231 &rtc);
~Settings();
void setTimeRTC();
void setConfigDevice();
void finishConfigDevice();
void begin();
bool isPressed(uint8_t btnIndex);
// bool is1and3();
bool isBtnReset();
bool isSetClock();
bool readBtnUp() { return digitalRead(BTN_UP) == LOW; }
bool readBtnOk() { return digitalRead(BTN_OK) == LOW; }
bool readBtnDown() { return digitalRead(BTN_DOWN) == LOW; }
// Struktura na datę i czas
struct RtcDateTime {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
};
private:
Display &display_;
RTC_DS3231 &rtc_;
int editField(int value, int minVal, int maxVal, const char *label);
void constrainValue(int &value, int minVal, int maxVal);
void printField(const char *label, int value);
};
#endif

View File

@@ -0,0 +1,14 @@
#ifndef TOOL_H
#define TOOL_H
#include <Arduino.h>
#include <Wire.h>
#include "esp_log.h"
#include "Watchdog.h"
void scanI2C();
/* Funkcja przyjmuje adres jako argument i zwraca true gdy urządzenie odpowiada, w przeciwnym wypadku false */
bool isI2CDevPresent(uint8_t address);
#endif

View File

@@ -0,0 +1,31 @@
#ifndef UPLOADMANAGER_H
#define UPLOADMANAGER_H
#include <Arduino.h>
#include <FS.h>
#include <SD.h>
#include "APIClient.h"
#include "RTClib.h"
#include "Measure.h"
class UploadManager {
public:
UploadManager(APIClient& client, RTC_DS3231& rtc, DataCapture& capture);
// Call this to upload a specific file immediately
void uploadFile(const String& filePath);
// Call this in the background when WiFi is connected
void processPendingUploads();
private:
APIClient& apiClient;
RTC_DS3231& rtc_;
DataCapture& capture_;
bool isAlreadyUploaded(const String& filePath);
void appendLog(const String& filePath, const String& status);
String getCurrentTimestamp();
};
#endif

View File

@@ -0,0 +1,9 @@
#ifndef VERSION_H
#define VERSION_H
#define VERSION "1.3.4.2"
// 1: graphical 128x64, 2: LCD I2C Text 4x20
#define LCD_TYPE 2
#endif

View File

@@ -0,0 +1,38 @@
#pragma once
/**
* Watchdog — prosty interfejs do inicjalizacji i karmienia WDT z dowolnego modułu.
*
* Obsługiwane środowiska:
* - ESP32 Arduino Core / ESP-IDF (esp_task_wdt)
* - Fallback: no-op na innych platformach
*
* Użycie:
* Watchdog::init(5, true);
* Watchdog::addThisTask();
* ...
* Watchdog::feed();
*/
#include <stdint.h>
namespace Watchdog {
/** Inicjalizacja Task Watchdog (idempotentna). */
bool init(int timeout_seconds = 5, bool panic_on_trigger = true);
/** Dodaje bieżący task (wątki FreeRTOS: wołaj w ciele tego taska). */
bool addThisTask();
/** Usuwa bieżący task z nadzoru WDT. */
bool removeThisTask();
/** Karmi watchdog (reset licznika). */
void feed();
/** Zmienia timeout (wykonuje re-init wewnętrznie, jeśli trzeba). */
bool setTimeout(int timeout_seconds);
/** Czy watchdog jest aktywny (zainicjalizowany)? */
bool isActive();
} // namespace Watchdog

View File

@@ -0,0 +1,147 @@
#include "ADXL345FastSPI.h"
#include "Watchdog.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)
uint32_t start_wait = millis();
while (!availableAll()) {
if (millis() - start_wait > 100) {
ESP_LOGE(TAG_FRESH, "Timeout waiting for sensors data!");
return 0;
}
Watchdog::feed();
yield();
}
// 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,124 @@
#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;
}
// Wyciągnij samą nazwę pliku (bez ścieżki)
String filename = filePath;
int slashIndex = filename.lastIndexOf('/');
if (slashIndex >= 0) {
filename = filename.substring(slashIndex + 1);
}
// multipart/form-data boundary
String boundary = "----ESP32WMTBoundary";
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();
// Połączenie z hostem
WiFiClient client;
String host = String(config.restURL);
int port = config.restPort;
// Usuń prefiks protokołu z hosta
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);
// --- HTTP Request ---
// Endpoint wg dokumentacji API IoT
client.println("POST /api/v1/measurements/measurements/upload HTTP/1.1");
client.println("Host: " + host + ":" + String(port));
// Basic Auth: base64(serial:password)
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();
// Multipart body: head + file data + tail
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();
// --- Parsowanie odpowiedzi ---
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();
// --- Obsługa kodów odpowiedzi wg dokumentacji API ---
if (httpCode == 201) {
ESP_LOGI(TAG_API, "Upload successful: 201 Created");
return true;
} else if (httpCode == 401) {
ESP_LOGE(TAG_API, "Unauthorized 401: Wrong serial or password!");
} else if (httpCode == 403) {
ESP_LOGE(TAG_API, "Forbidden 403: Device not authorized or user tried upload.");
} else if (httpCode == 400) {
ESP_LOGE(TAG_API, "Bad Request 400: %s", responseBody.c_str());
} 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) != 253){
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.begin(EEPROM_SIZE);
EEPROM.put(1, config);
EEPROM.write(0, 253);
if (EEPROM.commit()) {
ESP_LOGI(TAG_CONF, "Config saved");
} else {
ESP_LOGE(TAG_CONF, "Error save config");
}
}
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 = 1; // 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, "SN001234ABCD56789012");
strcpy(config.restPass, "device001");
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, 253);
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,281 @@
#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 = 0; omonth = 0; oday = 0;
ohour = 0; omin = 0; osec = 0;
ospace = 0; 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,461 @@
#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;
}
// Obliczamy ile danych maksymalnie może wejść do bufora
const uint8_t presentCnt = adxl_.size();
const size_t frameSize = presentCnt * sizeof(Sample);
currentCapturePath_ = String(filename);
ESP_LOGI(TAG_CAPTURE, "Start RAM capture: %u sec.", (unsigned)captureSeconds);
display_.textStatus("Sampling...");
static constexpr uint8_t MAXN = ADXL345FastSPI::MAX_SENSORS;
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
uint32_t start_wait = millis();
while (!adxl_.availableAll()) {
if (isEscape()) break;
if (millis() - start_wait > 500) {
ESP_LOGE(TAG_CAPTURE, "Sensor data timeout - aborting capture.");
measurementActive_ = false;
break;
}
Watchdog::feed();
yield();
}
if (!measurementActive_) break;
const uint8_t got = adxl_.readAlignedOnce(X, Y, Z, TS);
if (got == 0 || 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
size_t hdrWritten = dataFile.write(reinterpret_cast<const uint8_t*>(&hdr), sizeof(hdr));
if (hdrWritten != sizeof(hdr)) {
ESP_LOGE(TAG_CAPTURE, "Header write failed!");
dataFile.close();
return false;
}
// 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);
dataFile.flush(); // Power-loss resilience: wymuszenie zapisu na kartę
display_.textStatus(basenameFromPath(filename));
delay(500);
} else {
ESP_LOGE(TAG_CAPTURE, "SD Write Error!");
display_.textStatus("SD Write Err");
}
dataFile.close();
printSamplingRate(frames, captureSeconds, filename);
currentCapturePath_ = "";
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(500);
}
// --- 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 && strncmp(name + _digits, ".upl", 4) != 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()) {
Watchdog::feed();
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()) {
Watchdog::feed();
if (f.isDirectory()) { 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
ESP_LOGI(TAG_CAPTURE, "Last file: %s", info.path.c_str());
ESP_LOGI(TAG_CAPTURE, "Size: %.2f MB", mb);
#endif
}

View File

@@ -0,0 +1,275 @@
#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);
s.trim();
p.trim();
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,96 @@
#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) {
// Legacy CSV log removed to fix O(N^2) SD card bottleneck
}
bool UploadManager::isAlreadyUploaded(const String& filePath) {
// We now rely on file extensions (.wmt = pending, .upl = uploaded)
return false;
}
void UploadManager::uploadFile(const String& filePath) {
if (WiFi.status() != WL_CONNECTED) {
ESP_LOGE(TAG_UPLOAD, "No WiFi. Cannot upload %s", filePath.c_str());
return;
}
bool success = apiClient.uploadMeasurement(filePath);
if (success) {
String newPath = filePath;
newPath.replace(".wmt", ".upl");
if (SD.rename(filePath, newPath)) {
ESP_LOGI(TAG_UPLOAD, "Renamed %s to .upl", filePath.c_str());
} else {
ESP_LOGE(TAG_UPLOAD, "Rename to .upl failed for %s", filePath.c_str());
}
} else {
if (WiFi.status() != WL_CONNECTED) {
ESP_LOGE(TAG_UPLOAD, "WiFi lost during upload");
} else {
ESP_LOGE(TAG_UPLOAD, "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();
vTaskDelay(1); // yield do IDLE0 między katalogami
continue;
}
for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {
vTaskDelay(1); // yield do IDLE0 przy każdym pliku — zapobiega głodzeniu WDT
if (f.isDirectory()) { f.close(); continue; }
String childPath = String(f.name());
if (!childPath.startsWith("/")) {
childPath = path + "/" + childPath;
}
f.close();
// Pomijamy pliki .upl (już wgrane) i inne rozszerzenia
if (!childPath.endsWith(".wmt")) continue;
// Pomijamy plik aktualnie zapisywany przez pomiar
if (childPath == capture_.getCurrentCapturePath()) continue;
Watchdog::feed();
if (WiFi.status() != WL_CONNECTED) {
dir.close();
return;
}
ESP_LOGI(TAG_UPLOAD, "Found pending file: %s", childPath.c_str());
uploadFile(childPath);
// Krótka przerwa po uploaderze — pozwala IDLE0 na reset WDT
vTaskDelay(pdMS_TO_TICKS(200));
}
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,466 @@
#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"
#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)
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
TaskHandle_t uploadTaskHandle = NULL;
SemaphoreHandle_t sdMutex = NULL; // Mutex chroniący dostęp do karty SD
void uploadTaskCode(void *parameter) {
Watchdog::addThisTask();
while(true) {
if (config.connect && WiFi.status() == WL_CONNECTED && !testingNow) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
uploadManager.processPendingUploads();
xSemaphoreGive(sdMutex);
}
}
Watchdog::feed();
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
//////// 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");
// Inicjalizacja mutex SD (po SD.begin)
sdMutex = xSemaphoreCreateMutex();
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_ADXL, 5000000, ADXL345FastSPI::RATE_3200HZ, ADXL345FastSPI::RANGE_16G, 1)){
if(!adxl.begin(&SPI_ADXL, 2000000, 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();
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){
xTaskCreatePinnedToCore(
uploadTaskCode, // Funkcja zadania
"UploadTask", // Nazwa zadania
8192, // Rozmiar stosu
NULL, // Parametr
1, // Priorytet (1 - niski, domyślna pętla ma 1 na Core 1)
&uploadTaskHandle,// Uchwyt
0 // Przypięcie do rdzenia 0 (Wi-Fi działa domyślnie na 0)
);
ESP_LOGI(TAG_MAIN, "Upload task created on Core 0");
}
// 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");
xSemaphoreTake(sdMutex, portMAX_DELAY);
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");
}
xSemaphoreGive(sdMutex);
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(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 ++;
}

View File

@@ -71,6 +71,7 @@ bool APIClient::uploadMeasurement(const String &filePath) {
// Multipart body: head + file data + tail
client.print(head);
ESP_LOGI(TAG_API, "DEBUG: Beginning file read loop");
uint8_t buffer[2048];
while (file.available()) {
@@ -79,8 +80,10 @@ bool APIClient::uploadMeasurement(const String &filePath) {
Watchdog::feed();
}
ESP_LOGI(TAG_API, "DEBUG: File read loop finished. Writing tail.");
client.print(tail);
file.close();
ESP_LOGI(TAG_API, "DEBUG: Tail written. Waiting for response...");
// --- Parsowanie odpowiedzi ---
int httpCode = 0;
@@ -98,6 +101,7 @@ bool APIClient::uploadMeasurement(const String &filePath) {
}
Watchdog::feed();
}
ESP_LOGI(TAG_API, "DEBUG: Header parsing loop finished. httpCode: %d", httpCode);
String responseBody = "";
while (client.available()) {

View File

@@ -236,8 +236,6 @@ bool DataCapture::capture(uint32_t captureSeconds, const char *filename) {
const uint8_t presentCnt = adxl_.size();
const size_t frameSize = presentCnt * sizeof(Sample);
currentCapturePath_ = String(filename);
ESP_LOGI(TAG_CAPTURE, "Start RAM capture: %u sec.", (unsigned)captureSeconds);
display_.textStatus("Sampling...");
@@ -344,7 +342,6 @@ bool DataCapture::capture(uint32_t captureSeconds, const char *filename) {
dataFile.close();
printSamplingRate(frames, captureSeconds, filename);
currentCapturePath_ = "";
return (written == bufferIndex_);
}
@@ -512,7 +509,7 @@ const char *DataCapture::basenameFromPath(const char *full) { const char *slash
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 && strncmp(name + _digits, ".upl", 4) != 0) 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;
@@ -536,7 +533,6 @@ uint32_t DataCapture::findHighestNumericDir() {
if (!root || !root.isDirectory()) return 0;
uint32_t maxDir = 0;
for (File f = root.openNextFile(); f; f = root.openNextFile()) {
Watchdog::feed();
if (f.isDirectory()) {
const char *nm = basenameFromPath(f.name());
if (isAllDigits(nm)) { uint32_t v = toUint(nm); if (v > maxDir) maxDir = v; }
@@ -552,8 +548,7 @@ void DataCapture::scanDirForWmt(uint32_t dirNum, uint32_t &count, uint32_t &high
File dir = _fs.open(path);
if (!dir || !dir.isDirectory()) return;
for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {
Watchdog::feed();
if (f.isDirectory()) { f.close(); continue; }
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; }

View File

@@ -272,4 +272,95 @@ bool WiFiManager::performOTAUpdate(bool allowInsecureTLS, std::function<void(int
ESP_LOGE(WIFI, "[OTA] Błąd (%d): %s", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str());
return false;
}
}
void WiFiManager::startCaptivePortal() {
if (captivePortalActive) {
ESP_LOGI(WIFI, "Captive Portal is already running.");
return;
}
ESP_LOGI(WIFI, "Starting Captive Portal");
setupAccessPoint(ssidAP.c_str(), passwordAP.c_str());
// Start DNS Server
const byte DNS_PORT = 53;
dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
// Start Web Server
server.on("/", std::bind(&WiFiManager::handleRoot, this));
server.on("/save", HTTP_POST, std::bind(&WiFiManager::handleSave, this));
server.onNotFound(std::bind(&WiFiManager::handleNotFound, this));
server.begin();
captivePortalActive = true;
ESP_LOGI(WIFI, "Captive Portal Started");
}
void WiFiManager::handleClient() {
if (captivePortalActive) {
dnsServer.processNextRequest();
server.handleClient();
}
}
void WiFiManager::handleRoot() {
int bases[] = {2, 2, 2, 2, 3, 3, 10, 10};
int vals[] = {4, 8, 16, 32, 9, 27, 100, 1000};
int answers[] = {2, 3, 4, 5, 2, 3, 2, 3};
int idx = random(0, 8);
int base = bases[idx];
int val = vals[idx];
expectedCaptchaAnswer = answers[idx];
String html = "<!DOCTYPE html><html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
html += "<style>body{font-family: Arial, sans-serif; text-align: center; margin:0; padding: 20px;}";
html += "input[type=text], input[type=password], input[type=number] {width: 100%; padding: 12px; margin: 8px 0; display: inline-block; border: 1px solid #ccc; box-sizing: border-box;}";
html += "input[type=submit] {background-color: #4CAF50; color: white; padding: 14px 20px; margin: 8px 0; border: none; cursor: pointer; width: 100%;}";
html += "input[type=submit]:hover {opacity: 0.8;}";
html += ".container {max-width: 400px; margin: auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);}</style></head><body>";
html += "<h2>WiFi Configuration</h2>";
html += "<div class=\"container\">";
html += "<form action=\"/save\" method=\"POST\">";
html += "<label for=\"ssid\"><b>SSID</b></label>";
html += "<input type=\"text\" placeholder=\"Enter SSID\" name=\"ssid\" required>";
html += "<label for=\"password\"><b>Password</b></label>";
html += "<input type=\"password\" placeholder=\"Enter Password\" name=\"password\" required>";
html += "<label for=\"captcha\"><b>Captcha: What is log<sub>" + String(base) + "</sub>(" + String(val) + ")?</b></label>";
html += "<input type=\"number\" placeholder=\"Enter Answer\" name=\"captcha\" required>";
html += "<input type=\"submit\" value=\"Save and Restart\">";
html += "</form></div></body></html>";
server.send(200, "text/html", html);
}
void WiFiManager::handleSave() {
String ssid = server.arg("ssid");
String pass = server.arg("password");
String captchaStr = server.arg("captcha");
if (captchaStr.toInt() != expectedCaptchaAnswer) {
server.send(400, "text/plain", "Incorrect Captcha. Please go back and try again.");
return;
}
if (ssid.length() > 0) {
File f = SD.open("/wifi.txt", FILE_APPEND);
if (f) {
f.println(ssid + ";" + pass);
f.close();
server.send(200, "text/plain", "Credentials saved successfully! The device will now restart and try to connect.");
delay(2000);
ESP.restart();
} else {
server.send(500, "text/plain", "Failed to open wifi.txt on SD card for appending.");
}
} else {
server.send(400, "text/plain", "SSID cannot be empty.");
}
}
void WiFiManager::handleNotFound() {
server.sendHeader("Location", String("http://") + WiFi.softAPIP().toString(), true);
server.send(302, "text/plain", "");
}

View File

@@ -52,18 +52,22 @@ bool UploadManager::isAlreadyUploaded(const String& filePath) {
}
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;
}
ESP_LOGI(TAG_UPLOAD, "DEBUG: Before apiClient.uploadMeasurement");
bool success = apiClient.uploadMeasurement(filePath);
ESP_LOGI(TAG_UPLOAD, "DEBUG: After apiClient.uploadMeasurement. Success: %d", success);
if (success) {
appendLog(filePath, "OK");
String newPath = filePath;
newPath.replace(".wmt", ".upl");
SD.rename(filePath, newPath);
} else {
if (WiFi.status() != WL_CONNECTED) {
appendLog(filePath, "ERROR: WiFi lost during upload");
@@ -77,11 +81,14 @@ void UploadManager::processPendingUploads() {
if (WiFi.status() != WL_CONNECTED) return;
ESP_LOGI(TAG_UPLOAD, "Checking for pending uploads...");
ESP_LOGI(TAG_UPLOAD, "DEBUG: findHighestNumericDir()");
uint32_t highestDir = capture_.findHighestNumericDir();
ESP_LOGI(TAG_UPLOAD, "DEBUG: highestDir: %u", highestDir);
if (highestDir == 0) return;
for (uint32_t d = 1; d <= highestDir; d++) {
String path = capture_.dirPath(d);
ESP_LOGI(TAG_UPLOAD, "DEBUG: Scanning dir: %s", path.c_str());
File dir = SD.open(path);
if (!dir || !dir.isDirectory()) {
if(dir) dir.close();
@@ -96,10 +103,13 @@ void UploadManager::processPendingUploads() {
}
if (childPath.endsWith(".wmt")) {
if (childPath == capture_.getCurrentCapturePath()) continue;
ESP_LOGI(TAG_UPLOAD, "Found pending file: %s", childPath.c_str());
uploadFile(childPath);
delay(1000);
if (!isAlreadyUploaded(childPath)) {
ESP_LOGI(TAG_UPLOAD, "Found pending file: %s", childPath.c_str());
ESP_LOGI(TAG_UPLOAD, "DEBUG: Calling uploadFile()");
uploadFile(childPath);
ESP_LOGI(TAG_UPLOAD, "DEBUG: Finished uploadFile(), delaying 1000ms");
delay(1000);
}
}
f.close();
Watchdog::feed();

79
src/Uploader.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include "Uploader.h"
Uploader::Uploader(Display &display) : _display(display) {}
void Uploader::processQueue(int maxFiles) {
if (WiFi.status() != WL_CONNECTED) return;
String caCert = loadCACert("/cert.pem");
if (caCert == "") {
ESP_LOGE("UPLOADER", "Brak cert.pem na SD!");
return;
}
int sentCount = 0;
File root = SD.open("/");
// Przeszukiwanie folderów numerycznych stworzonych przez Measure.cpp
while (File folder = root.openNextFile()) {
if (sentCount >= maxFiles) break;
if (folder.isDirectory()) {
File dir = SD.open(folder.path());
while (File file = dir.openNextFile()) {
if (sentCount >= maxFiles) break;
String fileName = file.name();
if (fileName.endsWith(".wmt")) {
_display.textStatus("SSL UPLOADING...");
if (sendFile(String(file.path()), caCert)) {
String oldPath = String(file.path());
String newPath = oldPath;
newPath.replace(".wmt", ".sent");
file.close();
SD.rename(oldPath.c_str(), newPath.c_str());
sentCount++;
}
}
file.close();
}
dir.close();
}
folder.close();
}
root.close();
}
bool Uploader::sendFile(String filePath, String& caCert) {
File f = SD.open(filePath, FILE_READ);
if (!f) return false;
WiFiClientSecure client;
client.setCACert(caCert.c_str());
HTTPClient http;
bool success = false;
if (http.begin(client, "https://api.pwojtaszek.codes/upload")) {
http.addHeader("Content-Type", "application/octet-stream");
http.addHeader("X-File-Name", filePath);
http.addHeader("X-Device-ID", config.hostname);
int code = http.sendRequest("POST", &f, f.size());
if (code == 200) {
ESP_LOGI("UPLOADER", "Upload OK: %s", filePath.c_str());
success = true;
} else {
ESP_LOGE("UPLOADER", "Error: %d", code);
}
http.end();
}
f.close();
return success;
}
String Uploader::loadCACert(const char* path) {
File f = SD.open(path);
if (!f) return "";
String cert = f.readString();
f.close();
return cert;
}

View File

@@ -53,19 +53,7 @@ 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
TaskHandle_t uploadTaskHandle = NULL;
void uploadTaskCode(void *parameter) {
Watchdog::addThisTask();
while(true) {
if (config.connect && WiFi.status() == WL_CONNECTED && !testingNow) {
uploadManager.processPendingUploads();
}
Watchdog::feed();
vTaskDelay(pdMS_TO_TICKS(5000)); // wait 5 seconds before checking again
}
}
Thread uploadThread = Thread(); // Background upload
//////// PROTOTYPY /////////////////
void setup();
@@ -236,16 +224,8 @@ void setup() {
measureThread.setInterval(config.pause); // Test co X sekund
if(config.connect){
xTaskCreatePinnedToCore(
uploadTaskCode, // Funkcja zadania
"UploadTask", // Nazwa zadania
8192, // Rozmiar stosu
NULL, // Parametr
1, // Priorytet (1 - niski, domyślna pętla ma 1 na Core 1)
&uploadTaskHandle,// Uchwyt
0 // Przypięcie do rdzenia 0 (Wi-Fi działa domyślnie na 0)
);
ESP_LOGI(TAG_MAIN, "Upload task created on Core 0");
uploadThread.onRun([]() { uploadManager.processPendingUploads(); });
uploadThread.setInterval(60000); // Co 1 minutę sprawdzaj zaległe
}
// Dodanie taska loop do WDT
@@ -367,10 +347,20 @@ void measure(){
Watchdog::feed();
if(!capture.isExit){
capture.printLastFileInfoSerial();
ESP_LOGI(TAG_MAIN, "DEBUG: before readHeaderAndPrint");
capture.readHeaderAndPrint(capture.generateNextFilename().c_str());
ESP_LOGI(TAG_MAIN, "MEASURE FINISH");
} else {
ESP_LOGI(TAG_MAIN, "MEASURE INTERRUPT");
//runMeasure = false;
}
ESP_LOGI(TAG_MAIN, "DEBUG: Checking WiFi status for upload");
if (config.connect && WiFi.status() == WL_CONNECTED) {
ESP_LOGI(TAG_MAIN, "TRIGGER UPLOAD PENDING...");
ESP_LOGI(TAG_MAIN, "DEBUG: Before uploadManager.processPendingUploads()");
uploadManager.processPendingUploads();
ESP_LOGI(TAG_MAIN, "DEBUG: After uploadManager.processPendingUploads()");
}
runMeasure = false;
@@ -413,6 +403,18 @@ void loop() {
if(settings.isPressed(3)) settingsDevice(); // DOWN
if(settings.isPressed(1)) toogleMode(); // UP
if (settings.readBtnUp() && settings.readBtnOk()) {
ESP_LOGI(TAG_MAIN, "Manual AP Mode trigger");
wifi.startCaptivePortal();
display.clear();
display.textCenter(1, "AP MODE");
display.textCenter(2, "192.168.4.1");
while(settings.readBtnUp() && settings.readBtnOk()) {
Watchdog::feed();
delay(50);
}
}
if(testingNow) {
if((runMeasure) && (settings.isPressed(2))){
runMeasure = false;
@@ -424,6 +426,9 @@ void loop() {
if(wifiTestThread.shouldRun() && (config.connect))
wifiTestThread.run();
if(uploadThread.shouldRun() && config.connect)
uploadThread.run();
if(offlineThread.shouldRun() && (!config.connect)){
offlineThread.run();
}
@@ -452,6 +457,7 @@ void loop() {
reboot();
}
wifi.handleClient();
Watchdog::feed();
delay(20); // 100
licznik ++;