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