2706 lines
123 KiB
C++
2706 lines
123 KiB
C++
/*
|
||
* Byun CAN Logger with Web Interface + Serial Terminal
|
||
* Version: 3.0 - RingBuffer Optimized (SPSC Lock-Free)
|
||
*
|
||
* ★ RingBuffer 핵심 변경사항 (v2.3 → v3.0):
|
||
* - FreeRTOS Queue (xQueueSend/xQueueReceive) → SPSC Lock-Free RingBuffer
|
||
* - CAN Queue 6000개 → CAN RingBuffer 8192개 (power-of-2, 비트마스크 연산)
|
||
* - Serial/Serial2 Queue 1200개 → RingBuffer 2048개
|
||
* - Producer(canRxTask) 완전 non-blocking: 큐 full 시 drop 없이 즉시 복귀
|
||
* - Consumer(sdWriteTask) 뮤텍스 없이 BIN 버퍼 처리 (hot path 최적화)
|
||
* - dropped 카운터: 웹 UI에서 손실 메시지 수 실시간 확인
|
||
* - __sync_synchronize() 메모리 배리어: 멀티코어 안전
|
||
*
|
||
* PSRAM 최적화 완전판:
|
||
* - 원본 기능 100% 유지
|
||
* - 대용량 버퍼/RingBuffer를 PSRAM에 할당
|
||
* - 웹서버, WebSocket, 모든 Task 포함
|
||
*
|
||
* Arduino IDE 설정:
|
||
* - Board: ESP32S3 Dev Module
|
||
* - PSRAM: OPI PSRAM ⭐ 필수!
|
||
* - Flash Size: 16MB (128Mb)
|
||
* - Partition: 16MB Flash (3MB APP/9.9MB FATFS)
|
||
*/
|
||
|
||
#include <Arduino.h>
|
||
#include <SPI.h>
|
||
#include <mcp2515.h>
|
||
#include <SoftWire.h>
|
||
#include <SD_MMC.h>
|
||
#include <WiFi.h>
|
||
#include <esp_wifi.h>
|
||
#include <esp_task_wdt.h>
|
||
#include <esp_sntp.h>
|
||
#include <WebServer.h>
|
||
#include <WebSocketsServer.h>
|
||
#include <ArduinoJson.h>
|
||
#include <freertos/FreeRTOS.h>
|
||
#include <freertos/task.h>
|
||
#include <freertos/semphr.h>
|
||
#include <sys/time.h>
|
||
#include <time.h>
|
||
#include <Preferences.h>
|
||
#include "index.h"
|
||
#include "transmit.h"
|
||
#include "graph.h"
|
||
#include "graph_viewer.h"
|
||
#include "settings.h"
|
||
#include "serial_terminal.h"
|
||
#include "serial2_terminal.h"
|
||
|
||
// GPIO 핀 정의
|
||
#define CAN_INT_PIN 4
|
||
#define SERIAL_TX_PIN 17
|
||
#define SERIAL_RX_PIN 18
|
||
#define SERIAL2_TX_PIN 6
|
||
#define SERIAL2_RX_PIN 7
|
||
|
||
// HSPI 핀 (CAN)
|
||
#define HSPI_MISO 13
|
||
#define HSPI_MOSI 11
|
||
#define HSPI_SCLK 12
|
||
#define HSPI_CS 10
|
||
|
||
// SDIO 4-bit Pins
|
||
#define SDIO_CLK 39
|
||
#define SDIO_CMD 38
|
||
#define SDIO_D0 40
|
||
#define SDIO_D1 41
|
||
#define SDIO_D2 42
|
||
#define SDIO_D3 21
|
||
|
||
// I2C2 핀 (RTC DS3231)
|
||
#define RTC_SDA 8
|
||
#define RTC_SCL 9
|
||
#define DS3231_ADDRESS 0x68
|
||
|
||
// ========================================
|
||
// ★ RingBuffer 크기 설정 (반드시 2의 거듭제곱!)
|
||
// ========================================
|
||
#define CAN_RING_SIZE 8192 // 이전 Queue 6000 → 8192 (2^13, ~1초@8Kfps)
|
||
#define SERIAL_RING_SIZE 2048 // 이전 Queue 1200 → 2048 (2^11)
|
||
|
||
// 기타 버퍼 크기
|
||
#define FILE_BUFFER_SIZE 65536 // 64KB 더블버퍼 (SD BIN write)
|
||
#define SERIAL_CSV_BUFFER_SIZE 32768 // 32KB Serial CSV 버퍼
|
||
#define SERIAL2_CSV_BUFFER_SIZE 32768 // 32KB Serial2 CSV 버퍼
|
||
|
||
#define MAX_FILENAME_LEN 64
|
||
#define RECENT_MSG_COUNT 100
|
||
#define MAX_TX_MESSAGES 20
|
||
#define MAX_COMMENT_LEN 128
|
||
#define MAX_SERIAL_LINE_LEN 64
|
||
#define RTC_SYNC_INTERVAL_MS 60000
|
||
#define VOLTAGE_CHECK_INTERVAL_MS 5000
|
||
#define LOW_VOLTAGE_THRESHOLD 3.0
|
||
#define MONITORING_VOLT 5
|
||
#define MAX_GRAPH_SIGNALS 20
|
||
#define MAX_SEQUENCES 10
|
||
#define MAX_FILE_COMMENTS 50
|
||
|
||
// ========================================
|
||
// 구조체 정의
|
||
// ========================================
|
||
struct CANMessage {
|
||
uint64_t timestamp_us;
|
||
uint32_t id;
|
||
uint8_t dlc;
|
||
uint8_t flags; // bit7=EFF(29bit확장), bit6=RTR, bit5=ERR ← ★ PCAP EFF 복원용
|
||
uint8_t data[8];
|
||
} __attribute__((packed)); // 22 bytes
|
||
|
||
// ========================================
|
||
// ★ PCAP 구조체 (Wireshark 호환)
|
||
// LINKTYPE_CAN_SOCKETCAN = 227
|
||
// ========================================
|
||
struct PcapGlobalHeader {
|
||
uint32_t magic_number; // 0xa1b2c3d4
|
||
uint16_t version_major; // 2
|
||
uint16_t version_minor; // 4
|
||
int32_t thiszone; // 0
|
||
uint32_t sigfigs; // 0
|
||
uint32_t snaplen; // 65535
|
||
uint32_t network; // 227 = LINKTYPE_CAN_SOCKETCAN
|
||
} __attribute__((packed)); // 24 bytes
|
||
|
||
struct PcapPacketHeader {
|
||
uint32_t ts_sec; // 타임스탬프 초
|
||
uint32_t ts_usec; // 타임스탬프 마이크로초
|
||
uint32_t incl_len; // 캡처 길이 (16)
|
||
uint32_t orig_len; // 원본 길이 (16)
|
||
} __attribute__((packed)); // 16 bytes
|
||
|
||
struct SocketCANFrame {
|
||
uint32_t can_id; // CAN ID (bit31=EFF, bit30=RTR)
|
||
uint8_t can_dlc; // 데이터 길이
|
||
uint8_t pad;
|
||
uint8_t res0;
|
||
uint8_t res1;
|
||
uint8_t data[8];
|
||
} __attribute__((packed)); // 16 bytes
|
||
|
||
// PCAP 패킷 1개 = PcapPacketHeader(16) + SocketCANFrame(16) = 32 bytes
|
||
struct PcapRecord {
|
||
PcapPacketHeader hdr;
|
||
SocketCANFrame frame;
|
||
} __attribute__((packed)); // 32 bytes
|
||
|
||
// ★ 컴파일 타임 크기 검증 (틀리면 빌드 에러로 즉시 알 수 있음)
|
||
static_assert(sizeof(PcapGlobalHeader) == 24, "PcapGlobalHeader must be 24 bytes");
|
||
static_assert(sizeof(PcapPacketHeader) == 16, "PcapPacketHeader must be 16 bytes");
|
||
static_assert(sizeof(SocketCANFrame) == 16, "SocketCANFrame must be 16 bytes");
|
||
static_assert(sizeof(PcapRecord) == 32, "PcapRecord must be 32 bytes");
|
||
|
||
struct SerialMessage {
|
||
uint64_t timestamp_us;
|
||
uint16_t length;
|
||
uint8_t data[MAX_SERIAL_LINE_LEN];
|
||
bool isTx;
|
||
} __attribute__((packed));
|
||
|
||
struct SerialSettings {
|
||
uint32_t baudRate;
|
||
uint8_t dataBits;
|
||
uint8_t parity;
|
||
uint8_t stopBits;
|
||
};
|
||
|
||
struct RecentCANData {
|
||
CANMessage msg;
|
||
uint32_t count;
|
||
};
|
||
|
||
struct TxMessage {
|
||
uint32_t id;
|
||
bool extended;
|
||
uint8_t dlc;
|
||
uint8_t data[8];
|
||
uint32_t interval;
|
||
uint32_t lastSent;
|
||
bool active;
|
||
};
|
||
|
||
struct SequenceStep {
|
||
uint32_t canId;
|
||
bool extended;
|
||
uint8_t dlc;
|
||
uint8_t data[8];
|
||
uint32_t delayMs;
|
||
};
|
||
|
||
struct CANSequence {
|
||
char name[32];
|
||
SequenceStep steps[20];
|
||
uint8_t stepCount;
|
||
uint8_t repeatMode;
|
||
uint32_t repeatCount;
|
||
};
|
||
|
||
struct SequenceRuntime {
|
||
bool running;
|
||
uint8_t currentStep;
|
||
uint32_t currentRepeat;
|
||
uint32_t lastStepTime;
|
||
int8_t activeSequenceIndex;
|
||
};
|
||
|
||
struct FileComment {
|
||
char filename[MAX_FILENAME_LEN];
|
||
char comment[MAX_COMMENT_LEN];
|
||
};
|
||
|
||
// ★ Arduino IDE auto-prototype 오류 방지: TriggerCondition을 상단에 정의
|
||
struct TriggerCondition {
|
||
uint32_t canId;
|
||
uint8_t startBit;
|
||
uint8_t bitLength;
|
||
char op[3];
|
||
int64_t value;
|
||
bool enabled;
|
||
};
|
||
|
||
struct TimeSyncStatus {
|
||
bool synchronized;
|
||
uint64_t lastSyncTime;
|
||
int32_t offsetUs;
|
||
uint32_t syncCount;
|
||
bool rtcAvailable;
|
||
uint32_t rtcSyncCount;
|
||
} timeSyncStatus = {false, 0, 0, 0, false, 0};
|
||
|
||
struct PowerStatus {
|
||
float voltage;
|
||
float minVoltage;
|
||
bool lowVoltage;
|
||
uint32_t lastCheck;
|
||
uint32_t lastMinReset;
|
||
} powerStatus = {0.0, 999.9, false, 0, 0};
|
||
|
||
enum MCP2515Mode {
|
||
MCP_MODE_NORMAL = 0,
|
||
MCP_MODE_LISTEN_ONLY = 1,
|
||
MCP_MODE_LOOPBACK = 2,
|
||
MCP_MODE_TRANSMIT = 3
|
||
};
|
||
|
||
// ========================================
|
||
// ★★★ SPSC Lock-Free RingBuffer 정의 ★★★
|
||
//
|
||
// 설계 원칙:
|
||
// - head: Producer만 쓰기 (canRxTask / serialRxTask)
|
||
// - tail: Consumer만 쓰기 (sdWriteTask / webUpdateTask)
|
||
// - __sync_synchronize(): 멀티코어 메모리 배리어
|
||
// - 2의 거듭제곱 size + bitmask: 나눗셈 연산 제거
|
||
// - dropped: 오버플로우 시 drop 카운트 (진단용)
|
||
// ========================================
|
||
|
||
struct CANRingBuffer {
|
||
volatile uint32_t head; // Producer write index (절대값, 증가만)
|
||
volatile uint32_t tail; // Consumer read index (절대값, 증가만)
|
||
uint32_t size; // 버퍼 용량 (반드시 2의 거듭제곱)
|
||
uint32_t mask; // size - 1 (비트마스크 인덱싱용)
|
||
CANMessage* buf; // PSRAM 할당 데이터 배열
|
||
volatile uint32_t dropped; // 버퍼 full 시 drop된 메시지 수
|
||
};
|
||
|
||
struct SerialRingBuffer {
|
||
volatile uint32_t head;
|
||
volatile uint32_t tail;
|
||
uint32_t size;
|
||
uint32_t mask;
|
||
SerialMessage* buf;
|
||
volatile uint32_t dropped;
|
||
};
|
||
|
||
// RingBuffer 전역 인스턴스
|
||
CANRingBuffer canRing;
|
||
SerialRingBuffer serialRing;
|
||
SerialRingBuffer serial2Ring;
|
||
|
||
// ── Producer Push (IRAM에 배치: ISR 경로 최적화) ──────────────────────────
|
||
// 반환: true=성공, false=버퍼 full(drop)
|
||
static inline bool IRAM_ATTR ring_can_push(CANRingBuffer* rb, const CANMessage* msg) {
|
||
uint32_t h = rb->head;
|
||
if ((h - rb->tail) >= rb->size) { // 버퍼 full 체크
|
||
rb->dropped++;
|
||
return false; // drop: 손실 카운트 후 즉시 반환
|
||
}
|
||
// ★ packed struct(22bytes) → 비정렬 PSRAM 주소에 안전하게 memcpy
|
||
memcpy(&rb->buf[h & rb->mask], msg, sizeof(CANMessage));
|
||
__sync_synchronize(); // ★ 메모리 배리어: buf 쓰기 완료 후 head 갱신
|
||
rb->head = h + 1;
|
||
return true;
|
||
}
|
||
|
||
// ── Consumer Pop ──────────────────────────────────────────────────────────
|
||
static inline bool ring_can_pop(CANRingBuffer* rb, CANMessage* msg) {
|
||
uint32_t t = rb->tail;
|
||
if (rb->head == t) return false; // 빈 버퍼
|
||
// ★ packed struct(22bytes) → 비정렬 PSRAM 주소에서 안전하게 memcpy
|
||
memcpy(msg, &rb->buf[t & rb->mask], sizeof(CANMessage));
|
||
__sync_synchronize(); // ★ 메모리 배리어: 데이터 읽기 완료 후 tail 갱신
|
||
rb->tail = t + 1;
|
||
return true;
|
||
}
|
||
|
||
// ── 현재 사용 중인 슬롯 수 ────────────────────────────────────────────────
|
||
static inline uint32_t ring_can_count(const CANRingBuffer* rb) {
|
||
return rb->head - rb->tail; // uint32_t 언더플로우 자동 처리
|
||
}
|
||
|
||
// ── Serial RingBuffer (동일한 SPSC 패턴) ─────────────────────────────────
|
||
static inline bool ring_serial_push(SerialRingBuffer* rb, const SerialMessage* msg) {
|
||
uint32_t h = rb->head;
|
||
if ((h - rb->tail) >= rb->size) {
|
||
rb->dropped++;
|
||
return false;
|
||
}
|
||
rb->buf[h & rb->mask] = *msg;
|
||
__sync_synchronize();
|
||
rb->head = h + 1;
|
||
return true;
|
||
}
|
||
|
||
static inline bool ring_serial_pop(SerialRingBuffer* rb, SerialMessage* msg) {
|
||
uint32_t t = rb->tail;
|
||
if (rb->head == t) return false;
|
||
*msg = rb->buf[t & rb->mask];
|
||
__sync_synchronize();
|
||
rb->tail = t + 1;
|
||
return true;
|
||
}
|
||
|
||
static inline uint32_t ring_serial_count(const SerialRingBuffer* rb) {
|
||
return rb->head - rb->tail;
|
||
}
|
||
|
||
// ── RingBuffer 초기화/클리어 ─────────────────────────────────────────────
|
||
static inline void ring_can_clear(CANRingBuffer* rb) {
|
||
rb->tail = rb->head; // head == tail → 빈 상태
|
||
rb->dropped = 0;
|
||
}
|
||
static inline void ring_serial_clear(SerialRingBuffer* rb) {
|
||
rb->tail = rb->head;
|
||
rb->dropped = 0;
|
||
}
|
||
|
||
// ========================================
|
||
// PSRAM 할당 변수
|
||
// ========================================
|
||
char *serialCsvBuffer = nullptr;
|
||
char *serial2CsvBuffer = nullptr;
|
||
RecentCANData *recentData = nullptr;
|
||
TxMessage *txMessages = nullptr;
|
||
CANSequence *sequences = nullptr;
|
||
FileComment *fileComments = nullptr;
|
||
|
||
// WiFi 설정
|
||
char wifiSSID[32] = "Byun_CAN_Logger";
|
||
char wifiPassword[64]= "12345678";
|
||
bool enableSTAMode = false;
|
||
char staSSID[32] = "";
|
||
char staPassword[64] = "";
|
||
|
||
// Serial 설정
|
||
SerialSettings serialSettings = {115200, 8, 0, 1};
|
||
SerialSettings serial2Settings = {115200, 8, 0, 1};
|
||
|
||
// 전역 객체
|
||
SPIClass hspi(HSPI);
|
||
|
||
#define MCP_CRYSTAL MCP_16MHZ
|
||
MCP2515 mcp2515(HSPI_CS, 40000000, &hspi);
|
||
HardwareSerial SerialComm(1);
|
||
HardwareSerial Serial2Comm(2);
|
||
WebServer server(80);
|
||
WebSocketsServer webSocket = WebSocketsServer(81);
|
||
Preferences preferences;
|
||
|
||
// FreeRTOS 핸들 (Mutex / Task만 유지, Queue 핸들 제거됨)
|
||
SemaphoreHandle_t sdMutex = NULL;
|
||
SemaphoreHandle_t rtcMutex = NULL;
|
||
SemaphoreHandle_t serialMutex = NULL;
|
||
SemaphoreHandle_t serial2Mutex = NULL;
|
||
TaskHandle_t canRxTaskHandle = NULL;
|
||
TaskHandle_t sdWriteTaskHandle = NULL;
|
||
TaskHandle_t webTaskHandle = NULL;
|
||
TaskHandle_t rtcTaskHandle = NULL;
|
||
TaskHandle_t serialRxTaskHandle = NULL;
|
||
TaskHandle_t serial2RxTaskHandle= NULL;
|
||
|
||
// 로깅 상태
|
||
volatile bool loggingEnabled = false;
|
||
volatile bool serialLoggingEnabled = false;
|
||
volatile bool serial2LoggingEnabled = false;
|
||
volatile bool sdCardReady = false;
|
||
|
||
// Auto Trigger
|
||
#define MAX_TRIGGERS 8
|
||
TriggerCondition startTriggers[MAX_TRIGGERS];
|
||
TriggerCondition stopTriggers[MAX_TRIGGERS];
|
||
int startTriggerCount = 0;
|
||
int stopTriggerCount = 0;
|
||
|
||
String startFormula = "";
|
||
String stopFormula = "";
|
||
|
||
bool autoTriggerEnabled = false;
|
||
char startLogicOp[4] = "OR";
|
||
char stopLogicOp[4] = "OR";
|
||
bool autoTriggerActive = false;
|
||
bool autoTriggerLogCSV = false;
|
||
bool autoTriggerLogPCAP = false; // ★ PCAP 추가
|
||
bool savedCanLogFormatCSV= false;
|
||
bool savedCanLogFormatPCAP=false; // ★ PCAP 추가
|
||
|
||
// 파일 핸들
|
||
File logFile;
|
||
File serialLogFile;
|
||
File serial2LogFile;
|
||
char currentFilename[MAX_FILENAME_LEN];
|
||
char currentSerialFilename[MAX_FILENAME_LEN];
|
||
char currentSerial2Filename[MAX_FILENAME_LEN];
|
||
|
||
// ★ 더블 버퍼링 (SD BIN write 전용)
|
||
uint8_t* fileBuffer1 = NULL;
|
||
uint8_t* fileBuffer2 = NULL;
|
||
uint8_t* currentWriteBuffer = NULL;
|
||
uint8_t* currentFlushBuffer = NULL;
|
||
uint32_t writeBufferIndex = 0;
|
||
uint32_t flushBufferSize = 0;
|
||
volatile bool flushInProgress = false;
|
||
TaskHandle_t sdFlushTaskHandle = NULL;
|
||
|
||
uint32_t serialCsvIndex = 0;
|
||
uint32_t serial2CsvIndex = 0;
|
||
volatile uint32_t currentFileSize = 0;
|
||
volatile uint32_t currentSerialFileSize = 0;
|
||
volatile uint32_t currentSerial2FileSize= 0;
|
||
volatile bool canLogFormatCSV = false;
|
||
volatile bool canLogFormatPCAP = false; // ★ PCAP 추가
|
||
uint32_t pcapDbgCount = 0; // ★ PCAP 시리얼 디버그 카운터
|
||
volatile bool serialLogFormatCSV = true;
|
||
volatile bool serial2LogFormatCSV = true;
|
||
volatile uint64_t canLogStartTime = 0;
|
||
volatile uint64_t serialLogStartTime = 0;
|
||
volatile uint64_t serial2LogStartTime = 0;
|
||
|
||
// 통계
|
||
MCP2515Mode currentMcpMode;
|
||
SoftWire rtcWire(RTC_SDA, RTC_SCL);
|
||
char rtcSyncBuffer[20];
|
||
CAN_SPEED currentCanSpeed = CAN_1000KBPS;
|
||
const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"};
|
||
CAN_SPEED canSpeedValues[]= {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS};
|
||
|
||
volatile uint32_t totalMsgCount = 0; // Core 1 write / Core 0 read → volatile 필수
|
||
volatile uint32_t msgPerSecond = 0; // Core 0 webUpdateTask에서 계산
|
||
uint32_t lastMsgCountTime= 0;
|
||
uint32_t lastMsgCount = 0;
|
||
volatile uint32_t totalSerialRxCount = 0;
|
||
volatile uint32_t totalSerialTxCount = 0;
|
||
volatile uint32_t totalSerial2RxCount = 0;
|
||
volatile uint32_t totalSerial2TxCount = 0;
|
||
uint32_t totalTxCount = 0;
|
||
uint8_t sequenceCount = 0;
|
||
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
|
||
int commentCount = 0;
|
||
|
||
// Forward declarations
|
||
void IRAM_ATTR canISR(); // 초경량 ISR 전방선언
|
||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
|
||
void resetMCP2515();
|
||
|
||
// ========================================
|
||
// MCP2515 리셋
|
||
// ========================================
|
||
void resetMCP2515() {
|
||
Serial.println("🔄 MCP2515 리셋 시작...");
|
||
|
||
if (loggingEnabled) { /* 필요시 버퍼 플러시 */ }
|
||
|
||
// ★ 큐 대신 RingBuffer 클리어
|
||
ring_can_clear(&canRing);
|
||
|
||
Serial.println(" 1. 하드웨어 리셋...");
|
||
digitalWrite(HSPI_CS, LOW);
|
||
delayMicroseconds(100);
|
||
digitalWrite(HSPI_CS, HIGH);
|
||
delay(100);
|
||
|
||
Serial.println(" 2. 소프트웨어 리셋...");
|
||
mcp2515.reset();
|
||
delay(100);
|
||
|
||
preferences.begin("can-logger", true);
|
||
int speedIndex = preferences.getInt("can_speed", 3);
|
||
if (speedIndex >= 0 && speedIndex < 4) currentCanSpeed = canSpeedValues[speedIndex];
|
||
int savedMode = preferences.getInt("mcp_mode", 1);
|
||
if (savedMode >= 0 && savedMode <= 3) currentMcpMode = (MCP2515Mode)savedMode;
|
||
preferences.end();
|
||
|
||
mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL);
|
||
delay(10);
|
||
|
||
mcp2515.setFilterMask(MCP2515::MASK0, false, 0x00000000);
|
||
mcp2515.setFilter(MCP2515::RXF0, false, 0x00000000);
|
||
mcp2515.setFilter(MCP2515::RXF1, false, 0x00000000);
|
||
mcp2515.setFilterMask(MCP2515::MASK1, true, 0x00000000);
|
||
mcp2515.setFilter(MCP2515::RXF2, true, 0x00000000);
|
||
mcp2515.setFilter(MCP2515::RXF3, true, 0x00000000);
|
||
mcp2515.setFilter(MCP2515::RXF4, true, 0x00000000);
|
||
mcp2515.setFilter(MCP2515::RXF5, true, 0x00000000);
|
||
delay(10);
|
||
|
||
if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode();
|
||
else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode();
|
||
else mcp2515.setListenOnlyMode();
|
||
delay(50);
|
||
|
||
struct can_frame dummyFrame;
|
||
int clearCount = 0;
|
||
while (mcp2515.readMessage(&dummyFrame) == MCP2515::ERROR_OK) {
|
||
if (clearCount++ > 100) break;
|
||
}
|
||
|
||
mcp2515.clearRXnOVRFlags();
|
||
mcp2515.clearInterrupts();
|
||
mcp2515.clearTXInterrupts();
|
||
mcp2515.clearMERR();
|
||
mcp2515.clearERRIF();
|
||
|
||
uint8_t errorFlag = mcp2515.getErrorFlags();
|
||
uint8_t txErr = mcp2515.errorCountTX();
|
||
uint8_t rxErr = mcp2515.errorCountRX();
|
||
Serial.printf(" 에러 상태: EFLG=0x%02X, TEC=%d, REC=%d\n", errorFlag, txErr, rxErr);
|
||
|
||
if (errorFlag != 0 || txErr > 0 || rxErr > 0) {
|
||
mcp2515.reset(); delay(50);
|
||
mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); delay(10);
|
||
if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode();
|
||
else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode();
|
||
else mcp2515.setListenOnlyMode();
|
||
delay(50);
|
||
mcp2515.clearRXnOVRFlags();
|
||
mcp2515.clearInterrupts();
|
||
}
|
||
|
||
totalMsgCount = 0;
|
||
lastMsgCount = 0;
|
||
msgPerSecond = 0;
|
||
|
||
for (int i = 0; i < RECENT_MSG_COUNT; i++) recentData[i].count = 0;
|
||
|
||
Serial.println("✅ MCP2515 리셋 완료!");
|
||
}
|
||
|
||
// ========================================
|
||
// PSRAM 초기화 (RingBuffer 버전)
|
||
// ========================================
|
||
bool initPSRAM() {
|
||
Serial.println("\n========================================");
|
||
Serial.println(" PSRAM 메모리 할당 (RingBuffer 버전)");
|
||
Serial.println("========================================");
|
||
|
||
if (!psramFound()) {
|
||
Serial.println("✗ PSRAM을 찾을 수 없습니다!");
|
||
return false;
|
||
}
|
||
Serial.printf("✓ PSRAM 총 용량: %d MB\n", ESP.getPsramSize() / 1024 / 1024);
|
||
Serial.printf("✓ PSRAM 여유: %d KB\n\n", ESP.getFreePsram() / 1024);
|
||
|
||
// ── 더블 버퍼 (SD BIN write) ──────────────────────────────
|
||
fileBuffer1 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE);
|
||
fileBuffer2 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE);
|
||
if (!fileBuffer1 || !fileBuffer2) { Serial.println("✗ fileBuffer 할당 실패"); return false; }
|
||
currentWriteBuffer = fileBuffer1;
|
||
currentFlushBuffer = fileBuffer2;
|
||
writeBufferIndex = 0;
|
||
flushBufferSize = 0;
|
||
Serial.printf("✓ Double Buffer: 2 × %d KB\n", FILE_BUFFER_SIZE / 1024);
|
||
|
||
// ── Serial CSV 버퍼 ────────────────────────────────────────
|
||
serialCsvBuffer = (char*)ps_malloc(SERIAL_CSV_BUFFER_SIZE);
|
||
serial2CsvBuffer = (char*)ps_malloc(SERIAL2_CSV_BUFFER_SIZE);
|
||
if (!serialCsvBuffer || !serial2CsvBuffer) { Serial.println("✗ SerialCsvBuffer 할당 실패"); return false; }
|
||
Serial.printf("✓ Serial CSV Buffer: 2 × %d KB\n", SERIAL_CSV_BUFFER_SIZE / 1024);
|
||
|
||
// ── 기타 PSRAM 데이터 ────────────────────────────────────
|
||
recentData = (RecentCANData*)ps_calloc(RECENT_MSG_COUNT, sizeof(RecentCANData));
|
||
txMessages = (TxMessage*) ps_calloc(MAX_TX_MESSAGES, sizeof(TxMessage));
|
||
sequences = (CANSequence*) ps_calloc(MAX_SEQUENCES, sizeof(CANSequence));
|
||
fileComments = (FileComment*) ps_calloc(MAX_FILE_COMMENTS,sizeof(FileComment));
|
||
if (!recentData || !txMessages || !sequences || !fileComments) {
|
||
Serial.println("✗ 데이터 버퍼 할당 실패"); return false;
|
||
}
|
||
|
||
// ========================================
|
||
// ★★★ RingBuffer 할당 (FreeRTOS Queue 대체) ★★★
|
||
// ========================================
|
||
Serial.println("\n📦 RingBuffer 할당 (Lock-Free SPSC)...");
|
||
|
||
// CAN RingBuffer
|
||
canRing.buf = (CANMessage*)ps_malloc(CAN_RING_SIZE * sizeof(CANMessage));
|
||
if (!canRing.buf) { Serial.println("✗ CAN RingBuffer 할당 실패"); return false; }
|
||
canRing.size = CAN_RING_SIZE;
|
||
canRing.mask = CAN_RING_SIZE - 1;
|
||
canRing.head = 0;
|
||
canRing.tail = 0;
|
||
canRing.dropped = 0;
|
||
Serial.printf("✓ CAN RingBuffer: %d 슬롯 × %d bytes = %.2f KB\n",
|
||
CAN_RING_SIZE, sizeof(CANMessage),
|
||
(float)(CAN_RING_SIZE * sizeof(CANMessage)) / 1024.0f);
|
||
|
||
// Serial RingBuffer
|
||
serialRing.buf = (SerialMessage*)ps_malloc(SERIAL_RING_SIZE * sizeof(SerialMessage));
|
||
if (!serialRing.buf) { Serial.println("✗ Serial RingBuffer 할당 실패"); return false; }
|
||
serialRing.size = SERIAL_RING_SIZE;
|
||
serialRing.mask = SERIAL_RING_SIZE - 1;
|
||
serialRing.head = 0;
|
||
serialRing.tail = 0;
|
||
serialRing.dropped = 0;
|
||
Serial.printf("✓ SER RingBuffer: %d 슬롯 × %d bytes = %.2f KB\n",
|
||
SERIAL_RING_SIZE, sizeof(SerialMessage),
|
||
(float)(SERIAL_RING_SIZE * sizeof(SerialMessage)) / 1024.0f);
|
||
|
||
// Serial2 RingBuffer
|
||
serial2Ring.buf = (SerialMessage*)ps_malloc(SERIAL_RING_SIZE * sizeof(SerialMessage));
|
||
if (!serial2Ring.buf) { Serial.println("✗ Serial2 RingBuffer 할당 실패"); return false; }
|
||
serial2Ring.size = SERIAL_RING_SIZE;
|
||
serial2Ring.mask = SERIAL_RING_SIZE - 1;
|
||
serial2Ring.head = 0;
|
||
serial2Ring.tail = 0;
|
||
serial2Ring.dropped = 0;
|
||
Serial.printf("✓ SER2 RingBuffer: %d 슬롯 × %d bytes = %.2f KB\n",
|
||
SERIAL_RING_SIZE, sizeof(SerialMessage),
|
||
(float)(SERIAL_RING_SIZE * sizeof(SerialMessage)) / 1024.0f);
|
||
|
||
Serial.println("========================================");
|
||
Serial.printf("✓ PSRAM 남은 용량: %.2f KB\n", (float)ESP.getFreePsram() / 1024.0f);
|
||
Serial.println("========================================\n");
|
||
return true;
|
||
}
|
||
|
||
// ========================================
|
||
// 설정 저장/로드
|
||
// ========================================
|
||
void loadSerialSettings() {
|
||
serialSettings.baudRate = preferences.getUInt("ser_baud", 115200);
|
||
serialSettings.dataBits = preferences.getUChar("ser_data", 8);
|
||
serialSettings.parity = preferences.getUChar("ser_parity", 0);
|
||
serialSettings.stopBits = preferences.getUChar("ser_stop", 1);
|
||
serial2Settings.baudRate = preferences.getUInt("ser2_baud", 115200);
|
||
serial2Settings.dataBits = preferences.getUChar("ser2_data", 8);
|
||
serial2Settings.parity = preferences.getUChar("ser2_parity", 0);
|
||
serial2Settings.stopBits = preferences.getUChar("ser2_stop", 1);
|
||
}
|
||
|
||
void saveSerialSettings() {
|
||
preferences.putUInt("ser_baud", serialSettings.baudRate);
|
||
preferences.putUChar("ser_data", serialSettings.dataBits);
|
||
preferences.putUChar("ser_parity", serialSettings.parity);
|
||
preferences.putUChar("ser_stop", serialSettings.stopBits);
|
||
preferences.putUInt("ser2_baud", serial2Settings.baudRate);
|
||
preferences.putUChar("ser2_data", serial2Settings.dataBits);
|
||
preferences.putUChar("ser2_parity", serial2Settings.parity);
|
||
preferences.putUChar("ser2_stop", serial2Settings.stopBits);
|
||
}
|
||
|
||
void applySerialSettings() {
|
||
uint32_t config = SERIAL_8N1;
|
||
if (serialSettings.dataBits == 5) {
|
||
if (serialSettings.parity == 0) config = SERIAL_5N1;
|
||
else if (serialSettings.parity == 1) config = SERIAL_5E1;
|
||
else config = SERIAL_5O1;
|
||
} else if (serialSettings.dataBits == 6) {
|
||
if (serialSettings.parity == 0) config = SERIAL_6N1;
|
||
else if (serialSettings.parity == 1) config = SERIAL_6E1;
|
||
else config = SERIAL_6O1;
|
||
} else if (serialSettings.dataBits == 7) {
|
||
if (serialSettings.parity == 0) config = SERIAL_7N1;
|
||
else if (serialSettings.parity == 1) config = SERIAL_7E1;
|
||
else config = SERIAL_7O1;
|
||
} else {
|
||
if (serialSettings.parity == 0) config = SERIAL_8N1;
|
||
else if (serialSettings.parity == 1) config = SERIAL_8E1;
|
||
else config = SERIAL_8O1;
|
||
}
|
||
if (serialSettings.stopBits == 2) config |= 0x3000;
|
||
SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN);
|
||
SerialComm.setRxBufferSize(2048);
|
||
|
||
uint32_t config2 = SERIAL_8N1;
|
||
if (serial2Settings.dataBits == 5) {
|
||
if (serial2Settings.parity == 0) config2 = SERIAL_5N1;
|
||
else if (serial2Settings.parity == 1) config2 = SERIAL_5E1;
|
||
else config2 = SERIAL_5O1;
|
||
} else if (serial2Settings.dataBits == 6) {
|
||
if (serial2Settings.parity == 0) config2 = SERIAL_6N1;
|
||
else if (serial2Settings.parity == 1) config2 = SERIAL_6E1;
|
||
else config2 = SERIAL_6O1;
|
||
} else if (serial2Settings.dataBits == 7) {
|
||
if (serial2Settings.parity == 0) config2 = SERIAL_7N1;
|
||
else if (serial2Settings.parity == 1) config2 = SERIAL_7E1;
|
||
else config2 = SERIAL_7O1;
|
||
} else {
|
||
if (serial2Settings.parity == 0) config2 = SERIAL_8N1;
|
||
else if (serial2Settings.parity == 1) config2 = SERIAL_8E1;
|
||
else config2 = SERIAL_8O1;
|
||
}
|
||
if (serial2Settings.stopBits == 2) config2 |= 0x3000;
|
||
Serial2Comm.begin(serial2Settings.baudRate, config2, SERIAL2_RX_PIN, SERIAL2_TX_PIN);
|
||
Serial2Comm.setRxBufferSize(2048);
|
||
}
|
||
|
||
// ========================================
|
||
// ★ PCAP 전역 헤더 쓰기 (파일 오픈 직후 1회 호출)
|
||
// ========================================
|
||
bool writePcapGlobalHeader(File &f) {
|
||
PcapGlobalHeader gh;
|
||
gh.magic_number = 0xa1b2c3d4;
|
||
gh.version_major = 2;
|
||
gh.version_minor = 4;
|
||
gh.thiszone = 0;
|
||
gh.sigfigs = 0;
|
||
gh.snaplen = 65535;
|
||
gh.network = 227; // LINKTYPE_CAN_SOCKETCAN
|
||
return f.write((uint8_t*)&gh, sizeof(gh)) == sizeof(gh);
|
||
}
|
||
|
||
// ========================================
|
||
void loadSettings() {
|
||
preferences.begin("can-logger", false);
|
||
preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID));
|
||
preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword));
|
||
enableSTAMode = preferences.getBool("sta_enable", false);
|
||
preferences.getString("sta_ssid", staSSID, sizeof(staSSID));
|
||
preferences.getString("sta_pass", staPassword, sizeof(staPassword));
|
||
if (strlen(wifiSSID) == 0) strcpy(wifiSSID, "Byun_CAN_Logger");
|
||
if (strlen(wifiPassword)== 0) strcpy(wifiPassword,"12345678");
|
||
int speedIndex = preferences.getInt("can_speed", 3);
|
||
if (speedIndex >= 0 && speedIndex < 4) currentCanSpeed = canSpeedValues[speedIndex];
|
||
int savedMode = preferences.getInt("mcp_mode", 1);
|
||
if (savedMode >= 0 && savedMode <= 3) currentMcpMode = (MCP2515Mode)savedMode;
|
||
savedCanLogFormatCSV = preferences.getBool("can_format_csv", false);
|
||
savedCanLogFormatPCAP = preferences.getBool("can_format_pcap", false);
|
||
// 둘 다 false면 BIN이 기본값
|
||
loadSerialSettings();
|
||
preferences.end();
|
||
}
|
||
|
||
void saveSettings() {
|
||
preferences.begin("can-logger", false);
|
||
preferences.putString("wifi_ssid", wifiSSID);
|
||
preferences.putString("wifi_pass", wifiPassword);
|
||
preferences.putBool("sta_enable", enableSTAMode);
|
||
preferences.putString("sta_ssid", staSSID);
|
||
preferences.putString("sta_pass", staPassword);
|
||
for (int i = 0; i < 4; i++) {
|
||
if (canSpeedValues[i] == currentCanSpeed) { preferences.putInt("can_speed", i); break; }
|
||
}
|
||
preferences.putInt("mcp_mode", (int)currentMcpMode);
|
||
preferences.putBool("can_format_csv", savedCanLogFormatCSV);
|
||
preferences.putBool("can_format_pcap", savedCanLogFormatPCAP);
|
||
saveSerialSettings();
|
||
preferences.end();
|
||
}
|
||
|
||
// ========================================
|
||
// Auto Trigger
|
||
// ========================================
|
||
int64_t extractBits(uint8_t *data, uint8_t startBit, uint8_t bitLength) {
|
||
if (bitLength == 0 || bitLength > 64 || startBit >= 64) return 0;
|
||
int64_t result = 0;
|
||
for (int i = 0; i < bitLength; i++) {
|
||
uint8_t bitPos = startBit + i;
|
||
uint8_t byteIdx = bitPos / 8;
|
||
uint8_t bitIdx = 7 - (bitPos % 8);
|
||
if (byteIdx < 8 && (data[byteIdx] & (1 << bitIdx)))
|
||
result |= (1LL << (bitLength - 1 - i));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
bool checkCondition(TriggerCondition &trigger, uint8_t *data) {
|
||
if (!trigger.enabled) return false;
|
||
int64_t v = extractBits(data, trigger.startBit, trigger.bitLength);
|
||
if (strcmp(trigger.op, "==") == 0) return v == trigger.value;
|
||
if (strcmp(trigger.op, "!=") == 0) return v != trigger.value;
|
||
if (strcmp(trigger.op, ">") == 0) return v > trigger.value;
|
||
if (strcmp(trigger.op, "<") == 0) return v < trigger.value;
|
||
if (strcmp(trigger.op, ">=") == 0) return v >= trigger.value;
|
||
if (strcmp(trigger.op, "<=") == 0) return v <= trigger.value;
|
||
return false;
|
||
}
|
||
|
||
void checkAutoTriggers(struct can_frame &frame) {
|
||
if (!autoTriggerEnabled || !sdCardReady) return;
|
||
|
||
// Start 조건
|
||
if (!loggingEnabled && startTriggerCount > 0) {
|
||
bool result = (strcmp(startLogicOp, "AND") == 0);
|
||
bool anyMatch = false;
|
||
for (int i = 0; i < startTriggerCount; i++) {
|
||
if (startTriggers[i].canId == frame.can_id && startTriggers[i].enabled) {
|
||
bool match = checkCondition(startTriggers[i], frame.data);
|
||
anyMatch = true;
|
||
if (strcmp(startLogicOp, "AND") == 0) {
|
||
result = result && match; if (!match) break;
|
||
} else { if (match) { result = true; break; } }
|
||
}
|
||
}
|
||
if (anyMatch && result) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
time_t now; struct tm timeinfo;
|
||
time(&now); localtime_r(&now, &timeinfo);
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
canLogFormatCSV = autoTriggerLogCSV;
|
||
canLogFormatPCAP = autoTriggerLogPCAP;
|
||
const char* ext;
|
||
if (canLogFormatPCAP) ext = "pcap";
|
||
else if (canLogFormatCSV) ext = "csv";
|
||
else ext = "bin";
|
||
snprintf(currentFilename, sizeof(currentFilename),
|
||
"/CAN_%04d%02d%02d_%02d%02d%02d.%s",
|
||
timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday,
|
||
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext);
|
||
logFile = SD_MMC.open(currentFilename, FILE_WRITE);
|
||
if (logFile) {
|
||
if (canLogFormatCSV) { logFile.println("Time_us,CAN_ID,DLC,Data"); logFile.flush(); }
|
||
else if (canLogFormatPCAP) { writePcapGlobalHeader(logFile); logFile.flush(); }
|
||
logFile.close();
|
||
logFile = SD_MMC.open(currentFilename, FILE_APPEND);
|
||
if (logFile) {
|
||
loggingEnabled = true;
|
||
autoTriggerActive = true;
|
||
writeBufferIndex = 0;
|
||
flushBufferSize = 0;
|
||
flushInProgress = false;
|
||
currentFileSize = logFile.size();
|
||
pcapDbgCount = 0; // ★ PCAP 디버그 카운터 리셋
|
||
Serial.printf("🎯 Auto Trigger 시작: %s\n", currentFilename);
|
||
}
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Stop 조건
|
||
if (loggingEnabled && stopTriggerCount > 0) {
|
||
bool result = (strcmp(stopLogicOp, "AND") == 0);
|
||
bool anyMatch = false;
|
||
for (int i = 0; i < stopTriggerCount; i++) {
|
||
if (stopTriggers[i].canId == frame.can_id && stopTriggers[i].enabled) {
|
||
bool match = checkCondition(stopTriggers[i], frame.data);
|
||
anyMatch = true;
|
||
if (strcmp(stopLogicOp, "AND") == 0) {
|
||
result = result && match; if (!match) break;
|
||
} else { if (match) { result = true; break; } }
|
||
}
|
||
}
|
||
if (anyMatch && result) {
|
||
loggingEnabled = false; // 먼저 중단 → sdFlushTask 새 flush 안 함
|
||
int waitCount = 0;
|
||
while (flushInProgress && waitCount++ < 500) vTaskDelay(10);
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) {
|
||
if (writeBufferIndex > 0 && logFile) {
|
||
logFile.write(currentWriteBuffer, writeBufferIndex);
|
||
writeBufferIndex = 0;
|
||
}
|
||
if (logFile) { logFile.flush(); logFile.close(); }
|
||
autoTriggerActive = false;
|
||
currentFilename[0]= '\0';
|
||
flushBufferSize = 0;
|
||
Serial.println("🎯 Auto Trigger 중지");
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void saveAutoTriggerSettings() {
|
||
preferences.begin("autotrigger", false);
|
||
preferences.putBool("enabled", autoTriggerEnabled);
|
||
preferences.putBool("logCSV", autoTriggerLogCSV);
|
||
preferences.putBool("logPCAP", autoTriggerLogPCAP);
|
||
preferences.putString("start_logic", startLogicOp);
|
||
preferences.putString("stop_logic", stopLogicOp);
|
||
preferences.putInt("start_count", startTriggerCount);
|
||
for (int i = 0; i < startTriggerCount; i++) {
|
||
char key[32];
|
||
sprintf(key, "s%d_id", i); preferences.putUInt(key, startTriggers[i].canId);
|
||
sprintf(key, "s%d_bit", i); preferences.putUChar(key, startTriggers[i].startBit);
|
||
sprintf(key, "s%d_len", i); preferences.putUChar(key, startTriggers[i].bitLength);
|
||
sprintf(key, "s%d_op", i); preferences.putString(key, startTriggers[i].op);
|
||
sprintf(key, "s%d_val", i); preferences.putLong(key, startTriggers[i].value);
|
||
sprintf(key, "s%d_en", i); preferences.putBool(key, startTriggers[i].enabled);
|
||
}
|
||
preferences.putInt("stop_count", stopTriggerCount);
|
||
for (int i = 0; i < stopTriggerCount; i++) {
|
||
char key[32];
|
||
sprintf(key, "p%d_id", i); preferences.putUInt(key, stopTriggers[i].canId);
|
||
sprintf(key, "p%d_bit", i); preferences.putUChar(key, stopTriggers[i].startBit);
|
||
sprintf(key, "p%d_len", i); preferences.putUChar(key, stopTriggers[i].bitLength);
|
||
sprintf(key, "p%d_op", i); preferences.putString(key, stopTriggers[i].op);
|
||
sprintf(key, "p%d_val", i); preferences.putLong(key, stopTriggers[i].value);
|
||
sprintf(key, "p%d_en", i); preferences.putBool(key, stopTriggers[i].enabled);
|
||
}
|
||
preferences.putString("start_formula", startFormula);
|
||
preferences.putString("stop_formula", stopFormula);
|
||
preferences.end();
|
||
}
|
||
|
||
void loadAutoTriggerSettings() {
|
||
preferences.begin("autotrigger", true);
|
||
autoTriggerEnabled = preferences.getBool("enabled", false);
|
||
autoTriggerLogCSV = preferences.getBool("logCSV", false);
|
||
autoTriggerLogPCAP = preferences.getBool("logPCAP", false);
|
||
preferences.getString("start_logic", startLogicOp, sizeof(startLogicOp));
|
||
preferences.getString("stop_logic", stopLogicOp, sizeof(stopLogicOp));
|
||
startFormula = preferences.getString("start_formula", "");
|
||
stopFormula = preferences.getString("stop_formula", "");
|
||
if (strlen(startLogicOp) == 0) strcpy(startLogicOp, "OR");
|
||
if (strlen(stopLogicOp) == 0) strcpy(stopLogicOp, "OR");
|
||
startTriggerCount = preferences.getInt("start_count", 0);
|
||
if (startTriggerCount > MAX_TRIGGERS) startTriggerCount = MAX_TRIGGERS;
|
||
for (int i = 0; i < startTriggerCount; i++) {
|
||
char key[32];
|
||
sprintf(key, "s%d_id", i); startTriggers[i].canId = preferences.getUInt(key, 0);
|
||
sprintf(key, "s%d_bit", i); startTriggers[i].startBit = preferences.getUChar(key, 0);
|
||
sprintf(key, "s%d_len", i); startTriggers[i].bitLength = preferences.getUChar(key, 8);
|
||
sprintf(key, "s%d_op", i); preferences.getString(key, startTriggers[i].op, sizeof(startTriggers[i].op));
|
||
if (strlen(startTriggers[i].op) == 0) strcpy(startTriggers[i].op, "==");
|
||
sprintf(key, "s%d_val", i); startTriggers[i].value = preferences.getLong(key, 0);
|
||
sprintf(key, "s%d_en", i); startTriggers[i].enabled = preferences.getBool(key, true);
|
||
}
|
||
stopTriggerCount = preferences.getInt("stop_count", 0);
|
||
if (stopTriggerCount > MAX_TRIGGERS) stopTriggerCount = MAX_TRIGGERS;
|
||
for (int i = 0; i < stopTriggerCount; i++) {
|
||
char key[32];
|
||
sprintf(key, "p%d_id", i); stopTriggers[i].canId = preferences.getUInt(key, 0);
|
||
sprintf(key, "p%d_bit", i); stopTriggers[i].startBit = preferences.getUChar(key, 0);
|
||
sprintf(key, "p%d_len", i); stopTriggers[i].bitLength = preferences.getUChar(key, 8);
|
||
sprintf(key, "p%d_op", i); preferences.getString(key, stopTriggers[i].op, sizeof(stopTriggers[i].op));
|
||
if (strlen(stopTriggers[i].op) == 0) strcpy(stopTriggers[i].op, "==");
|
||
sprintf(key, "p%d_val", i); stopTriggers[i].value = preferences.getLong(key, 0);
|
||
sprintf(key, "p%d_en", i); stopTriggers[i].enabled = preferences.getBool(key, true);
|
||
}
|
||
preferences.end();
|
||
}
|
||
|
||
// ========================================
|
||
// RTC
|
||
// ========================================
|
||
void initRTC() {
|
||
rtcWire.begin();
|
||
rtcWire.setClock(100000);
|
||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||
timeSyncStatus.rtcAvailable = (rtcWire.endTransmission() == 0);
|
||
Serial.printf("%s RTC(DS3231) %s\n",
|
||
timeSyncStatus.rtcAvailable ? "✓" : "!",
|
||
timeSyncStatus.rtcAvailable ? "감지됨" : "없음");
|
||
}
|
||
|
||
uint8_t bcdToDec(uint8_t v) { return (v >> 4) * 10 + (v & 0x0F); }
|
||
uint8_t decToBcd(uint8_t v) { return ((v / 10) << 4) | (v % 10); }
|
||
|
||
bool readRTC(struct tm *t) {
|
||
if (!timeSyncStatus.rtcAvailable) return false;
|
||
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false;
|
||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||
rtcWire.write(0x00);
|
||
if (rtcWire.endTransmission() != 0) { xSemaphoreGive(rtcMutex); return false; }
|
||
if (rtcWire.requestFrom(DS3231_ADDRESS, 7) != 7) { xSemaphoreGive(rtcMutex); return false; }
|
||
uint8_t buf[7];
|
||
for (int i = 0; i < 7; i++) buf[i] = rtcWire.read();
|
||
xSemaphoreGive(rtcMutex);
|
||
t->tm_sec = bcdToDec(buf[0] & 0x7F);
|
||
t->tm_min = bcdToDec(buf[1] & 0x7F);
|
||
t->tm_hour = bcdToDec(buf[2] & 0x3F);
|
||
t->tm_wday = bcdToDec(buf[3] & 0x07) - 1;
|
||
t->tm_mday = bcdToDec(buf[4] & 0x3F);
|
||
t->tm_mon = bcdToDec(buf[5] & 0x1F) - 1;
|
||
t->tm_year = bcdToDec(buf[6]) + 100;
|
||
return true;
|
||
}
|
||
|
||
bool writeRTC(const struct tm *t) {
|
||
if (!timeSyncStatus.rtcAvailable) return false;
|
||
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false;
|
||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||
rtcWire.write(0x00);
|
||
rtcWire.write(decToBcd(t->tm_sec));
|
||
rtcWire.write(decToBcd(t->tm_min));
|
||
rtcWire.write(decToBcd(t->tm_hour));
|
||
rtcWire.write(decToBcd(t->tm_wday + 1));
|
||
rtcWire.write(decToBcd(t->tm_mday));
|
||
rtcWire.write(decToBcd(t->tm_mon + 1));
|
||
rtcWire.write(decToBcd(t->tm_year - 100));
|
||
bool ok = (rtcWire.endTransmission() == 0);
|
||
xSemaphoreGive(rtcMutex);
|
||
return ok;
|
||
}
|
||
|
||
void timeSyncCallback(struct timeval *tv) {
|
||
timeSyncStatus.synchronized = true;
|
||
timeSyncStatus.lastSyncTime = (uint64_t)tv->tv_sec * 1000000ULL + tv->tv_usec;
|
||
timeSyncStatus.syncCount++;
|
||
if (timeSyncStatus.rtcAvailable) {
|
||
struct tm t; time_t now = tv->tv_sec; localtime_r(&now, &t);
|
||
if (writeRTC(&t)) timeSyncStatus.rtcSyncCount++;
|
||
}
|
||
}
|
||
|
||
void initNTP() {
|
||
configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov");
|
||
sntp_set_time_sync_notification_cb(timeSyncCallback);
|
||
}
|
||
|
||
void rtcSyncTask(void *parameter) {
|
||
while (1) {
|
||
if (timeSyncStatus.rtcAvailable) {
|
||
struct tm t;
|
||
if (readRTC(&t)) {
|
||
time_t now = mktime(&t);
|
||
struct timeval tv = {now, 0};
|
||
settimeofday(&tv, NULL);
|
||
timeSyncStatus.synchronized = true;
|
||
timeSyncStatus.lastSyncTime = (uint64_t)now * 1000000ULL;
|
||
timeSyncStatus.rtcSyncCount++;
|
||
}
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS));
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// MCP2515 모드
|
||
// ========================================
|
||
bool setMCP2515Mode(MCP2515Mode mode) {
|
||
MCP2515::ERROR result;
|
||
switch (mode) {
|
||
case MCP_MODE_NORMAL: result = mcp2515.setNormalMode(); break;
|
||
case MCP_MODE_LISTEN_ONLY: result = mcp2515.setListenOnlyMode(); break;
|
||
case MCP_MODE_LOOPBACK: result = mcp2515.setLoopbackMode(); break;
|
||
case MCP_MODE_TRANSMIT: result = mcp2515.setListenOnlyMode(); break;
|
||
default: return false;
|
||
}
|
||
if (result == MCP2515::ERROR_OK) { currentMcpMode = mode; return true; }
|
||
return false;
|
||
}
|
||
|
||
// ========================================
|
||
// ★★★ CAN 수신: 초경량 ISR + 레벨 폴링 하이브리드 ★★★
|
||
//
|
||
// [문제 분석]
|
||
// 기존 ISR: vTaskNotifyGiveFromISR + portYIELD_FROM_ISR
|
||
// = FreeRTOS 컨텍스트 스위치 × 8000회/s → 시스템 부하
|
||
// 순수 폴링: vTaskDelay(1~2ms) 공백 동안 MCP2515 RX버퍼(2개) 오버플로우 → 손실
|
||
//
|
||
// [해결책: 하이브리드]
|
||
// ISR → volatile 플래그 세팅만 (FreeRTOS 호출 0, 컨텍스트 스위치 0)
|
||
// Task → 플래그 + INT 레벨 폴링으로 즉시 읽기, 빈 상태엔 taskYIELD()만 사용
|
||
// ★ vTaskDelay 사용 금지: delay 중 HW 오버플로우 발생 가능
|
||
// ========================================
|
||
volatile bool IRAM_DATA_ATTR canIntFlag = false;
|
||
|
||
void IRAM_ATTR canISR() {
|
||
canIntFlag = true; // FreeRTOS 호출 없음 → 컨텍스트 스위치 없음
|
||
}
|
||
|
||
void canRxTask(void *parameter) {
|
||
struct can_frame frame;
|
||
CANMessage msg;
|
||
uint32_t lastErrorCheck = 0;
|
||
uint32_t errorRecoveryCount= 0;
|
||
|
||
Serial.println("✓ CAN RX Task 시작 (초경량ISR+레벨폴링 하이브리드, Core 1, Pri 24)");
|
||
|
||
// ★ setup() 완료 대기 (이중 방어: 태스크가 마지막 생성되더라도 안전하게)
|
||
vTaskDelay(pdMS_TO_TICKS(500));
|
||
|
||
// 초기 버퍼 클리어
|
||
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {}
|
||
canIntFlag = false;
|
||
|
||
while (1) {
|
||
// ── 주기적 에러 체크 (1초마다) ──────────────────────────────────
|
||
uint32_t now = millis();
|
||
if (now - lastErrorCheck > 1000) {
|
||
lastErrorCheck = now;
|
||
uint8_t errorFlag = mcp2515.getErrorFlags();
|
||
uint8_t rxErr = mcp2515.errorCountRX();
|
||
|
||
if (errorFlag & 0xC0) {
|
||
mcp2515.clearRXnOVRFlags();
|
||
mcp2515.clearInterrupts();
|
||
errorRecoveryCount++;
|
||
}
|
||
if (errorFlag & 0x20) {
|
||
mcp2515.reset(); delay(100);
|
||
mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); delay(10);
|
||
mcp2515.clearRXnOVRFlags();
|
||
mcp2515.clearInterrupts();
|
||
mcp2515.clearTXInterrupts();
|
||
mcp2515.clearMERR();
|
||
mcp2515.clearERRIF(); delay(10);
|
||
if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode();
|
||
else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode();
|
||
else mcp2515.setListenOnlyMode();
|
||
delay(50);
|
||
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {}
|
||
mcp2515.clearRXnOVRFlags();
|
||
errorRecoveryCount++;
|
||
Serial.printf("✓ Bus-Off 복구 (총 %lu회)\n", errorRecoveryCount);
|
||
}
|
||
if ((errorFlag & 0x18) && rxErr > 96)
|
||
Serial.printf("⚠️ Error Passive! REC=%d\n", rxErr);
|
||
}
|
||
|
||
// ── ★ 엣지(ISR 플래그) + 레벨(INT핀) 이중 감지 ─────────────────
|
||
if (canIntFlag || digitalRead(CAN_INT_PIN) == LOW) {
|
||
canIntFlag = false;
|
||
|
||
// INT LOW 동안 RX 버퍼 완전히 비우기 (손실 없음)
|
||
while (digitalRead(CAN_INT_PIN) == LOW &&
|
||
mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
||
checkAutoTriggers(frame);
|
||
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
msg.id = frame.can_id & 0x1FFFFFFF;
|
||
msg.flags = (uint8_t)((frame.can_id >> 24) & 0xE0);
|
||
msg.dlc = frame.can_dlc;
|
||
memcpy(msg.data, frame.data, 8);
|
||
|
||
if (ring_can_push(&canRing, &msg)) totalMsgCount++;
|
||
}
|
||
// 읽기 완료 후 즉시 다음 폴링 (loop 재진입)
|
||
} else {
|
||
// INT HIGH, 플래그도 없음 → 타 태스크에 CPU 양보
|
||
// ★ vTaskDelay 사용 금지: delay 중 HW 오버플로우 가능
|
||
taskYIELD();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// Serial RX Tasks (RingBuffer 버전)
|
||
// ========================================
|
||
void serialRxTask(void *parameter) {
|
||
SerialMessage msg;
|
||
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
|
||
uint16_t lineIndex = 0;
|
||
uint32_t lastActivity = millis();
|
||
|
||
while (1) {
|
||
while (SerialComm.available()) {
|
||
uint8_t c = SerialComm.read();
|
||
lineBuffer[lineIndex++] = c;
|
||
lastActivity = millis();
|
||
if (c == '\n' || c == '\r' || lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
|
||
if (lineIndex > 0) {
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
msg.length = lineIndex;
|
||
memcpy(msg.data, lineBuffer, lineIndex);
|
||
msg.isTx = false;
|
||
if (ring_serial_push(&serialRing, &msg)) totalSerialRxCount++;
|
||
lineIndex = 0;
|
||
}
|
||
}
|
||
if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) lineIndex = 0;
|
||
}
|
||
if (lineIndex > 0 && (millis() - lastActivity > 100)) {
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
msg.length = lineIndex;
|
||
memcpy(msg.data, lineBuffer, lineIndex);
|
||
msg.isTx = false;
|
||
if (ring_serial_push(&serialRing, &msg)) totalSerialRxCount++;
|
||
lineIndex = 0;
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(1));
|
||
}
|
||
}
|
||
|
||
void serial2RxTask(void *parameter) {
|
||
SerialMessage msg;
|
||
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
|
||
uint16_t lineIndex = 0;
|
||
uint32_t lastActivity = millis();
|
||
|
||
while (1) {
|
||
while (Serial2Comm.available()) {
|
||
uint8_t c = Serial2Comm.read();
|
||
lineBuffer[lineIndex++] = c;
|
||
lastActivity = millis();
|
||
if (c == '\n' || c == '\r' || lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
|
||
if (lineIndex > 0) {
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
msg.length = lineIndex;
|
||
memcpy(msg.data, lineBuffer, lineIndex);
|
||
msg.isTx = false;
|
||
if (ring_serial_push(&serial2Ring, &msg)) totalSerial2RxCount++;
|
||
lineIndex = 0;
|
||
}
|
||
}
|
||
if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) lineIndex = 0;
|
||
}
|
||
if (lineIndex > 0 && (millis() - lastActivity > 100)) {
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
msg.length = lineIndex;
|
||
memcpy(msg.data, lineBuffer, lineIndex);
|
||
msg.isTx = false;
|
||
if (ring_serial_push(&serial2Ring, &msg)) totalSerial2RxCount++;
|
||
lineIndex = 0;
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(1));
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ★★★ SD Flush Task (더블 버퍼 비동기 flush) ★★★
|
||
// ========================================
|
||
void sdFlushTask(void *parameter) {
|
||
Serial.println("✓ sdFlushTask 시작 (더블 버퍼링 + RingBuffer)");
|
||
uint32_t flushCount = 0; // flush 횟수 카운터 (flush() 호출 빈도 조절용)
|
||
while (1) {
|
||
uint32_t flushSize;
|
||
if (xTaskNotifyWait(0, 0xFFFFFFFF, &flushSize, portMAX_DELAY) == pdTRUE) {
|
||
if (flushSize > 0 && flushSize <= FILE_BUFFER_SIZE) {
|
||
flushInProgress = true;
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(5000)) == pdTRUE) {
|
||
if (logFile && sdCardReady && loggingEnabled) {
|
||
uint32_t t0 = millis();
|
||
logFile.write(currentFlushBuffer, flushSize);
|
||
flushCount++;
|
||
// ★ logFile.flush()는 FAT 디렉토리 엔트리 2회 섹터 쓰기 유발
|
||
// 매번 호출하면 300ms+ 지연 → 4번(~256KB)마다 1회로 줄임
|
||
// 전원 차단 시 최대 256KB 손실 가능하나, 로깅 완료 후 stopLogging에서 반드시 flush
|
||
if (flushCount % 4 == 0) {
|
||
logFile.flush();
|
||
}
|
||
uint32_t elapsed = millis() - t0;
|
||
if (elapsed > 150)
|
||
Serial.printf("⚠️ SD Flush 지연: %d ms (%d bytes)\n", elapsed, (int)flushSize);
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
} else {
|
||
Serial.println("✗ SD Flush: Mutex 타임아웃");
|
||
}
|
||
flushInProgress = false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ★★★ SD Write Task (RingBuffer Consumer) ★★★
|
||
//
|
||
// 변경점:
|
||
// - xQueueReceive() → ring_can_pop() (non-blocking, no mutex)
|
||
// - BIN hot path: sdMutex 제거 (sdWriteTask만 쓰는 버퍼)
|
||
// - 버퍼 스왑 시에만 sdMutex 획득 (최소화)
|
||
// ========================================
|
||
void sdWriteTask(void *parameter) {
|
||
CANMessage canMsg;
|
||
|
||
while (1) {
|
||
bool hasWork = false;
|
||
|
||
// ★ RingBuffer에서 pop (mutex 없음, 완전 non-blocking)
|
||
if (ring_can_pop(&canRing, &canMsg)) {
|
||
hasWork = true;
|
||
|
||
// 실시간 모니터링 테이블 업데이트
|
||
bool found = false;
|
||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||
if (recentData[i].msg.id == canMsg.id) {
|
||
recentData[i].msg = canMsg;
|
||
recentData[i].count++;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||
if (recentData[i].count == 0) {
|
||
recentData[i].msg = canMsg;
|
||
recentData[i].count = 1;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// SD 로깅
|
||
if (loggingEnabled && sdCardReady) {
|
||
if (canLogFormatCSV) {
|
||
// ── CSV: logFile 직접 쓰기 → sdMutex 필요 ────────────────
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1)) == pdTRUE) {
|
||
char csvLine[128];
|
||
uint64_t relTime = canMsg.timestamp_us - canLogStartTime;
|
||
char dataStr[32]; int dataLen = 0;
|
||
for (int i = 0; i < canMsg.dlc; i++) {
|
||
dataLen += sprintf(&dataStr[dataLen], "%02X", canMsg.data[i]);
|
||
if (i < canMsg.dlc - 1) dataStr[dataLen++] = ' ';
|
||
}
|
||
dataStr[dataLen] = '\0';
|
||
int lineLen = snprintf(csvLine, sizeof(csvLine),
|
||
"%llu,0x%X,%d,%s\n", relTime, canMsg.id, canMsg.dlc, dataStr);
|
||
if (logFile) {
|
||
logFile.write((uint8_t*)csvLine, lineLen);
|
||
currentFileSize += lineLen;
|
||
static int csvFlushCnt = 0;
|
||
if (++csvFlushCnt >= 2000) { logFile.flush(); csvFlushCnt = 0; }
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
} else if (canLogFormatPCAP) {
|
||
// ── PCAP: 32bytes/record → 더블 버퍼 (BIN과 동일 구조) ──
|
||
if (writeBufferIndex + sizeof(PcapRecord) > FILE_BUFFER_SIZE) {
|
||
// ★ flush 완전 완료까지 대기 (타임아웃 없음 - 데이터 손상 방지)
|
||
while (flushInProgress) vTaskDelay(1);
|
||
uint8_t* tmp = currentWriteBuffer;
|
||
currentWriteBuffer = currentFlushBuffer;
|
||
currentFlushBuffer = tmp;
|
||
flushBufferSize = writeBufferIndex;
|
||
writeBufferIndex = 0;
|
||
if (sdFlushTaskHandle)
|
||
xTaskNotify(sdFlushTaskHandle, flushBufferSize, eSetValueWithOverwrite);
|
||
}
|
||
// PcapRecord 조립 후 버퍼에 기록
|
||
PcapRecord rec;
|
||
memset(&rec, 0, sizeof(PcapRecord)); // ★ 안전 초기화
|
||
uint32_t sec = (uint32_t)(canMsg.timestamp_us / 1000000ULL);
|
||
uint32_t usec = (uint32_t)(canMsg.timestamp_us % 1000000ULL);
|
||
rec.hdr.ts_sec = sec;
|
||
rec.hdr.ts_usec = usec;
|
||
rec.hdr.incl_len = sizeof(SocketCANFrame); // 16
|
||
rec.hdr.orig_len = sizeof(SocketCANFrame); // 16
|
||
// ★ flags 필드로 EFF/RTR 정확히 복원 (id>0x7FF 휴리스틱 불필요)
|
||
rec.frame.can_id = canMsg.id;
|
||
if (canMsg.flags & 0x80) rec.frame.can_id |= 0x80000000U; // CAN_EFF_FLAG
|
||
if (canMsg.flags & 0x40) rec.frame.can_id |= 0x40000000U; // CAN_RTR_FLAG
|
||
rec.frame.can_dlc = canMsg.dlc;
|
||
// data: dlc 바이트만 유효, 나머지는 memset으로 이미 0
|
||
memcpy(rec.frame.data, canMsg.data,
|
||
(canMsg.dlc <= 8) ? canMsg.dlc : 8);
|
||
memcpy(¤tWriteBuffer[writeBufferIndex], &rec, sizeof(PcapRecord));
|
||
writeBufferIndex += sizeof(PcapRecord);
|
||
currentFileSize += sizeof(PcapRecord);
|
||
// ★ 최초 3패킷 시리얼 디버그 (PCAP ID=0 확인용)
|
||
if (pcapDbgCount < 3) {
|
||
Serial.printf("[PCAP DBG #%lu] id=0x%X flags=0x%02X dlc=%d data=%02X%02X...\n",
|
||
pcapDbgCount, canMsg.id, canMsg.flags, canMsg.dlc,
|
||
canMsg.data[0], canMsg.data[1]);
|
||
pcapDbgCount++;
|
||
}
|
||
} else {
|
||
// ── BIN: 더블 버퍼 (hot path, sdMutex 없음!) ─────────────
|
||
// writeBuffer는 sdWriteTask만 쓰므로 mutex 불필요
|
||
|
||
if (writeBufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) {
|
||
// ★ flush 완전 완료까지 대기 (타임아웃 없음)
|
||
// 이유: 100ms 타임아웃 후 강제 스왑하면 sdFlushTask가 읽는 중인
|
||
// 버퍼에 sdWriteTask가 덮어써서 SD 파일 데이터 손상 발생
|
||
// 안전성: RingBuffer 8192슬롯(~1초@8kfps)이 대기 중 메시지 흡수
|
||
while (flushInProgress) vTaskDelay(1);
|
||
|
||
uint8_t* tmp = currentWriteBuffer;
|
||
currentWriteBuffer = currentFlushBuffer;
|
||
currentFlushBuffer = tmp;
|
||
flushBufferSize = writeBufferIndex;
|
||
writeBufferIndex = 0;
|
||
|
||
if (sdFlushTaskHandle)
|
||
xTaskNotify(sdFlushTaskHandle, flushBufferSize, eSetValueWithOverwrite);
|
||
}
|
||
|
||
// 현재 쓰기 버퍼에 메시지 추가 (순수 memcpy)
|
||
memcpy(¤tWriteBuffer[writeBufferIndex], &canMsg, sizeof(CANMessage));
|
||
writeBufferIndex += sizeof(CANMessage);
|
||
currentFileSize += sizeof(CANMessage);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!hasWork) vTaskDelay(pdMS_TO_TICKS(1));
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// 모니터 Task
|
||
// ========================================
|
||
void sdMonitorTask(void *parameter) {
|
||
while (1) {
|
||
uint32_t now = millis();
|
||
// ★ msgPerSecond는 Core 0 webUpdateTask에서 계산 (Core 1 스타베이션 방지)
|
||
if (now - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) {
|
||
float rawV = analogRead(MONITORING_VOLT) * (3.3f / 4095.0f);
|
||
powerStatus.voltage = rawV;
|
||
if (now - powerStatus.lastMinReset >= 1000) {
|
||
powerStatus.minVoltage = rawV;
|
||
powerStatus.lastMinReset= now;
|
||
} else {
|
||
if (rawV < powerStatus.minVoltage) powerStatus.minVoltage = rawV;
|
||
}
|
||
powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD);
|
||
powerStatus.lastCheck = now;
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(500));
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// 파일 코멘트 / 시퀀스 관리
|
||
// ========================================
|
||
void saveFileComments() {
|
||
if (!sdCardReady) return;
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
File f = SD_MMC.open("/comments.dat", FILE_WRITE);
|
||
if (f) {
|
||
f.write((uint8_t*)&commentCount, sizeof(commentCount));
|
||
f.write((uint8_t*)fileComments, sizeof(FileComment) * commentCount);
|
||
f.close();
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
|
||
void loadFileComments() {
|
||
if (!sdCardReady) return;
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
if (SD_MMC.exists("/comments.dat")) {
|
||
File f = SD_MMC.open("/comments.dat", FILE_READ);
|
||
if (f) {
|
||
f.read((uint8_t*)&commentCount, sizeof(commentCount));
|
||
if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS;
|
||
f.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount);
|
||
f.close();
|
||
}
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
|
||
const char* getFileComment(const char* filename) {
|
||
for (int i = 0; i < commentCount; i++)
|
||
if (strcmp(fileComments[i].filename, filename) == 0)
|
||
return fileComments[i].comment;
|
||
return "";
|
||
}
|
||
|
||
void addFileComment(const char* filename, const char* comment) {
|
||
for (int i = 0; i < commentCount; i++) {
|
||
if (strcmp(fileComments[i].filename, filename) == 0) {
|
||
strncpy(fileComments[i].comment, comment, MAX_COMMENT_LEN - 1);
|
||
fileComments[i].comment[MAX_COMMENT_LEN-1] = '\0';
|
||
saveFileComments(); return;
|
||
}
|
||
}
|
||
if (commentCount < MAX_FILE_COMMENTS) {
|
||
strncpy(fileComments[commentCount].filename, filename, MAX_FILENAME_LEN-1);
|
||
fileComments[commentCount].filename[MAX_FILENAME_LEN-1] = '\0';
|
||
strncpy(fileComments[commentCount].comment, comment, MAX_COMMENT_LEN-1);
|
||
fileComments[commentCount].comment[MAX_COMMENT_LEN-1] = '\0';
|
||
commentCount++;
|
||
saveFileComments();
|
||
}
|
||
}
|
||
|
||
void saveSequences() {
|
||
if (!sdCardReady) return;
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
File f = SD_MMC.open("/sequences.dat", FILE_WRITE);
|
||
if (f) {
|
||
f.write((uint8_t*)&sequenceCount, sizeof(sequenceCount));
|
||
f.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
|
||
f.close();
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
|
||
void loadSequences() {
|
||
if (!sdCardReady) return;
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
if (SD_MMC.exists("/sequences.dat")) {
|
||
File f = SD_MMC.open("/sequences.dat", FILE_READ);
|
||
if (f) {
|
||
f.read((uint8_t*)&sequenceCount, sizeof(sequenceCount));
|
||
if (sequenceCount > MAX_SEQUENCES) sequenceCount = MAX_SEQUENCES;
|
||
f.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
|
||
f.close();
|
||
}
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// TX / Sequence Task
|
||
// ========================================
|
||
void txTask(void *parameter) {
|
||
struct can_frame frame;
|
||
while (1) {
|
||
uint32_t now = millis();
|
||
bool anyActive = false;
|
||
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
||
if (txMessages[i].active && txMessages[i].interval > 0) {
|
||
anyActive = true;
|
||
if (now - txMessages[i].lastSent >= txMessages[i].interval) {
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setNormalMode();
|
||
frame.can_id = txMessages[i].id;
|
||
if (txMessages[i].extended) frame.can_id |= CAN_EFF_FLAG;
|
||
frame.can_dlc = txMessages[i].dlc;
|
||
memcpy(frame.data, txMessages[i].data, 8);
|
||
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
|
||
totalTxCount++;
|
||
txMessages[i].lastSent = now;
|
||
}
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setListenOnlyMode();
|
||
}
|
||
}
|
||
}
|
||
vTaskDelay(anyActive ? pdMS_TO_TICKS(1) : pdMS_TO_TICKS(10));
|
||
}
|
||
}
|
||
|
||
void sequenceTask(void *parameter) {
|
||
while (1) {
|
||
if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0 &&
|
||
seqRuntime.activeSequenceIndex < sequenceCount) {
|
||
CANSequence* seq = &sequences[seqRuntime.activeSequenceIndex];
|
||
uint32_t now = millis();
|
||
if (seqRuntime.currentStep < seq->stepCount) {
|
||
SequenceStep* step = &seq->steps[seqRuntime.currentStep];
|
||
if (now - seqRuntime.lastStepTime >= step->delayMs) {
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setNormalMode();
|
||
struct can_frame frame;
|
||
frame.can_id = step->canId;
|
||
if (step->extended) frame.can_id |= CAN_EFF_FLAG;
|
||
frame.can_dlc = step->dlc;
|
||
memcpy(frame.data, step->data, 8);
|
||
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) totalTxCount++;
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setListenOnlyMode();
|
||
seqRuntime.currentStep++;
|
||
seqRuntime.lastStepTime = now;
|
||
}
|
||
} else {
|
||
if (seq->repeatMode == 0) {
|
||
seqRuntime.running = false;
|
||
} else if (seq->repeatMode == 1) {
|
||
if (++seqRuntime.currentRepeat >= seq->repeatCount) seqRuntime.running = false;
|
||
else { seqRuntime.currentStep = 0; seqRuntime.lastStepTime = now; }
|
||
} else {
|
||
seqRuntime.currentStep = 0; seqRuntime.lastStepTime = now;
|
||
}
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(1));
|
||
} else {
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// WebSocket 이벤트
|
||
// ========================================
|
||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
|
||
if (type == WStype_CONNECTED) {
|
||
DynamicJsonDocument allData(3072);
|
||
allData["type"] = "initialData";
|
||
int speedIndex = 3;
|
||
for (int i = 0; i < 4; i++) {
|
||
if (canSpeedValues[i] == currentCanSpeed) { speedIndex = i; break; }
|
||
}
|
||
allData["canSpeed"] = speedIndex;
|
||
allData["mcpMode"] = (int)currentMcpMode;
|
||
allData["autoTriggerEnabled"]= autoTriggerEnabled;
|
||
allData["autoTriggerLogCSV"] = autoTriggerLogCSV;
|
||
allData["autoTriggerLogPCAP"]= autoTriggerLogPCAP;
|
||
if (autoTriggerEnabled) {
|
||
allData["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin");
|
||
allData["startLogic"] = startLogicOp;
|
||
allData["stopLogic"] = stopLogicOp;
|
||
allData["startFormula"]= startFormula;
|
||
allData["stopFormula"] = stopFormula;
|
||
JsonArray sa = allData.createNestedArray("startTriggers");
|
||
for (int i = 0; i < startTriggerCount; i++) {
|
||
JsonObject t = sa.createNestedObject();
|
||
char idStr[10]; sprintf(idStr, "0x%03X", startTriggers[i].canId);
|
||
t["canId"] = idStr; t["startBit"] = startTriggers[i].startBit;
|
||
t["bitLength"] = startTriggers[i].bitLength; t["op"] = startTriggers[i].op;
|
||
t["value"] = (long)startTriggers[i].value; t["enabled"] = startTriggers[i].enabled;
|
||
}
|
||
JsonArray ea = allData.createNestedArray("stopTriggers");
|
||
for (int i = 0; i < stopTriggerCount; i++) {
|
||
JsonObject t = ea.createNestedObject();
|
||
char idStr[10]; sprintf(idStr, "0x%03X", stopTriggers[i].canId);
|
||
t["canId"] = idStr; t["startBit"] = stopTriggers[i].startBit;
|
||
t["bitLength"] = stopTriggers[i].bitLength; t["op"] = stopTriggers[i].op;
|
||
t["value"] = (long)stopTriggers[i].value; t["enabled"] = stopTriggers[i].enabled;
|
||
}
|
||
}
|
||
String json; serializeJson(allData, json);
|
||
webSocket.sendTXT(num, json);
|
||
}
|
||
else if (type == WStype_TEXT) {
|
||
DynamicJsonDocument doc(44384);
|
||
if (deserializeJson(doc, payload)) return;
|
||
const char* cmd = doc["cmd"];
|
||
|
||
if (strcmp(cmd, "getSettings") == 0) {
|
||
DynamicJsonDocument r(1024);
|
||
r["type"] = "settings"; r["ssid"] = wifiSSID; r["password"] = wifiPassword;
|
||
r["staEnable"] = enableSTAMode; r["staSSID"] = staSSID; r["staPassword"] = staPassword;
|
||
r["staConnected"] = (WiFi.status() == WL_CONNECTED);
|
||
r["staIP"] = WiFi.localIP().toString();
|
||
String j; serializeJson(r, j); webSocket.sendTXT(num, j);
|
||
}
|
||
else if (strcmp(cmd, "saveSettings") == 0) {
|
||
const char* s = doc["ssid"];
|
||
if (s && strlen(s) > 0) { strncpy(wifiSSID, s, sizeof(wifiSSID)-1); wifiSSID[sizeof(wifiSSID)-1]='\0'; }
|
||
const char* p = doc["password"];
|
||
if (p) { strncpy(wifiPassword, p, sizeof(wifiPassword)-1); wifiPassword[sizeof(wifiPassword)-1]='\0'; }
|
||
enableSTAMode = doc["staEnable"];
|
||
const char* ss = doc["staSSID"];
|
||
if (ss) { strncpy(staSSID, ss, sizeof(staSSID)-1); staSSID[sizeof(staSSID)-1]='\0'; }
|
||
const char* sp = doc["staPassword"];
|
||
if (sp) { strncpy(staPassword, sp, sizeof(staPassword)-1); staPassword[sizeof(staPassword)-1]='\0'; }
|
||
saveSettings();
|
||
DynamicJsonDocument r(256); r["type"]="settingsSaved"; r["success"]=true;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "startLogging") == 0) {
|
||
if (!loggingEnabled && sdCardReady) {
|
||
const char* fmt = doc["format"];
|
||
canLogFormatCSV = (fmt && strcmp(fmt, "csv") == 0);
|
||
canLogFormatPCAP = (fmt && strcmp(fmt, "pcap") == 0);
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
time_t now; struct tm ti; time(&now); localtime_r(&now, &ti);
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
const char* ext;
|
||
if (canLogFormatPCAP) ext = "pcap";
|
||
else if (canLogFormatCSV) ext = "csv";
|
||
else ext = "bin";
|
||
snprintf(currentFilename, sizeof(currentFilename),
|
||
"/CAN_%04d%02d%02d_%02d%02d%02d.%s",
|
||
ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday,
|
||
ti.tm_hour, ti.tm_min, ti.tm_sec, ext);
|
||
logFile = SD_MMC.open(currentFilename, FILE_WRITE);
|
||
if (logFile) {
|
||
if (canLogFormatCSV) { logFile.println("Time_us,CAN_ID,DLC,Data"); logFile.flush(); }
|
||
else if (canLogFormatPCAP) { writePcapGlobalHeader(logFile); logFile.flush(); }
|
||
logFile.close();
|
||
logFile = SD_MMC.open(currentFilename, FILE_APPEND);
|
||
if (logFile) {
|
||
loggingEnabled = true;
|
||
writeBufferIndex = 0; flushBufferSize = 0; flushInProgress = false;
|
||
currentFileSize = logFile.size();
|
||
pcapDbgCount = 0; // ★ PCAP 디버그 카운터 리셋
|
||
const char* fmtName = canLogFormatPCAP ? "PCAP" : (canLogFormatCSV ? "CSV" : "BIN");
|
||
Serial.printf("✅ 로깅 시작 [%s]: %s\n", fmtName, currentFilename);
|
||
}
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "stopLogging") == 0) {
|
||
if (loggingEnabled) {
|
||
// ① 로깅 중단 신호 → sdWriteTask/sdFlushTask가 새 데이터 쓰기 중단
|
||
loggingEnabled = false;
|
||
// ② sdFlushTask가 진행 중인 flush 완료 대기 (뮤텍스 잡기 전에!)
|
||
// sdMutex를 먼저 잡으면 sdFlushTask가 뮤텍스 못 얻어 데드락
|
||
int wc = 0;
|
||
while (flushInProgress && wc++ < 500) vTaskDelay(10);
|
||
// ③ 이제 sdMutex 안전하게 획득
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) {
|
||
// ④ writeBuffer에 남은 데이터 마지막으로 쓰기
|
||
if (writeBufferIndex > 0 && logFile) {
|
||
logFile.write(currentWriteBuffer, writeBufferIndex);
|
||
writeBufferIndex = 0;
|
||
}
|
||
// ⑤ 최종 flush (4번에 1번 규칙 무관하게 반드시 호출)
|
||
if (logFile) { logFile.flush(); logFile.close(); }
|
||
currentFilename[0] = '\0';
|
||
flushBufferSize = 0;
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "startSerialLogging") == 0) {
|
||
if (!serialLoggingEnabled && sdCardReady) {
|
||
const char* fmt = doc["format"];
|
||
serialLogFormatCSV = !(fmt && strcmp(fmt, "bin") == 0);
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
time_t now; struct tm ti; time(&now); localtime_r(&now, &ti);
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
serialLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
const char* ext = serialLogFormatCSV ? "csv" : "bin";
|
||
snprintf(currentSerialFilename, sizeof(currentSerialFilename),
|
||
"/SER_%04d%02d%02d_%02d%02d%02d.%s",
|
||
ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday,
|
||
ti.tm_hour, ti.tm_min, ti.tm_sec, ext);
|
||
serialLogFile = SD_MMC.open(currentSerialFilename, FILE_WRITE);
|
||
if (serialLogFile) {
|
||
if (serialLogFormatCSV) serialLogFile.println("Time_us,Direction,Data");
|
||
serialLoggingEnabled = true;
|
||
serialCsvIndex = 0;
|
||
currentSerialFileSize = serialLogFile.size();
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "stopSerialLogging") == 0) {
|
||
if (serialLoggingEnabled) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
if (serialCsvIndex > 0 && serialLogFile)
|
||
serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex);
|
||
if (serialLogFile) serialLogFile.close();
|
||
serialLoggingEnabled = false; serialCsvIndex = 0;
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "sendSerial") == 0) {
|
||
const char* data = doc["data"];
|
||
if (data && strlen(data) > 0) {
|
||
SerialComm.println(data);
|
||
SerialMessage msg;
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
msg.length = min((int)(strlen(data)+2), MAX_SERIAL_LINE_LEN-1);
|
||
snprintf((char*)msg.data, MAX_SERIAL_LINE_LEN, "%s\r\n", data);
|
||
msg.isTx = true;
|
||
if (ring_serial_push(&serialRing, &msg)) totalSerialTxCount++;
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "setSerialConfig") == 0) {
|
||
serialSettings.baudRate = doc["baudRate"] | 115200;
|
||
serialSettings.dataBits = doc["dataBits"] | 8;
|
||
serialSettings.parity = doc["parity"] | 0;
|
||
serialSettings.stopBits = doc["stopBits"] | 1;
|
||
saveSerialSettings(); applySerialSettings();
|
||
}
|
||
else if (strcmp(cmd, "getSerialConfig") == 0) {
|
||
DynamicJsonDocument r(512);
|
||
r["type"]="serialConfig"; r["baudRate"]=serialSettings.baudRate;
|
||
r["dataBits"]=serialSettings.dataBits; r["parity"]=serialSettings.parity;
|
||
r["stopBits"]=serialSettings.stopBits;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "startSerial2Logging") == 0) {
|
||
if (!serial2LoggingEnabled && sdCardReady) {
|
||
const char* fmt = doc["format"];
|
||
serial2LogFormatCSV = !(fmt && strcmp(fmt, "bin") == 0);
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
time_t now; struct tm ti; time(&now); localtime_r(&now, &ti);
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
serial2LogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
const char* ext = serial2LogFormatCSV ? "csv" : "bin";
|
||
snprintf(currentSerial2Filename, sizeof(currentSerial2Filename),
|
||
"/SER2_%04d%02d%02d_%02d%02d%02d.%s",
|
||
ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday,
|
||
ti.tm_hour, ti.tm_min, ti.tm_sec, ext);
|
||
serial2LogFile = SD_MMC.open(currentSerial2Filename, FILE_WRITE);
|
||
if (serial2LogFile) {
|
||
if (serial2LogFormatCSV) serial2LogFile.println("Time_us,Direction,Data");
|
||
serial2LoggingEnabled = true;
|
||
serial2CsvIndex = 0;
|
||
currentSerial2FileSize = serial2LogFile.size();
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "stopSerial2Logging") == 0) {
|
||
if (serial2LoggingEnabled) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
if (serial2CsvIndex > 0 && serial2LogFile)
|
||
serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex);
|
||
if (serial2LogFile) serial2LogFile.close();
|
||
serial2LoggingEnabled = false; serial2CsvIndex = 0;
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "sendSerial2") == 0) {
|
||
const char* data = doc["data"];
|
||
if (data && strlen(data) > 0) {
|
||
Serial2Comm.println(data);
|
||
SerialMessage msg;
|
||
struct timeval tv; gettimeofday(&tv, NULL);
|
||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
msg.length = min((int)(strlen(data)+2), MAX_SERIAL_LINE_LEN-1);
|
||
snprintf((char*)msg.data, MAX_SERIAL_LINE_LEN, "%s\r\n", data);
|
||
msg.isTx = true;
|
||
if (ring_serial_push(&serial2Ring, &msg)) totalSerial2TxCount++;
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "setSerial2Config") == 0) {
|
||
serial2Settings.baudRate = doc["baudRate"] | 115200;
|
||
serial2Settings.dataBits = doc["dataBits"] | 8;
|
||
serial2Settings.parity = doc["parity"] | 0;
|
||
serial2Settings.stopBits = doc["stopBits"] | 1;
|
||
saveSerialSettings(); applySerialSettings();
|
||
}
|
||
else if (strcmp(cmd, "getSerial2Config") == 0) {
|
||
DynamicJsonDocument r(512);
|
||
r["type"]="serial2Config"; r["baudRate"]=serial2Settings.baudRate;
|
||
r["dataBits"]=serial2Settings.dataBits; r["parity"]=serial2Settings.parity;
|
||
r["stopBits"]=serial2Settings.stopBits;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "setSpeed") == 0) {
|
||
int si = doc["speed"];
|
||
if (si >= 0 && si < 4) {
|
||
currentCanSpeed = canSpeedValues[si];
|
||
saveSettings();
|
||
StaticJsonDocument<256> r;
|
||
r["type"]="info"; r["message"]="CAN speed saved. Restart to apply.";
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "setMcpMode") == 0) {
|
||
int mode = doc["mode"];
|
||
if (mode >= 0 && mode <= 3) { setMCP2515Mode((MCP2515Mode)mode); saveSettings(); }
|
||
}
|
||
else if (strcmp(cmd, "syncTimeFromPhone") == 0) {
|
||
struct tm ti;
|
||
ti.tm_year = (int)doc["year"] - 1900;
|
||
ti.tm_mon = (int)doc["month"] - 1;
|
||
ti.tm_mday = doc["day"] | 1;
|
||
ti.tm_hour = doc["hour"] | 0;
|
||
ti.tm_min = doc["minute"] | 0;
|
||
ti.tm_sec = doc["second"] | 0;
|
||
time_t t = mktime(&ti);
|
||
struct timeval tv = {t, 0}; settimeofday(&tv, NULL);
|
||
timeSyncStatus.synchronized = true;
|
||
timeSyncStatus.lastSyncTime = (uint64_t)t * 1000000ULL;
|
||
timeSyncStatus.syncCount++;
|
||
if (timeSyncStatus.rtcAvailable) writeRTC(&ti);
|
||
}
|
||
else if (strcmp(cmd, "getFiles") == 0) {
|
||
if (sdCardReady) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
DynamicJsonDocument r(6144);
|
||
r["type"] = "files";
|
||
JsonArray files = r.createNestedArray("files");
|
||
File root = SD_MMC.open("/");
|
||
if (root) {
|
||
File file = root.openNextFile();
|
||
int cnt = 0;
|
||
while (file && cnt < 50) {
|
||
if (!file.isDirectory()) {
|
||
const char* fn = file.name();
|
||
if (fn[0] == '/') fn++;
|
||
if (fn[0] != '.' && strcmp(fn,"System Volume Information")!=0 && strlen(fn)>0) {
|
||
JsonObject fo = files.createNestedObject();
|
||
fo["name"] = fn; fo["size"] = file.size();
|
||
const char* cm = getFileComment(fn);
|
||
if (strlen(cm) > 0) fo["comment"] = cm;
|
||
cnt++;
|
||
}
|
||
}
|
||
file.close();
|
||
file = root.openNextFile();
|
||
}
|
||
root.close();
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "deleteFile") == 0) {
|
||
const char* fn = doc["filename"];
|
||
if (fn && strlen(fn) > 0) {
|
||
String fp = "/" + String(fn);
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
bool ok = SD_MMC.exists(fp) && SD_MMC.remove(fp);
|
||
xSemaphoreGive(sdMutex);
|
||
DynamicJsonDocument r(256); r["type"]="deleteResult"; r["success"]=ok;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "addComment") == 0) {
|
||
const char* fn = doc["filename"], *cm = doc["comment"];
|
||
if (fn && cm) addFileComment(fn, cm);
|
||
}
|
||
else if (strcmp(cmd, "setAutoTrigger") == 0) {
|
||
autoTriggerEnabled = doc["enabled"] | false;
|
||
const char* lf = doc["logFormat"];
|
||
if (lf) {
|
||
autoTriggerLogCSV = (strcmp(lf,"csv") == 0);
|
||
autoTriggerLogPCAP = (strcmp(lf,"pcap") == 0);
|
||
}
|
||
saveAutoTriggerSettings();
|
||
DynamicJsonDocument r(256); r["type"]="autoTriggerSet";
|
||
r["enabled"]=autoTriggerEnabled;
|
||
r["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin");
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "setAutoTriggerFormat") == 0) {
|
||
const char* lf = doc["logFormat"];
|
||
if (lf) {
|
||
autoTriggerLogCSV = (strcmp(lf,"csv") == 0);
|
||
autoTriggerLogPCAP = (strcmp(lf,"pcap") == 0);
|
||
saveAutoTriggerSettings();
|
||
}
|
||
DynamicJsonDocument r(128); r["type"]="autoTriggerFormatSet";
|
||
r["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin");
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "saveCanFormat") == 0) {
|
||
const char* fmt = doc["format"];
|
||
if (fmt) {
|
||
savedCanLogFormatCSV = (strcmp(fmt, "csv") == 0);
|
||
savedCanLogFormatPCAP = (strcmp(fmt, "pcap") == 0);
|
||
saveSettings();
|
||
Serial.printf("💾 CAN Format 저장: %s\n",
|
||
savedCanLogFormatPCAP ? "PCAP" : (savedCanLogFormatCSV ? "CSV" : "BIN"));
|
||
}
|
||
DynamicJsonDocument r(128); r["type"]="canFormatSaved";
|
||
r["format"] = savedCanLogFormatPCAP ? "pcap" : (savedCanLogFormatCSV ? "csv" : "bin");
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "setStartTriggers") == 0) {
|
||
JsonArray triggers = doc["triggers"];
|
||
strcpy(startLogicOp, doc["logic"] | "OR");
|
||
startTriggerCount = 0;
|
||
for (JsonObject t : triggers) {
|
||
if (startTriggerCount >= MAX_TRIGGERS) break;
|
||
String idStr = t["canId"] | "0x0";
|
||
startTriggers[startTriggerCount].canId = strtoul(idStr.c_str(),NULL,16);
|
||
startTriggers[startTriggerCount].startBit = t["startBit"] | 0;
|
||
startTriggers[startTriggerCount].bitLength = t["bitLength"] | 8;
|
||
strcpy(startTriggers[startTriggerCount].op, t["op"] | "==");
|
||
startTriggers[startTriggerCount].value = t["value"] | 0;
|
||
startTriggers[startTriggerCount].enabled = t["enabled"] | true;
|
||
startTriggerCount++;
|
||
}
|
||
saveAutoTriggerSettings();
|
||
DynamicJsonDocument r(256); r["type"]="startTriggersSet"; r["count"]=startTriggerCount;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "setStopTriggers") == 0) {
|
||
JsonArray triggers = doc["triggers"];
|
||
strcpy(stopLogicOp, doc["logic"] | "OR");
|
||
stopTriggerCount = 0;
|
||
for (JsonObject t : triggers) {
|
||
if (stopTriggerCount >= MAX_TRIGGERS) break;
|
||
String idStr = t["canId"] | "0x0";
|
||
stopTriggers[stopTriggerCount].canId = strtoul(idStr.c_str(),NULL,16);
|
||
stopTriggers[stopTriggerCount].startBit = t["startBit"] | 0;
|
||
stopTriggers[stopTriggerCount].bitLength = t["bitLength"] | 8;
|
||
strcpy(stopTriggers[stopTriggerCount].op, t["op"] | "==");
|
||
stopTriggers[stopTriggerCount].value = t["value"] | 0;
|
||
stopTriggers[stopTriggerCount].enabled = t["enabled"] | true;
|
||
stopTriggerCount++;
|
||
}
|
||
saveAutoTriggerSettings();
|
||
DynamicJsonDocument r(256); r["type"]="stopTriggersSet"; r["count"]=stopTriggerCount;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "setStartFormula") == 0) {
|
||
const char* f = doc["formula"];
|
||
if (f) { startFormula = String(f); saveAutoTriggerSettings(); }
|
||
}
|
||
else if (strcmp(cmd, "setStopFormula") == 0) {
|
||
const char* f = doc["formula"];
|
||
if (f) { stopFormula = String(f); saveAutoTriggerSettings(); }
|
||
}
|
||
else if (strcmp(cmd, "getAutoTriggers") == 0) {
|
||
DynamicJsonDocument r(2048);
|
||
r["type"]="autoTriggers"; r["enabled"]=autoTriggerEnabled;
|
||
r["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin");
|
||
r["startLogic"]=startLogicOp; r["stopLogic"]=stopLogicOp;
|
||
r["startFormula"]=startFormula; r["stopFormula"]=stopFormula;
|
||
JsonArray sa = r.createNestedArray("startTriggers");
|
||
for (int i = 0; i < startTriggerCount; i++) {
|
||
JsonObject t = sa.createNestedObject();
|
||
char ids[10]; sprintf(ids,"0x%03X",startTriggers[i].canId);
|
||
t["canId"]=ids; t["startBit"]=startTriggers[i].startBit;
|
||
t["bitLength"]=startTriggers[i].bitLength; t["op"]=startTriggers[i].op;
|
||
t["value"]=(long)startTriggers[i].value; t["enabled"]=startTriggers[i].enabled;
|
||
}
|
||
JsonArray ea = r.createNestedArray("stopTriggers");
|
||
for (int i = 0; i < stopTriggerCount; i++) {
|
||
JsonObject t = ea.createNestedObject();
|
||
char ids[10]; sprintf(ids,"0x%03X",stopTriggers[i].canId);
|
||
t["canId"]=ids; t["startBit"]=stopTriggers[i].startBit;
|
||
t["bitLength"]=stopTriggers[i].bitLength; t["op"]=stopTriggers[i].op;
|
||
t["value"]=(long)stopTriggers[i].value; t["enabled"]=stopTriggers[i].enabled;
|
||
}
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "sendOnce") == 0) {
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setNormalMode();
|
||
struct can_frame frame;
|
||
frame.can_id = strtoul(doc["id"], NULL, 16);
|
||
if (doc["ext"] | false) frame.can_id |= CAN_EFF_FLAG;
|
||
frame.can_dlc = doc["dlc"] | 8;
|
||
JsonArray da = doc["data"];
|
||
for (int i = 0; i < 8; i++) frame.data[i] = da[i] | 0;
|
||
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) totalTxCount++;
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setListenOnlyMode();
|
||
}
|
||
else if (strcmp(cmd, "addSequence") == 0) {
|
||
if (sequenceCount >= MAX_SEQUENCES) {
|
||
DynamicJsonDocument r(256); r["type"]="error"; r["message"]="Max sequences reached";
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
} else {
|
||
const char* name = doc["name"];
|
||
JsonArray stepsArr = doc["steps"];
|
||
if (name && stepsArr.size() > 0) {
|
||
CANSequence* seq = &sequences[sequenceCount];
|
||
strncpy(seq->name, name, sizeof(seq->name)-1);
|
||
seq->name[sizeof(seq->name)-1]='\0';
|
||
seq->repeatMode = doc["repeatMode"] | 0;
|
||
seq->repeatCount = doc["repeatCount"] | 1;
|
||
seq->stepCount = 0;
|
||
for (JsonObject so : stepsArr) {
|
||
if (seq->stepCount >= 20) break;
|
||
SequenceStep* step = &seq->steps[seq->stepCount];
|
||
const char* ids = so["id"];
|
||
if (ids) step->canId = strtoul(
|
||
(strncmp(ids,"0x",2)==0||strncmp(ids,"0X",2)==0) ? ids+2 : ids, NULL, 16);
|
||
step->extended = so["ext"] | false;
|
||
step->dlc = so["dlc"] | 8;
|
||
JsonArray da = so["data"];
|
||
for (int i = 0; i < 8 && i < (int)da.size(); i++) step->data[i] = da[i];
|
||
step->delayMs = so["delay"] | 0;
|
||
seq->stepCount++;
|
||
}
|
||
sequenceCount++;
|
||
saveSequences();
|
||
DynamicJsonDocument r(256); r["type"]="sequenceSaved";
|
||
r["name"]=name; r["steps"]=seq->stepCount;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
delay(100);
|
||
webSocket.sendTXT(num, "{\"cmd\":\"getSequences\"}");
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "getSequences") == 0) {
|
||
DynamicJsonDocument r(3072); r["type"]="sequences";
|
||
JsonArray sa = r.createNestedArray("list");
|
||
for (int i = 0; i < sequenceCount; i++) {
|
||
JsonObject so = sa.createNestedObject();
|
||
so["name"]=sequences[i].name; so["steps"]=sequences[i].stepCount;
|
||
so["repeatMode"]=sequences[i].repeatMode; so["repeatCount"]=sequences[i].repeatCount;
|
||
}
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
else if (strcmp(cmd, "startSequence") == 0) {
|
||
int idx = doc["index"] | -1;
|
||
if (idx >= 0 && idx < sequenceCount) {
|
||
seqRuntime = {true, 0, 0, millis(), (int8_t)idx};
|
||
DynamicJsonDocument r(256); r["type"]="sequenceStarted";
|
||
r["index"]=idx; r["name"]=sequences[idx].name;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "stopSequence") == 0) {
|
||
if (seqRuntime.running) {
|
||
seqRuntime.running = false; seqRuntime.activeSequenceIndex = -1;
|
||
DynamicJsonDocument r(256); r["type"]="sequenceStopped";
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "removeSequence") == 0) {
|
||
int idx = doc["index"] | -1;
|
||
if (idx >= 0 && idx < sequenceCount) {
|
||
for (int i = idx; i < sequenceCount-1; i++)
|
||
memcpy(&sequences[i], &sequences[i+1], sizeof(CANSequence));
|
||
sequenceCount--;
|
||
saveSequences();
|
||
DynamicJsonDocument r(256); r["type"]="sequenceDeleted"; r["index"]=idx;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
delay(100);
|
||
webSocket.sendTXT(num, "{\"cmd\":\"getSequences\"}");
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "getSequenceDetail") == 0) {
|
||
int idx = doc["index"] | -1;
|
||
if (idx >= 0 && idx < sequenceCount) {
|
||
DynamicJsonDocument r(4096); r["type"]="sequenceDetail";
|
||
r["index"]=idx; r["name"]=sequences[idx].name;
|
||
r["repeatMode"]=sequences[idx].repeatMode; r["repeatCount"]=sequences[idx].repeatCount;
|
||
JsonArray sa = r.createNestedArray("steps");
|
||
for (int i = 0; i < sequences[idx].stepCount; i++) {
|
||
SequenceStep* s = &sequences[idx].steps[i];
|
||
JsonObject so = sa.createNestedObject();
|
||
char ids[12]; sprintf(ids,"0x%X",s->canId);
|
||
so["id"]=ids; so["ext"]=s->extended; so["dlc"]=s->dlc;
|
||
JsonArray da = so.createNestedArray("data");
|
||
for (int j=0;j<8;j++) da.add(s->data[j]);
|
||
so["delay"]=s->delayMs;
|
||
}
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "hwReset") == 0) {
|
||
DynamicJsonDocument r(256); r["type"]="hwReset"; r["success"]=true;
|
||
String j; serializeJson(r,j); webSocket.sendTXT(num,j);
|
||
delay(100);
|
||
ESP.restart();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// ★★★ Web Update Task (RingBuffer Serial Consumer) ★★★
|
||
// ========================================
|
||
void webUpdateTask(void *parameter) {
|
||
vTaskDelay(pdMS_TO_TICKS(500));
|
||
|
||
while (1) {
|
||
// ★ server.handleClient()를 Core 0에서 호출
|
||
// loop()(Core 1, Pri 1)는 canRxTask(Core 1, Pri 24)에 선점당해 실행 불가
|
||
server.handleClient();
|
||
webSocket.loop();
|
||
|
||
// ★ msgPerSecond를 Core 0에서 직접 계산
|
||
// (sdMonitorTask가 Core 1에서 canRxTask(Pri24)에 스타베이션될 수 있으므로)
|
||
{
|
||
uint32_t now = millis();
|
||
if (now - lastMsgCountTime >= 1000) {
|
||
uint32_t cur = totalMsgCount; // volatile 읽기
|
||
msgPerSecond = cur - lastMsgCount;
|
||
lastMsgCount = cur;
|
||
lastMsgCountTime = now;
|
||
}
|
||
}
|
||
|
||
if (webSocket.connectedClients() > 0) {
|
||
DynamicJsonDocument doc(16384);
|
||
doc["type"] = "update";
|
||
doc["logging"] = loggingEnabled;
|
||
|
||
// Auto Trigger 상태
|
||
doc["autoTriggerEnabled"] = autoTriggerEnabled;
|
||
doc["autoTriggerActive"] = autoTriggerActive;
|
||
doc["startTriggerCount"] = startTriggerCount;
|
||
doc["stopTriggerCount"] = stopTriggerCount;
|
||
|
||
// Serial2
|
||
doc["serialLogging"] = serialLoggingEnabled;
|
||
doc["serial2Logging"] = serial2LoggingEnabled;
|
||
doc["totalSerial2Rx"] = totalSerial2RxCount;
|
||
doc["totalSerial2Tx"] = totalSerial2TxCount;
|
||
|
||
// ★ RingBuffer 사용량 (queueUsed 호환 필드명 유지)
|
||
doc["queueUsed"] = ring_can_count(&canRing);
|
||
doc["queueSize"] = CAN_RING_SIZE;
|
||
doc["serialQueueUsed"] = ring_serial_count(&serialRing);
|
||
doc["serialQueueSize"] = SERIAL_RING_SIZE;
|
||
doc["serial2QueueUsed"] = ring_serial_count(&serial2Ring);
|
||
doc["serial2QueueSize"] = SERIAL_RING_SIZE;
|
||
|
||
// ★ 드랍 카운터 (새로 추가: 웹 UI에서 손실 감지용)
|
||
doc["canDropped"] = canRing.dropped;
|
||
doc["serDropped"] = serialRing.dropped;
|
||
doc["ser2Dropped"] = serial2Ring.dropped;
|
||
|
||
doc["serial2FileSize"] = currentSerial2FileSize;
|
||
doc["currentSerial2File"] =
|
||
(serial2LoggingEnabled && currentSerial2Filename[0]) ?
|
||
String(currentSerial2Filename) : "";
|
||
|
||
doc["sdReady"] = sdCardReady;
|
||
doc["totalMsg"] = totalMsgCount;
|
||
doc["msgPerSec"] = msgPerSecond;
|
||
|
||
// 버스 부하율
|
||
uint32_t maxMPS;
|
||
switch(currentCanSpeed) {
|
||
case CAN_125KBPS: maxMPS=1000; break;
|
||
case CAN_250KBPS: maxMPS=2000; break;
|
||
case CAN_500KBPS: maxMPS=4000; break;
|
||
default: maxMPS=8000; break;
|
||
}
|
||
float busLoad = (msgPerSecond * 100.0f) / maxMPS;
|
||
if (busLoad > 100.0f) busLoad = 100.0f;
|
||
doc["busLoad"] = (int)busLoad;
|
||
|
||
doc["totalTx"] = totalTxCount;
|
||
doc["totalSerialRx"] = totalSerialRxCount;
|
||
doc["totalSerialTx"] = totalSerialTxCount;
|
||
doc["fileSize"] = currentFileSize;
|
||
doc["serialFileSize"] = currentSerialFileSize;
|
||
doc["timeSync"] = timeSyncStatus.synchronized;
|
||
doc["rtcAvail"] = timeSyncStatus.rtcAvailable;
|
||
doc["rtcSyncCnt"] = timeSyncStatus.rtcSyncCount;
|
||
doc["syncCount"] = timeSyncStatus.syncCount;
|
||
doc["voltage"] = powerStatus.voltage;
|
||
doc["minVoltage"] = powerStatus.minVoltage;
|
||
doc["lowVoltage"] = powerStatus.lowVoltage;
|
||
doc["mcpMode"] = (int)currentMcpMode;
|
||
doc["savedCanFormat"] = savedCanLogFormatPCAP ? "pcap" : (savedCanLogFormatCSV ? "csv" : "bin");
|
||
doc["currentFile"] =
|
||
(loggingEnabled && currentFilename[0]) ? String(currentFilename) : "";
|
||
doc["currentSerialFile"]=
|
||
(serialLoggingEnabled && currentSerialFilename[0]) ? String(currentSerialFilename) : "";
|
||
|
||
time_t now; time(&now);
|
||
doc["timestamp"] = (uint64_t)now;
|
||
|
||
// CAN 최근 메시지 (최대 20개)
|
||
JsonArray messages = doc.createNestedArray("messages");
|
||
int msgCnt = 0;
|
||
for (int i = 0; i < RECENT_MSG_COUNT && msgCnt < 20; i++) {
|
||
if (recentData[i].count > 0) {
|
||
JsonObject mo = messages.createNestedObject();
|
||
mo["id"] = recentData[i].msg.id;
|
||
mo["dlc"] = recentData[i].msg.dlc;
|
||
mo["count"] = recentData[i].count;
|
||
JsonArray da = mo.createNestedArray("data");
|
||
for (int j = 0; j < recentData[i].msg.dlc; j++) da.add(recentData[i].msg.data[j]);
|
||
msgCnt++;
|
||
}
|
||
}
|
||
|
||
// ★ Serial RingBuffer pop (최대 10개)
|
||
SerialMessage serialMsg;
|
||
JsonArray serialMessages = doc.createNestedArray("serialMessages");
|
||
int serialCnt = 0;
|
||
while (serialCnt < 10 && ring_serial_pop(&serialRing, &serialMsg)) {
|
||
JsonObject so = serialMessages.createNestedObject();
|
||
so["timestamp"] = serialMsg.timestamp_us;
|
||
so["isTx"] = serialMsg.isTx;
|
||
char ds[MAX_SERIAL_LINE_LEN+1];
|
||
memcpy(ds, serialMsg.data, serialMsg.length);
|
||
ds[serialMsg.length] = '\0';
|
||
so["data"] = ds;
|
||
serialCnt++;
|
||
|
||
// Serial SD 로깅
|
||
if (serialLoggingEnabled && sdCardReady) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||
if (serialLogFormatCSV) {
|
||
uint64_t rt = serialMsg.timestamp_us - serialLogStartTime;
|
||
char csvLine[256];
|
||
int ll = snprintf(csvLine, sizeof(csvLine),
|
||
"%llu,%s,\"%s\"\n", rt, serialMsg.isTx?"TX":"RX", ds);
|
||
if (serialCsvIndex + ll <= SERIAL_CSV_BUFFER_SIZE) {
|
||
memcpy(&serialCsvBuffer[serialCsvIndex], csvLine, ll);
|
||
serialCsvIndex += ll;
|
||
currentSerialFileSize += ll;
|
||
}
|
||
if (serialCsvIndex >= SERIAL_CSV_BUFFER_SIZE - 256) {
|
||
if (serialLogFile) { serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex); serialLogFile.flush(); serialCsvIndex=0; }
|
||
}
|
||
} else {
|
||
if (serialLogFile) {
|
||
serialLogFile.write((uint8_t*)&serialMsg, sizeof(SerialMessage));
|
||
currentSerialFileSize += sizeof(SerialMessage);
|
||
static int bfc=0; if (++bfc>=50){serialLogFile.flush();bfc=0;}
|
||
}
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ★ Serial2 RingBuffer pop
|
||
SerialMessage serial2Msg;
|
||
JsonArray serial2Messages = doc.createNestedArray("serial2Messages");
|
||
int s2Cnt = 0;
|
||
while (s2Cnt < 10 && ring_serial_pop(&serial2Ring, &serial2Msg)) {
|
||
JsonObject so = serial2Messages.createNestedObject();
|
||
so["timestamp"] = serial2Msg.timestamp_us;
|
||
so["isTx"] = serial2Msg.isTx;
|
||
char ds[MAX_SERIAL_LINE_LEN+1];
|
||
memcpy(ds, serial2Msg.data, serial2Msg.length);
|
||
ds[serial2Msg.length] = '\0';
|
||
so["data"] = ds;
|
||
s2Cnt++;
|
||
|
||
if (serial2LoggingEnabled && sdCardReady) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||
if (serial2LogFormatCSV) {
|
||
uint64_t rt = serial2Msg.timestamp_us - serial2LogStartTime;
|
||
char csvLine[256];
|
||
int ll = snprintf(csvLine, sizeof(csvLine),
|
||
"%llu,%s,\"%s\"\n", rt, serial2Msg.isTx?"TX":"RX", ds);
|
||
if (serial2CsvIndex + ll <= SERIAL2_CSV_BUFFER_SIZE) {
|
||
memcpy(&serial2CsvBuffer[serial2CsvIndex], csvLine, ll);
|
||
serial2CsvIndex += ll;
|
||
currentSerial2FileSize += ll;
|
||
}
|
||
if (serial2CsvIndex >= SERIAL2_CSV_BUFFER_SIZE - 256) {
|
||
if (serial2LogFile) { serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex); serial2LogFile.flush(); serial2CsvIndex=0; }
|
||
}
|
||
} else {
|
||
if (serial2LogFile) {
|
||
serial2LogFile.write((uint8_t*)&serial2Msg, sizeof(SerialMessage));
|
||
currentSerial2FileSize += sizeof(SerialMessage);
|
||
static int bfc2=0; if (++bfc2>=50){serial2LogFile.flush();bfc2=0;}
|
||
}
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
|
||
String json;
|
||
size_t jsonSize = serializeJson(doc, json);
|
||
if (jsonSize > 0 && jsonSize < 8192) webSocket.broadcastTXT(json);
|
||
}
|
||
|
||
vTaskDelay(pdMS_TO_TICKS(500));
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// Setup
|
||
// ========================================
|
||
void setup() {
|
||
Serial.begin(115200);
|
||
delay(1000);
|
||
|
||
esp_reset_reason_t rr = esp_reset_reason();
|
||
if (rr == ESP_RST_BROWNOUT) {
|
||
Serial.println("\n🚨 브라운아웃 리셋 감지! 전원 공급 부족.");
|
||
delay(5000);
|
||
}
|
||
|
||
WiFi.setSleep(false);
|
||
WiFi.setTxPower(WIFI_POWER_15dBm);
|
||
|
||
Serial.println("\n========================================");
|
||
Serial.println(" Byun CAN Logger v3.0");
|
||
Serial.println(" SPSC Lock-Free RingBuffer Edition");
|
||
Serial.println("========================================\n");
|
||
|
||
// PSRAM 초기화 (RingBuffer 포함)
|
||
if (!initPSRAM()) {
|
||
Serial.println("✗ PSRAM 초기화 실패! Tools→PSRAM→OPI PSRAM 확인");
|
||
while (1) { delay(1000); Serial.println("✗ 재업로드 필요!"); }
|
||
}
|
||
|
||
loadSettings();
|
||
loadAutoTriggerSettings();
|
||
analogSetPinAttenuation(MONITORING_VOLT, ADC_11db);
|
||
pinMode(CAN_INT_PIN, INPUT_PULLUP);
|
||
analogSetAttenuation(ADC_11db);
|
||
|
||
hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
|
||
hspi.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0));
|
||
hspi.endTransaction();
|
||
|
||
esp_task_wdt_deinit();
|
||
|
||
// MCP2515 초기화
|
||
Serial.println("MCP2515 초기화...");
|
||
pinMode(HSPI_CS, OUTPUT);
|
||
digitalWrite(HSPI_CS, HIGH); delay(10);
|
||
digitalWrite(HSPI_CS, LOW); delayMicroseconds(100);
|
||
digitalWrite(HSPI_CS, HIGH); delay(100);
|
||
mcp2515.reset(); delay(100);
|
||
mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); delay(10);
|
||
mcp2515.setFilterMask(MCP2515::MASK0, false, 0); mcp2515.setFilter(MCP2515::RXF0, false, 0); mcp2515.setFilter(MCP2515::RXF1, false, 0);
|
||
mcp2515.setFilterMask(MCP2515::MASK1, true, 0); mcp2515.setFilter(MCP2515::RXF2, true, 0); mcp2515.setFilter(MCP2515::RXF3, true, 0);
|
||
mcp2515.setFilter(MCP2515::RXF4, true, 0); mcp2515.setFilter(MCP2515::RXF5, true, 0);
|
||
mcp2515.clearRXnOVRFlags(); mcp2515.clearInterrupts(); mcp2515.clearTXInterrupts();
|
||
mcp2515.clearMERR(); mcp2515.clearERRIF();
|
||
if (currentMcpMode == MCP_MODE_NORMAL) { mcp2515.setNormalMode(); Serial.println(" → Normal Mode"); }
|
||
else if (currentMcpMode == MCP_MODE_LOOPBACK) { mcp2515.setLoopbackMode(); Serial.println(" → Loopback Mode"); }
|
||
else { mcp2515.setListenOnlyMode();Serial.println(" → Listen-Only Mode"); }
|
||
delay(50);
|
||
struct can_frame df; int cc=0;
|
||
while (mcp2515.readMessage(&df)==MCP2515::ERROR_OK && cc++<100);
|
||
mcp2515.clearRXnOVRFlags();
|
||
Serial.println("✓ MCP2515 초기화 완료");
|
||
|
||
applySerialSettings();
|
||
|
||
sdMutex = xSemaphoreCreateMutex();
|
||
rtcMutex = xSemaphoreCreateMutex();
|
||
serialMutex = xSemaphoreCreateMutex();
|
||
serial2Mutex = xSemaphoreCreateMutex();
|
||
if (!sdMutex || !rtcMutex || !serialMutex) {
|
||
Serial.println("✗ Mutex 생성 실패!"); while(1) delay(1000);
|
||
}
|
||
|
||
initRTC();
|
||
|
||
// ★ 로깅: SDIO 4-bit (D0~D3) - 쓰기 속도 우선
|
||
// 다운로드: SDIO 1-bit (D0만) - WiFi RF 간섭 방지
|
||
Serial.println("SD 카드 초기화 (SDIO 4-bit)...");
|
||
if (!SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3)) {
|
||
sdCardReady = false;
|
||
} else if (SD_MMC.begin("/sdcard", false, false, 10000000)) { // false = 4-bit
|
||
sdCardReady = true;
|
||
Serial.printf("✓ SD 카드 초기화 완료: %llu MB\n", SD_MMC.cardSize()/(1024*1024));
|
||
loadFileComments();
|
||
loadSequences();
|
||
} else {
|
||
sdCardReady = false;
|
||
Serial.println("✗ SD 카드 초기화 실패");
|
||
}
|
||
|
||
// WiFi
|
||
WiFi.setSleep(false);
|
||
if (enableSTAMode && strlen(staSSID) > 0) {
|
||
WiFi.mode(WIFI_AP_STA);
|
||
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
|
||
WiFi.begin(staSSID, staPassword);
|
||
int att = 0;
|
||
while (WiFi.status() != WL_CONNECTED && att++ < 20) { delay(500); Serial.print("."); }
|
||
Serial.println();
|
||
if (WiFi.status() == WL_CONNECTED) { Serial.printf("✓ STA IP: %s\n", WiFi.localIP().toString().c_str()); initNTP(); }
|
||
} else {
|
||
WiFi.mode(WIFI_AP);
|
||
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
|
||
Serial.printf("✓ AP: %s | IP: %s\n", wifiSSID, WiFi.softAPIP().toString().c_str());
|
||
}
|
||
esp_wifi_set_max_tx_power(84);
|
||
|
||
webSocket.begin();
|
||
webSocket.onEvent(webSocketEvent);
|
||
|
||
server.on("/", HTTP_GET, [](){server.send_P(200,"text/html",index_html);});
|
||
server.on("/transmit", HTTP_GET, [](){server.send_P(200,"text/html",transmit_html);});
|
||
server.on("/graph", HTTP_GET, [](){server.send_P(200,"text/html",graph_html);});
|
||
server.on("/graph-view",HTTP_GET, [](){server.send_P(200,"text/html",graph_viewer_html);});
|
||
server.on("/settings", HTTP_GET, [](){server.send_P(200,"text/html",settings_html);});
|
||
server.on("/serial", HTTP_GET, [](){server.send_P(200,"text/html",serial_terminal_html);});
|
||
server.on("/serial2", HTTP_GET, [](){server.send_P(200,"text/html",serial2_terminal_html);});
|
||
|
||
server.on("/download", HTTP_GET, []() {
|
||
// ★★★ 주의: 이 핸들러는 webUpdateTask 컨텍스트에서 실행됨
|
||
// → webTaskHandle을 vTaskSuspend 하면 자기 자신을 정지시켜 영원히 멈춤
|
||
// → SD_MMC.end()/재마운트 불필요 (같은 SD 인스턴스 그대로 사용)
|
||
// → CHUNK를 충분히 크게 해서 Watchdog 타임아웃 방지
|
||
|
||
if (!server.hasArg("file")) {
|
||
server.send(400, "text/plain", "Bad request"); return;
|
||
}
|
||
String filename = "/" + server.arg("file");
|
||
|
||
// 로깅 중이면 완전 중단 후 SD 버스 정리
|
||
bool wasLogging = loggingEnabled;
|
||
if (wasLogging) {
|
||
loggingEnabled = false;
|
||
// ① sdWriteTask가 버퍼에 쓰던 것 완료 대기
|
||
// ② sdFlushTask가 SD에 flush 완료 대기 (여기가 핵심 - 안 기다리면 SD 버스 충돌)
|
||
int waitMs = 0;
|
||
while (flushInProgress && waitMs < 2000) { delay(10); waitMs += 10; }
|
||
delay(50); // SD 버스 안정화
|
||
}
|
||
|
||
// sdMutex 획득 (sdFlushTask와 동시 접근 차단)
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(3000)) != pdTRUE) {
|
||
if (wasLogging) loggingEnabled = true;
|
||
server.send(503, "text/plain", "SD busy"); return;
|
||
}
|
||
|
||
if (!SD_MMC.exists(filename)) {
|
||
xSemaphoreGive(sdMutex);
|
||
if (wasLogging) loggingEnabled = true;
|
||
server.send(404, "text/plain", "File not found"); return;
|
||
}
|
||
|
||
// ★ 다운로드 중 1-bit 모드 전환 (WiFi TX와 SD DMA 간섭 방지)
|
||
// 4-bit: D0~D3 동시 → WiFi RF 노이즈에 취약 → 0x109 DMA 타임아웃
|
||
// 1-bit: D0만 사용 → 노이즈 내성 대폭 향상
|
||
// 전환 절차: end() → setPins(D0만) → begin(1-bit)
|
||
{
|
||
SD_MMC.end();
|
||
delay(50);
|
||
SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0);
|
||
if (!SD_MMC.begin("/sdcard", true, false, 10000000)) {
|
||
xSemaphoreGive(sdMutex);
|
||
if (wasLogging) loggingEnabled = true;
|
||
server.send(500, "text/plain", "SD 1-bit init failed"); return;
|
||
}
|
||
Serial.println("✓ SD 1-bit 모드 전환 (다운로드용)");
|
||
}
|
||
|
||
File file = SD_MMC.open(filename, FILE_READ);
|
||
if (!file) {
|
||
xSemaphoreGive(sdMutex);
|
||
if (wasLogging) loggingEnabled = true;
|
||
server.send(500, "text/plain", "Failed to open file"); return;
|
||
}
|
||
|
||
size_t fileSize = file.size();
|
||
String dispName = server.arg("file");
|
||
server.setContentLength(fileSize);
|
||
server.sendHeader("Content-Disposition", "attachment; filename=\"" + dispName + "\"");
|
||
server.sendHeader("Content-Type", "application/octet-stream");
|
||
server.sendHeader("Connection", "close");
|
||
server.send(200, "application/octet-stream", "");
|
||
|
||
// ★ CHUNK 4KB: DMA 전송 크기를 줄여 SD 내부 타이밍 부담 감소
|
||
const size_t CHUNK = 4096;
|
||
uint8_t* buf = (uint8_t*)heap_caps_malloc(CHUNK, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
||
if (!buf) buf = (uint8_t*)malloc(CHUNK);
|
||
|
||
if (buf) {
|
||
size_t total = 0;
|
||
WiFiClient client = server.client();
|
||
client.setNoDelay(true);
|
||
|
||
// SD 읽기 실패 복구 람다 (SDMMC 페리페럴 완전 리셋)
|
||
// 0x109(ESP_ERR_TIMEOUT) 발생 후 seek+read 재시도는 무의미:
|
||
// SDMMC 하드웨어가 에러 상태로 굳어 있어서 end()+begin() 필수
|
||
auto sdReinit = [&]() -> bool {
|
||
Serial.println("⚠️ SDMMC 페리페럴 리셋 시도...");
|
||
file.close();
|
||
xSemaphoreGive(sdMutex); // 뮤텍스 해제 후 reinit
|
||
delay(200);
|
||
SD_MMC.end();
|
||
delay(100);
|
||
SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0); // 1-bit
|
||
if (!SD_MMC.begin("/sdcard", true, false, 10000000)) { // true = 1-bit
|
||
Serial.println("✗ SDMMC 재초기화 실패");
|
||
sdCardReady = false;
|
||
return false;
|
||
}
|
||
sdCardReady = true;
|
||
// 뮤텍스 재획득
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(3000)) != pdTRUE) {
|
||
Serial.println("✗ 뮤텍스 재획득 실패");
|
||
return false;
|
||
}
|
||
// 파일 재오픈 후 이어받기 위치로 seek
|
||
file = SD_MMC.open(filename, FILE_READ);
|
||
if (!file) {
|
||
Serial.println("✗ 파일 재오픈 실패");
|
||
xSemaphoreGive(sdMutex);
|
||
return false;
|
||
}
|
||
if (!file.seek(total)) {
|
||
Serial.println("✗ 파일 seek 실패");
|
||
file.close();
|
||
xSemaphoreGive(sdMutex);
|
||
return false;
|
||
}
|
||
Serial.printf("✓ SDMMC 리셋 완료, offset=%u 에서 재개", (unsigned)total);
|
||
return true;
|
||
};
|
||
|
||
int reinitCount = 0;
|
||
while (total < fileSize && client.connected()) {
|
||
size_t toRead = min((size_t)(fileSize - total), CHUNK);
|
||
size_t r = file.read(buf, toRead);
|
||
|
||
if (r == 0) {
|
||
// SDMMC 에러 상태 → 페리페럴 완전 리셋
|
||
if (reinitCount >= 3) {
|
||
Serial.println("✗ Download: SDMMC 리셋 3회 실패, 중단");
|
||
goto download_done;
|
||
}
|
||
reinitCount++;
|
||
Serial.printf("⚠️ SD 읽기 실패 (reinit %d), offset=%u",
|
||
reinitCount, (unsigned)total);
|
||
if (!sdReinit()) goto download_done;
|
||
continue; // 리셋 성공 → 같은 offset에서 재시도
|
||
}
|
||
reinitCount = 0; // 읽기 성공 시 카운터 리셋
|
||
|
||
size_t w = 0;
|
||
uint32_t writeStart = millis();
|
||
while (w < r) {
|
||
if (!client.connected()) {
|
||
Serial.println("⚠️ Download: 클라이언트 연결 끊김");
|
||
goto download_done;
|
||
}
|
||
size_t wr = client.write(buf + w, r - w);
|
||
if (wr > 0) {
|
||
w += wr;
|
||
writeStart = millis();
|
||
} else {
|
||
if (millis() - writeStart > 30000) {
|
||
Serial.println("⚠️ Download: 30초 타임아웃");
|
||
goto download_done;
|
||
}
|
||
yield();
|
||
delay(1);
|
||
}
|
||
}
|
||
total += r;
|
||
yield();
|
||
}
|
||
download_done:
|
||
free(buf);
|
||
Serial.printf("Download %s: %s (%u / %u bytes)\n",
|
||
(total == fileSize) ? "OK" : "PARTIAL",
|
||
filename.c_str(), (unsigned)total, (unsigned)fileSize);
|
||
}
|
||
|
||
if (file) file.close();
|
||
|
||
// ★ 다운로드 완료 후 4-bit 모드 복원 (로깅용)
|
||
{
|
||
SD_MMC.end();
|
||
delay(50);
|
||
SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3);
|
||
if (SD_MMC.begin("/sdcard", false, false, 10000000)) {
|
||
sdCardReady = true;
|
||
Serial.println("✓ SD 4-bit 모드 복원 (로깅용)");
|
||
} else {
|
||
sdCardReady = false;
|
||
Serial.println("✗ SD 4-bit 복원 실패");
|
||
}
|
||
}
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
|
||
// 로깅 재개
|
||
if (wasLogging) loggingEnabled = true;
|
||
});
|
||
|
||
server.begin();
|
||
Serial.println("✓ 웹 서버 시작");
|
||
|
||
// ★ 초경량 ISR 등록 (플래그 세팅만, 컨텍스트 스위치 없음)
|
||
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
|
||
|
||
// ========================================
|
||
// Task 생성
|
||
// ========================================
|
||
Serial.println("\nTask 생성 중...");
|
||
// ★ 중요: canRxTask(Core 1, Pri 24)는 반드시 마지막에 생성
|
||
// → 먼저 생성하면 setup()이 Core 1에서 선점당해 이후 태스크 생성 불가
|
||
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 12288, NULL, 8, &webTaskHandle, 0);
|
||
xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1);
|
||
xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1);
|
||
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1);
|
||
xTaskCreatePinnedToCore(sdFlushTask, "SD_FLUSH", 4096, NULL, 9, &sdFlushTaskHandle, 0);
|
||
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 18576, NULL, 8, &sdWriteTaskHandle, 0);
|
||
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 6, &serialRxTaskHandle, 0);
|
||
xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 6, &serial2RxTaskHandle, 0);
|
||
if (timeSyncStatus.rtcAvailable)
|
||
xTaskCreatePinnedToCore(rtcSyncTask,"RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0);
|
||
// ★ canRxTask 마지막 생성 (Core 1 Pri 24 → 이 시점엔 setup() 완료 직전)
|
||
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 12288, NULL, 24, &canRxTaskHandle, 1);
|
||
|
||
Serial.println("\n========================================");
|
||
Serial.println(" Task 구성 (RingBuffer v3.0)");
|
||
Serial.println("========================================");
|
||
Serial.println("Core 1:");
|
||
Serial.printf(" CAN_RX Pri 24 → canRing (SPSC push, no mutex)\n");
|
||
Serial.printf(" WEB_UPDATE Pri 8 ← serialRing (SPSC pop, no mutex)\n");
|
||
Serial.printf(" TX/SEQ/MON Pri 3/2/1\n");
|
||
Serial.println("Core 0:");
|
||
Serial.printf(" SD_FLUSH Pri 9 (Double Buffer 비동기 flush)\n");
|
||
Serial.printf(" SD_WRITE Pri 8 ← canRing (SPSC pop, BIN no mutex)\n");
|
||
Serial.printf(" SERIAL_RX Pri 6 → serialRing (SPSC push)\n");
|
||
Serial.printf(" SERIAL2_RX Pri 6 → serial2Ring(SPSC push)\n");
|
||
Serial.println("========================================");
|
||
Serial.printf("RingBuffer: CAN %d슬롯, SER %d슬롯 (PSRAM)\n",
|
||
CAN_RING_SIZE, SERIAL_RING_SIZE);
|
||
Serial.println("========================================\n");
|
||
}
|
||
|
||
// ========================================
|
||
// Loop
|
||
// ========================================
|
||
void loop() {
|
||
// ★ server.handleClient()는 webUpdateTask(Core 0)에서 처리
|
||
// loop()는 Core 1, Pri 1 → canRxTask(Pri 24)에 선점당해 실질적으로 실행 안됨
|
||
// 통계 출력만 여기서 하지 않고 vTaskDelay로 양보
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
|
||
// 30초마다 상태 출력 (RingBuffer 통계 포함)
|
||
static uint32_t lastPrint = 0;
|
||
if (millis() - lastPrint > 30000) {
|
||
Serial.printf("[RingBuf] CAN: %u/%u (drop:%u) | SER: %u/%u (drop:%u) | SER2: %u/%u (drop:%u) | PSRAM: %d KB\n",
|
||
ring_can_count(&canRing), CAN_RING_SIZE, canRing.dropped,
|
||
ring_serial_count(&serialRing), SERIAL_RING_SIZE, serialRing.dropped,
|
||
ring_serial_count(&serial2Ring), SERIAL_RING_SIZE, serial2Ring.dropped,
|
||
ESP.getFreePsram() / 1024);
|
||
lastPrint = millis();
|
||
}
|
||
|
||
// 5분마다 스택 사용량
|
||
static uint32_t lastStack = 0;
|
||
if (millis() - lastStack > 300000) {
|
||
Serial.println("\n========== Task Stack Usage ==========");
|
||
if (canRxTaskHandle) Serial.printf("CAN_RX: %5u bytes free\n", uxTaskGetStackHighWaterMark(canRxTaskHandle)*4);
|
||
if (sdWriteTaskHandle) Serial.printf("SD_WRITE: %5u bytes free\n", uxTaskGetStackHighWaterMark(sdWriteTaskHandle)*4);
|
||
if (webTaskHandle) Serial.printf("WEB_UPDATE: %5u bytes free\n", uxTaskGetStackHighWaterMark(webTaskHandle)*4);
|
||
if (serialRxTaskHandle) Serial.printf("SERIAL_RX: %5u bytes free\n", uxTaskGetStackHighWaterMark(serialRxTaskHandle)*4);
|
||
if (serial2RxTaskHandle) Serial.printf("SERIAL2_RX: %5u bytes free\n", uxTaskGetStackHighWaterMark(serial2RxTaskHandle)*4);
|
||
Serial.printf("Free Heap: %u bytes\n", ESP.getFreeHeap());
|
||
Serial.printf("Free PSRAM: %u KB\n", ESP.getFreePsram()/1024);
|
||
Serial.printf("CAN Dropped Total: %u\n", canRing.dropped);
|
||
Serial.println("======================================\n");
|
||
lastStack = millis();
|
||
}
|
||
} |