1883 lines
68 KiB
C++
1883 lines
68 KiB
C++
/*
|
|
* Byun CAN Logger with Web Interface + Serial Terminal
|
|
* Version: 2.1
|
|
* Added: Serial communication (RS232) with web terminal interface
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#include <SPI.h>
|
|
#include <mcp2515.h>
|
|
#include <SoftWire.h>
|
|
#include <SD.h>
|
|
#include <WiFi.h>
|
|
#include <esp_wifi.h>
|
|
#include <esp_task_wdt.h>
|
|
#include <WebServer.h>
|
|
#include <WebSocketsServer.h>
|
|
#include <ArduinoJson.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
#include <freertos/queue.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"
|
|
|
|
// GPIO 핀 정의
|
|
#define CAN_INT_PIN 4
|
|
|
|
// Serial 통신 핀 (추가)
|
|
#define SERIAL_TX_PIN 17
|
|
#define SERIAL_RX_PIN 18
|
|
|
|
// HSPI 핀 (CAN)
|
|
#define HSPI_MISO 13
|
|
#define HSPI_MOSI 11
|
|
#define HSPI_SCLK 12
|
|
#define HSPI_CS 10
|
|
|
|
// VSPI 핀 (SD Card)
|
|
#define VSPI_MISO 41
|
|
#define VSPI_MOSI 40
|
|
#define VSPI_SCLK 39
|
|
#define VSPI_CS 42
|
|
|
|
// I2C2 핀 (RTC DS3231) - SoftWire 사용
|
|
#define RTC_SDA 8
|
|
#define RTC_SCL 9
|
|
#define DS3231_ADDRESS 0x68
|
|
|
|
// 버퍼 설정
|
|
#define CAN_QUEUE_SIZE 1000 // 1500 → 1000으로 축소
|
|
#define FILE_BUFFER_SIZE 8192 // 16384 → 8192로 축소
|
|
#define MAX_FILENAME_LEN 64
|
|
#define RECENT_MSG_COUNT 50 // 100 → 50으로 축소
|
|
#define MAX_TX_MESSAGES 20
|
|
#define MAX_COMMENT_LEN 128
|
|
|
|
// Serial 버퍼 설정 (추가)
|
|
#define SERIAL_QUEUE_SIZE 100 // 200 → 100으로 축소
|
|
#define SERIAL_BUFFER_SIZE 1024 // 2048 → 1024로 축소
|
|
#define MAX_SERIAL_LINE_LEN 128
|
|
|
|
// RTC 동기화 설정
|
|
#define RTC_SYNC_INTERVAL_MS 60000
|
|
|
|
// 전력 모니터링 설정
|
|
#define VOLTAGE_CHECK_INTERVAL_MS 5000
|
|
#define LOW_VOLTAGE_THRESHOLD 3.0
|
|
#define MONITORING_VOLT 5
|
|
// CAN 메시지 구조체
|
|
struct CANMessage {
|
|
uint64_t timestamp_us;
|
|
uint32_t id;
|
|
uint8_t dlc;
|
|
uint8_t data[8];
|
|
} __attribute__((packed));
|
|
|
|
// Serial 메시지 구조체 (추가)
|
|
struct SerialMessage {
|
|
uint64_t timestamp_us;
|
|
uint16_t length;
|
|
uint8_t data[MAX_SERIAL_LINE_LEN];
|
|
bool isTx; // true=송신, false=수신
|
|
} __attribute__((packed));
|
|
|
|
// Serial 설정 구조체 (추가)
|
|
struct SerialSettings {
|
|
uint32_t baudRate;
|
|
uint8_t dataBits; // 5, 6, 7, 8
|
|
uint8_t parity; // 0=None, 1=Even, 2=Odd
|
|
uint8_t stopBits; // 1, 2
|
|
} serialSettings = {115200, 8, 0, 1};
|
|
|
|
// 실시간 모니터링용 구조체
|
|
struct RecentCANData {
|
|
CANMessage msg;
|
|
uint32_t count;
|
|
};
|
|
|
|
// CAN 송신용 구조체
|
|
struct TxMessage {
|
|
uint32_t id;
|
|
bool extended;
|
|
uint8_t dlc;
|
|
uint8_t data[8];
|
|
uint32_t interval;
|
|
uint32_t lastSent;
|
|
bool active;
|
|
};
|
|
|
|
// CAN 시퀀스 스텝 구조체
|
|
struct SequenceStep {
|
|
uint32_t canId;
|
|
bool extended;
|
|
uint8_t dlc;
|
|
uint8_t data[8];
|
|
uint32_t delayMs;
|
|
};
|
|
|
|
// CAN 시퀀스 구조체
|
|
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];
|
|
};
|
|
|
|
// 시간 동기화 상태
|
|
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};
|
|
|
|
// MCP2515 레지스터 주소 정의
|
|
#ifndef MCP_CANCTRL
|
|
#define MCP_CANCTRL 0x0F
|
|
#endif
|
|
|
|
#ifndef MCP_CANSTAT
|
|
#define MCP_CANSTAT 0x0E
|
|
#endif
|
|
|
|
// MCP2515 모드 정의
|
|
enum MCP2515Mode {
|
|
MCP_MODE_NORMAL = 0,
|
|
MCP_MODE_LISTEN_ONLY = 1,
|
|
MCP_MODE_LOOPBACK = 2,
|
|
MCP_MODE_TRANSMIT = 3
|
|
};
|
|
|
|
// WiFi AP 기본 설정
|
|
char wifiSSID[32] = "Byun_CAN_Logger";
|
|
char wifiPassword[64] = "12345678";
|
|
|
|
// WiFi Station 모드 설정
|
|
bool enableSTAMode = false;
|
|
char staSSID[32] = "";
|
|
char staPassword[64] = "";
|
|
|
|
// 전역 변수
|
|
SPIClass hspi(HSPI);
|
|
SPIClass vspi(FSPI);
|
|
MCP2515 mcp2515(HSPI_CS, 20000000, &hspi);
|
|
|
|
// Serial 통신용 (추가)
|
|
HardwareSerial SerialComm(1); // UART1 사용
|
|
|
|
WebServer server(80);
|
|
WebSocketsServer webSocket = WebSocketsServer(81);
|
|
Preferences preferences;
|
|
|
|
// Forward declaration
|
|
void IRAM_ATTR canISR();
|
|
|
|
QueueHandle_t canQueue;
|
|
QueueHandle_t serialQueue; // Serial Queue 추가
|
|
SemaphoreHandle_t sdMutex;
|
|
SemaphoreHandle_t rtcMutex;
|
|
SemaphoreHandle_t serialMutex; // Serial Mutex 추가
|
|
TaskHandle_t canRxTaskHandle = NULL;
|
|
TaskHandle_t sdWriteTaskHandle = NULL;
|
|
TaskHandle_t webTaskHandle = NULL;
|
|
TaskHandle_t rtcTaskHandle = NULL;
|
|
TaskHandle_t serialRxTaskHandle = NULL; // Serial Task 추가
|
|
|
|
volatile bool loggingEnabled = false;
|
|
volatile bool serialLoggingEnabled = false; // Serial 로깅 상태 추가
|
|
volatile bool sdCardReady = false;
|
|
File logFile;
|
|
File serialLogFile; // Serial 로그 파일 추가
|
|
char currentFilename[MAX_FILENAME_LEN];
|
|
char currentSerialFilename[MAX_FILENAME_LEN]; // Serial 로그 파일명 추가
|
|
uint8_t fileBuffer[FILE_BUFFER_SIZE];
|
|
uint8_t serialFileBuffer[SERIAL_BUFFER_SIZE]; // Serial 파일 버퍼 추가
|
|
uint16_t bufferIndex = 0;
|
|
uint16_t serialBufferIndex = 0; // Serial 버퍼 인덱스 추가
|
|
|
|
// 로깅 파일 크기 추적
|
|
volatile uint32_t currentFileSize = 0;
|
|
volatile uint32_t currentSerialFileSize = 0; // Serial 파일 크기 추가
|
|
|
|
// 현재 MCP2515 모드
|
|
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;
|
|
|
|
// RTC 관련
|
|
SoftWire rtcWire(RTC_SDA, RTC_SCL);
|
|
char rtcSyncBuffer[20];
|
|
|
|
// CAN 속도 설정
|
|
CAN_SPEED currentCanSpeed = CAN_1000KBPS;
|
|
const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"};
|
|
CAN_SPEED canSpeedValues[] = {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS};
|
|
|
|
// 실시간 모니터링용
|
|
RecentCANData recentData[RECENT_MSG_COUNT];
|
|
uint32_t totalMsgCount = 0;
|
|
uint32_t msgPerSecond = 0;
|
|
uint32_t lastMsgCountTime = 0;
|
|
uint32_t lastMsgCount = 0;
|
|
|
|
// Serial 통신 카운터 (추가)
|
|
volatile uint32_t totalSerialRxCount = 0;
|
|
volatile uint32_t totalSerialTxCount = 0;
|
|
|
|
// 그래프 최대 개수
|
|
#define MAX_GRAPH_SIGNALS 20
|
|
|
|
// CAN 송신용
|
|
TxMessage txMessages[MAX_TX_MESSAGES];
|
|
uint32_t totalTxCount = 0;
|
|
|
|
// CAN 시퀀스
|
|
#define MAX_SEQUENCES 10
|
|
CANSequence sequences[MAX_SEQUENCES];
|
|
uint8_t sequenceCount = 0;
|
|
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
|
|
|
|
// 파일 커멘트 저장
|
|
#define MAX_FILE_COMMENTS 50
|
|
FileComment fileComments[MAX_FILE_COMMENTS];
|
|
int commentCount = 0;
|
|
|
|
// ========================================
|
|
// Serial 설정 저장/로드 함수 (추가)
|
|
// ========================================
|
|
|
|
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);
|
|
|
|
Serial.printf("✓ Serial 설정 로드: %u-%u-%u-%u\n",
|
|
serialSettings.baudRate, serialSettings.dataBits,
|
|
serialSettings.parity, serialSettings.stopBits);
|
|
}
|
|
|
|
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);
|
|
|
|
Serial.printf("✓ Serial 설정 저장: %u-%u-%u-%u\n",
|
|
serialSettings.baudRate, serialSettings.dataBits,
|
|
serialSettings.parity, serialSettings.stopBits);
|
|
}
|
|
|
|
void applySerialSettings() {
|
|
uint32_t config = SERIAL_8N1;
|
|
|
|
// Data bits + Parity 설정
|
|
if (serialSettings.dataBits == 5) {
|
|
if (serialSettings.parity == 0) config = SERIAL_5N1;
|
|
else if (serialSettings.parity == 1) config = SERIAL_5E1;
|
|
else if (serialSettings.parity == 2) config = SERIAL_5O1;
|
|
} else if (serialSettings.dataBits == 6) {
|
|
if (serialSettings.parity == 0) config = SERIAL_6N1;
|
|
else if (serialSettings.parity == 1) config = SERIAL_6E1;
|
|
else if (serialSettings.parity == 2) config = SERIAL_6O1;
|
|
} else if (serialSettings.dataBits == 7) {
|
|
if (serialSettings.parity == 0) config = SERIAL_7N1;
|
|
else if (serialSettings.parity == 1) config = SERIAL_7E1;
|
|
else if (serialSettings.parity == 2) config = SERIAL_7O1;
|
|
} else { // 8 bits
|
|
if (serialSettings.parity == 0) config = SERIAL_8N1;
|
|
else if (serialSettings.parity == 1) config = SERIAL_8E1;
|
|
else if (serialSettings.parity == 2) config = SERIAL_8O1;
|
|
}
|
|
|
|
// Stop bits 설정
|
|
if (serialSettings.stopBits == 2) {
|
|
config |= 0x3000; // 2 stop bits
|
|
}
|
|
|
|
SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN);
|
|
SerialComm.setRxBufferSize(2048);
|
|
|
|
Serial.printf("✓ Serial 설정 적용: %u baud, config=0x%X\n",
|
|
serialSettings.baudRate, config);
|
|
}
|
|
|
|
// ========================================
|
|
// 설정 저장/로드 함수
|
|
// ========================================
|
|
|
|
void loadSettings() {
|
|
preferences.begin("can-logger", false);
|
|
|
|
// WiFi AP 설정 로드
|
|
preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID));
|
|
preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword));
|
|
|
|
// WiFi STA 모드 설정 로드
|
|
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");
|
|
}
|
|
|
|
// CAN 속도 로드
|
|
int speedIndex = preferences.getInt("can_speed", 3);
|
|
if (speedIndex >= 0 && speedIndex < 4) {
|
|
currentCanSpeed = canSpeedValues[speedIndex];
|
|
Serial.printf("✓ 저장된 CAN 속도 로드: %s\n", canSpeedNames[speedIndex]);
|
|
}
|
|
|
|
// MCP2515 모드 로드
|
|
int savedMode = preferences.getInt("mcp_mode", 0);
|
|
if (savedMode >= 0 && savedMode <= 3) {
|
|
currentMcpMode = (MCP2515Mode)savedMode;
|
|
Serial.printf("✓ 저장된 MCP 모드 로드: %d\n", savedMode);
|
|
}
|
|
|
|
// Serial 설정 로드 (추가)
|
|
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);
|
|
|
|
// CAN 속도 저장
|
|
for (int i = 0; i < 4; i++) {
|
|
if (canSpeedValues[i] == currentCanSpeed) {
|
|
preferences.putInt("can_speed", i);
|
|
Serial.printf("✓ CAN 속도 저장: %s (인덱스 %d)\n", canSpeedNames[i], i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// MCP2515 모드 저장
|
|
preferences.putInt("mcp_mode", (int)currentMcpMode);
|
|
Serial.printf("✓ MCP 모드 저장: %d\n", (int)currentMcpMode);
|
|
|
|
preferences.end();
|
|
|
|
Serial.println("✓ 설정 저장 완료");
|
|
}
|
|
|
|
// ========================================
|
|
// RTC 함수
|
|
// ========================================
|
|
|
|
void initRTC() {
|
|
rtcWire.begin();
|
|
rtcWire.setClock(100000);
|
|
|
|
rtcWire.beginTransmission(DS3231_ADDRESS);
|
|
if (rtcWire.endTransmission() == 0) {
|
|
timeSyncStatus.rtcAvailable = true;
|
|
Serial.println("✓ RTC(DS3231) 감지됨");
|
|
} else {
|
|
timeSyncStatus.rtcAvailable = false;
|
|
Serial.println("! RTC(DS3231) 없음 - 시간 동기화 필요");
|
|
}
|
|
}
|
|
|
|
uint8_t bcdToDec(uint8_t val) {
|
|
return (val >> 4) * 10 + (val & 0x0F);
|
|
}
|
|
|
|
uint8_t decToBcd(uint8_t val) {
|
|
return ((val / 10) << 4) | (val % 10);
|
|
}
|
|
|
|
bool readRTC(struct tm *timeinfo) {
|
|
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 buffer[7];
|
|
for (int i = 0; i < 7; i++) {
|
|
buffer[i] = rtcWire.read();
|
|
}
|
|
|
|
xSemaphoreGive(rtcMutex);
|
|
|
|
timeinfo->tm_sec = bcdToDec(buffer[0] & 0x7F);
|
|
timeinfo->tm_min = bcdToDec(buffer[1] & 0x7F);
|
|
timeinfo->tm_hour = bcdToDec(buffer[2] & 0x3F);
|
|
timeinfo->tm_wday = bcdToDec(buffer[3] & 0x07) - 1;
|
|
timeinfo->tm_mday = bcdToDec(buffer[4] & 0x3F);
|
|
timeinfo->tm_mon = bcdToDec(buffer[5] & 0x1F) - 1;
|
|
timeinfo->tm_year = bcdToDec(buffer[6]) + 100;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool writeRTC(const struct tm *timeinfo) {
|
|
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(timeinfo->tm_sec));
|
|
rtcWire.write(decToBcd(timeinfo->tm_min));
|
|
rtcWire.write(decToBcd(timeinfo->tm_hour));
|
|
rtcWire.write(decToBcd(timeinfo->tm_wday + 1));
|
|
rtcWire.write(decToBcd(timeinfo->tm_mday));
|
|
rtcWire.write(decToBcd(timeinfo->tm_mon + 1));
|
|
rtcWire.write(decToBcd(timeinfo->tm_year - 100));
|
|
|
|
bool success = (rtcWire.endTransmission() == 0);
|
|
|
|
xSemaphoreGive(rtcMutex);
|
|
|
|
return success;
|
|
}
|
|
|
|
void rtcSyncTask(void *parameter) {
|
|
const TickType_t xDelay = pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS);
|
|
|
|
while (1) {
|
|
if (timeSyncStatus.rtcAvailable) {
|
|
struct tm timeinfo;
|
|
if (readRTC(&timeinfo)) {
|
|
time_t now = mktime(&timeinfo);
|
|
struct timeval tv = { .tv_sec = now, .tv_usec = 0 };
|
|
settimeofday(&tv, NULL);
|
|
|
|
timeSyncStatus.synchronized = true;
|
|
timeSyncStatus.lastSyncTime = (uint64_t)now * 1000000ULL;
|
|
timeSyncStatus.rtcSyncCount++;
|
|
}
|
|
}
|
|
|
|
vTaskDelay(xDelay);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// MCP2515 모드 설정
|
|
// ========================================
|
|
|
|
bool setMCP2515Mode(MCP2515Mode mode) {
|
|
const char* modeName;
|
|
MCP2515::ERROR result;
|
|
|
|
switch (mode) {
|
|
case MCP_MODE_NORMAL:
|
|
result = mcp2515.setNormalMode();
|
|
modeName = "Normal";
|
|
break;
|
|
case MCP_MODE_LISTEN_ONLY:
|
|
result = mcp2515.setListenOnlyMode();
|
|
modeName = "Listen-Only";
|
|
break;
|
|
case MCP_MODE_LOOPBACK:
|
|
result = mcp2515.setLoopbackMode();
|
|
modeName = "Loopback";
|
|
break;
|
|
case MCP_MODE_TRANSMIT:
|
|
result = mcp2515.setNormalMode(); // Transmit는 Normal 모드 사용
|
|
modeName = "Transmit";
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (result == MCP2515::ERROR_OK) {
|
|
currentMcpMode = mode;
|
|
Serial.printf("✓ MCP2515 모드 변경: %s\n", modeName);
|
|
return true;
|
|
} else {
|
|
Serial.printf("✗ MCP2515 모드 변경 실패: %s (error=%d)\n", modeName, result);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// CAN 인터럽트 핸들러
|
|
// ========================================
|
|
|
|
void IRAM_ATTR canISR() {
|
|
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
|
if (canRxTaskHandle != NULL) {
|
|
vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken);
|
|
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Serial 수신 Task (추가)
|
|
// ========================================
|
|
|
|
void serialRxTask(void *parameter) {
|
|
SerialMessage serialMsg;
|
|
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
|
|
uint16_t lineIndex = 0;
|
|
|
|
Serial.println("✓ Serial RX Task 시작");
|
|
|
|
while (1) {
|
|
// Serial 데이터 수신
|
|
while (SerialComm.available()) {
|
|
uint8_t c = SerialComm.read();
|
|
|
|
// 바이너리 모드로 처리 (라인 단위)
|
|
lineBuffer[lineIndex++] = c;
|
|
|
|
// 개행 문자 또는 버퍼 가득 참
|
|
if (c == '\n' || c == '\r' || lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
|
|
if (lineIndex > 0) {
|
|
// 타임스탬프 생성
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
|
|
|
serialMsg.length = lineIndex;
|
|
memcpy(serialMsg.data, lineBuffer, lineIndex);
|
|
serialMsg.isTx = false; // 수신 데이터
|
|
|
|
// Queue에 전송
|
|
if (xQueueSend(serialQueue, &serialMsg, 0) == pdTRUE) {
|
|
totalSerialRxCount++;
|
|
}
|
|
|
|
lineIndex = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms 간격
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// CAN 수신 Task
|
|
// ========================================
|
|
|
|
void canRxTask(void *parameter) {
|
|
struct can_frame frame;
|
|
CANMessage msg;
|
|
|
|
Serial.println("✓ CAN RX Task 시작");
|
|
|
|
while (1) {
|
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
|
|
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
|
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.dlc = frame.can_dlc;
|
|
memcpy(msg.data, frame.data, 8);
|
|
|
|
if (xQueueSend(canQueue, &msg, 0) == pdTRUE) {
|
|
totalMsgCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// SD 쓰기 Task (CAN + Serial 동시 지원)
|
|
// ========================================
|
|
|
|
void sdWriteTask(void *parameter) {
|
|
CANMessage canMsg;
|
|
SerialMessage serialMsg;
|
|
|
|
Serial.println("✓ SD Write Task 시작");
|
|
|
|
while (1) {
|
|
bool hasWork = false;
|
|
|
|
// CAN 메시지 처리
|
|
if (xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// CAN 로깅
|
|
if (loggingEnabled && sdCardReady) {
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
if (bufferIndex + sizeof(CANMessage) <= FILE_BUFFER_SIZE) {
|
|
memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage));
|
|
bufferIndex += sizeof(CANMessage);
|
|
currentFileSize += sizeof(CANMessage);
|
|
}
|
|
|
|
if (bufferIndex >= FILE_BUFFER_SIZE - sizeof(CANMessage)) {
|
|
if (logFile) {
|
|
logFile.write(fileBuffer, bufferIndex);
|
|
logFile.flush();
|
|
bufferIndex = 0;
|
|
}
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Serial 메시지 처리 (추가)
|
|
if (xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) {
|
|
hasWork = true;
|
|
|
|
// Serial 로깅
|
|
if (serialLoggingEnabled && sdCardReady) {
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
if (serialBufferIndex + sizeof(SerialMessage) <= SERIAL_BUFFER_SIZE) {
|
|
memcpy(&serialFileBuffer[serialBufferIndex], &serialMsg, sizeof(SerialMessage));
|
|
serialBufferIndex += sizeof(SerialMessage);
|
|
currentSerialFileSize += sizeof(SerialMessage);
|
|
}
|
|
|
|
if (serialBufferIndex >= SERIAL_BUFFER_SIZE - sizeof(SerialMessage)) {
|
|
if (serialLogFile) {
|
|
serialLogFile.write(serialFileBuffer, serialBufferIndex);
|
|
serialLogFile.flush();
|
|
serialBufferIndex = 0;
|
|
}
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasWork) {
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// SD 모니터링 Task
|
|
// ========================================
|
|
|
|
void sdMonitorTask(void *parameter) {
|
|
const TickType_t xDelay = pdMS_TO_TICKS(1000);
|
|
|
|
while (1) {
|
|
if (!sdCardReady) {
|
|
if (SD.begin(VSPI_CS, vspi)) {
|
|
sdCardReady = true;
|
|
Serial.println("✓ SD 카드 재연결 감지");
|
|
}
|
|
}
|
|
|
|
uint32_t currentTime = millis();
|
|
|
|
// 메시지/초 계산
|
|
if (currentTime - lastMsgCountTime >= 1000) {
|
|
msgPerSecond = totalMsgCount - lastMsgCount;
|
|
lastMsgCount = totalMsgCount;
|
|
lastMsgCountTime = currentTime;
|
|
}
|
|
|
|
// 전압 체크
|
|
if (currentTime - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) {
|
|
float rawVoltage = analogRead(MONITORING_VOLT) * (3.3 / 4095.0);
|
|
powerStatus.voltage = rawVoltage * 1.0;
|
|
|
|
// 1초 단위 최소값 업데이트
|
|
if (currentTime - powerStatus.lastMinReset >= 1000) {
|
|
powerStatus.minVoltage = powerStatus.voltage;
|
|
powerStatus.lastMinReset = currentTime;
|
|
} else {
|
|
if (powerStatus.voltage < powerStatus.minVoltage) {
|
|
powerStatus.minVoltage = powerStatus.voltage;
|
|
}
|
|
}
|
|
|
|
powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD);
|
|
powerStatus.lastCheck = currentTime;
|
|
}
|
|
|
|
vTaskDelay(xDelay);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// 파일 커멘트 관리
|
|
// ========================================
|
|
|
|
void saveFileComments() {
|
|
if (!sdCardReady) return;
|
|
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
File commentFile = SD.open("/comments.dat", FILE_WRITE);
|
|
if (commentFile) {
|
|
commentFile.write((uint8_t*)&commentCount, sizeof(commentCount));
|
|
commentFile.write((uint8_t*)fileComments, sizeof(FileComment) * commentCount);
|
|
commentFile.close();
|
|
Serial.println("✓ 파일 커멘트 저장 완료");
|
|
}
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
|
|
void loadFileComments() {
|
|
if (!sdCardReady) return;
|
|
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
if (SD.exists("/comments.dat")) {
|
|
File commentFile = SD.open("/comments.dat", FILE_READ);
|
|
if (commentFile) {
|
|
commentFile.read((uint8_t*)&commentCount, sizeof(commentCount));
|
|
if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS;
|
|
commentFile.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount);
|
|
commentFile.close();
|
|
Serial.printf("✓ 파일 커멘트 %d개 로드 완료\n", commentCount);
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// CAN 시퀀스 관리
|
|
// ========================================
|
|
|
|
void saveSequences() {
|
|
if (!sdCardReady) return;
|
|
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
File seqFile = SD.open("/sequences.dat", FILE_WRITE);
|
|
if (seqFile) {
|
|
seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount));
|
|
seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
|
|
seqFile.close();
|
|
Serial.println("✓ 시퀀스 저장 완료");
|
|
}
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
|
|
void loadSequences() {
|
|
if (!sdCardReady) return;
|
|
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
if (SD.exists("/sequences.dat")) {
|
|
File seqFile = SD.open("/sequences.dat", FILE_READ);
|
|
if (seqFile) {
|
|
seqFile.read((uint8_t*)&sequenceCount, sizeof(sequenceCount));
|
|
if (sequenceCount > MAX_SEQUENCES) sequenceCount = MAX_SEQUENCES;
|
|
seqFile.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
|
|
seqFile.close();
|
|
Serial.printf("✓ 시퀀스 %d개 로드 완료\n", sequenceCount);
|
|
}
|
|
}
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// CAN TX Task
|
|
// ========================================
|
|
|
|
void txTask(void *parameter) {
|
|
struct can_frame frame;
|
|
|
|
Serial.println("✓ TX Task 시작");
|
|
|
|
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) {
|
|
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 (anyActive) {
|
|
vTaskDelay(pdMS_TO_TICKS(1));
|
|
} else {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// 시퀀스 Task
|
|
// ========================================
|
|
|
|
void sequenceTask(void *parameter) {
|
|
Serial.println("✓ Sequence Task 시작");
|
|
|
|
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) {
|
|
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++;
|
|
}
|
|
|
|
seqRuntime.currentStep++;
|
|
seqRuntime.lastStepTime = now;
|
|
}
|
|
} else {
|
|
if (seq->repeatMode == 0) {
|
|
seqRuntime.running = false;
|
|
} else if (seq->repeatMode == 1) {
|
|
seqRuntime.currentRepeat++;
|
|
if (seqRuntime.currentRepeat >= seq->repeatCount) {
|
|
seqRuntime.running = false;
|
|
} else {
|
|
seqRuntime.currentStep = 0;
|
|
seqRuntime.lastStepTime = now;
|
|
}
|
|
} else if (seq->repeatMode == 2) {
|
|
seqRuntime.currentStep = 0;
|
|
seqRuntime.lastStepTime = now;
|
|
}
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(1));
|
|
} else {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// WebSocket 이벤트 처리 (Serial 명령 추가)
|
|
// ========================================
|
|
|
|
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
|
|
if (type == WStype_TEXT) {
|
|
DynamicJsonDocument doc(2048);
|
|
DeserializationError error = deserializeJson(doc, payload);
|
|
|
|
if (error) {
|
|
Serial.print("✗ JSON 파싱 실패: ");
|
|
Serial.println(error.c_str());
|
|
return;
|
|
}
|
|
|
|
const char* cmd = doc["cmd"];
|
|
|
|
if (strcmp(cmd, "startLogging") == 0) {
|
|
if (!loggingEnabled && sdCardReady) {
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
struct tm timeinfo;
|
|
time_t now;
|
|
time(&now);
|
|
localtime_r(&now, &timeinfo);
|
|
|
|
snprintf(currentFilename, sizeof(currentFilename),
|
|
"/CAN_%04d%02d%02d_%02d%02d%02d.bin",
|
|
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
|
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
|
|
|
logFile = SD.open(currentFilename, FILE_WRITE);
|
|
|
|
if (logFile) {
|
|
loggingEnabled = true;
|
|
bufferIndex = 0;
|
|
currentFileSize = 0;
|
|
Serial.printf("✓ CAN 로깅 시작: %s\n", currentFilename);
|
|
} else {
|
|
Serial.println("✗ 파일 생성 실패");
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "stopLogging") == 0) {
|
|
if (loggingEnabled) {
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
if (bufferIndex > 0 && logFile) {
|
|
logFile.write(fileBuffer, bufferIndex);
|
|
bufferIndex = 0;
|
|
}
|
|
|
|
if (logFile) {
|
|
logFile.close();
|
|
}
|
|
|
|
loggingEnabled = false;
|
|
Serial.printf("✓ CAN 로깅 종료: %s (%u bytes)\n", currentFilename, currentFileSize);
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "startSerialLogging") == 0) { // Serial 로깅 시작
|
|
if (!serialLoggingEnabled && sdCardReady) {
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
struct tm timeinfo;
|
|
time_t now;
|
|
time(&now);
|
|
localtime_r(&now, &timeinfo);
|
|
|
|
snprintf(currentSerialFilename, sizeof(currentSerialFilename),
|
|
"/SER_%04d%02d%02d_%02d%02d%02d.bin",
|
|
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
|
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
|
|
|
serialLogFile = SD.open(currentSerialFilename, FILE_WRITE);
|
|
|
|
if (serialLogFile) {
|
|
serialLoggingEnabled = true;
|
|
serialBufferIndex = 0;
|
|
currentSerialFileSize = 0;
|
|
Serial.printf("✓ Serial 로깅 시작: %s\n", currentSerialFilename);
|
|
} else {
|
|
Serial.println("✗ Serial 파일 생성 실패");
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "stopSerialLogging") == 0) { // Serial 로깅 종료
|
|
if (serialLoggingEnabled) {
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
if (serialBufferIndex > 0 && serialLogFile) {
|
|
serialLogFile.write(serialFileBuffer, serialBufferIndex);
|
|
serialBufferIndex = 0;
|
|
}
|
|
|
|
if (serialLogFile) {
|
|
serialLogFile.close();
|
|
}
|
|
|
|
serialLoggingEnabled = false;
|
|
Serial.printf("✓ Serial 로깅 종료: %s (%u bytes)\n",
|
|
currentSerialFilename, currentSerialFileSize);
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "sendSerial") == 0) { // Serial 데이터 전송
|
|
const char* data = doc["data"];
|
|
if (data && strlen(data) > 0) {
|
|
SerialComm.println(data);
|
|
|
|
// 송신 데이터를 Queue에 추가 (모니터링용)
|
|
SerialMessage serialMsg;
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
|
serialMsg.length = strlen(data);
|
|
if (serialMsg.length > MAX_SERIAL_LINE_LEN - 1) {
|
|
serialMsg.length = MAX_SERIAL_LINE_LEN - 1;
|
|
}
|
|
memcpy(serialMsg.data, data, serialMsg.length);
|
|
serialMsg.isTx = true; // 송신 데이터
|
|
|
|
xQueueSend(serialQueue, &serialMsg, 0);
|
|
totalSerialTxCount++;
|
|
|
|
Serial.printf("→ Serial TX: %s\n", data);
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "setSerialConfig") == 0) { // Serial 설정 변경
|
|
uint32_t baud = doc["baudRate"] | 115200;
|
|
uint8_t data = doc["dataBits"] | 8;
|
|
uint8_t parity = doc["parity"] | 0;
|
|
uint8_t stop = doc["stopBits"] | 1;
|
|
|
|
serialSettings.baudRate = baud;
|
|
serialSettings.dataBits = data;
|
|
serialSettings.parity = parity;
|
|
serialSettings.stopBits = stop;
|
|
|
|
saveSerialSettings();
|
|
applySerialSettings();
|
|
|
|
Serial.printf("✓ Serial 설정 변경: %u-%u-%u-%u\n", baud, data, parity, stop);
|
|
}
|
|
else if (strcmp(cmd, "getSerialConfig") == 0) { // Serial 설정 조회
|
|
DynamicJsonDocument response(512);
|
|
response["type"] = "serialConfig";
|
|
response["baudRate"] = serialSettings.baudRate;
|
|
response["dataBits"] = serialSettings.dataBits;
|
|
response["parity"] = serialSettings.parity;
|
|
response["stopBits"] = serialSettings.stopBits;
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
else if (strcmp(cmd, "setSpeed") == 0) {
|
|
int speedIndex = doc["speed"];
|
|
if (speedIndex >= 0 && speedIndex < 4) {
|
|
currentCanSpeed = canSpeedValues[speedIndex];
|
|
mcp2515.reset();
|
|
mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
|
|
setMCP2515Mode(currentMcpMode);
|
|
|
|
saveSettings();
|
|
|
|
Serial.printf("✓ CAN 속도 변경: %s\n", canSpeedNames[speedIndex]);
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "setMcpMode") == 0) {
|
|
int mode = doc["mode"];
|
|
if (mode >= 0 && mode <= 3) {
|
|
setMCP2515Mode((MCP2515Mode)mode);
|
|
saveSettings();
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "syncTime") == 0) {
|
|
uint64_t phoneTime = doc["time"];
|
|
if (phoneTime > 0) {
|
|
time_t seconds = phoneTime / 1000;
|
|
suseconds_t microseconds = (phoneTime % 1000) * 1000;
|
|
|
|
struct timeval tv = {seconds, microseconds};
|
|
settimeofday(&tv, NULL);
|
|
|
|
timeSyncStatus.synchronized = true;
|
|
timeSyncStatus.lastSyncTime = phoneTime * 1000;
|
|
timeSyncStatus.syncCount++;
|
|
|
|
if (timeSyncStatus.rtcAvailable) {
|
|
struct tm timeinfo;
|
|
localtime_r(&seconds, &timeinfo);
|
|
writeRTC(&timeinfo);
|
|
Serial.println("✓ 시간 동기화 완료 (Phone → ESP32 → RTC)");
|
|
} else {
|
|
Serial.println("✓ 시간 동기화 완료 (Phone → ESP32)");
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "getFiles") == 0) {
|
|
if (sdCardReady) {
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
DynamicJsonDocument response(6144);
|
|
response["type"] = "files";
|
|
JsonArray files = response.createNestedArray("list");
|
|
|
|
File root = SD.open("/");
|
|
File file = root.openNextFile();
|
|
|
|
while (file) {
|
|
if (!file.isDirectory()) {
|
|
const char* filename = file.name();
|
|
|
|
if (filename[0] != '.' &&
|
|
strcmp(filename, "System Volume Information") != 0) {
|
|
|
|
JsonObject fileObj = files.createNestedObject();
|
|
fileObj["name"] = filename;
|
|
fileObj["size"] = file.size();
|
|
|
|
const char* comment = getFileComment(filename);
|
|
if (strlen(comment) > 0) {
|
|
fileObj["comment"] = comment;
|
|
}
|
|
}
|
|
}
|
|
file = root.openNextFile();
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "deleteFile") == 0) {
|
|
const char* filename = doc["filename"];
|
|
|
|
if (filename && strlen(filename) > 0) {
|
|
if (loggingEnabled && currentFilename[0] != '\0') {
|
|
String currentFileStr = String(currentFilename);
|
|
if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1);
|
|
|
|
if (strcmp(filename, currentFileStr.c_str()) == 0) {
|
|
DynamicJsonDocument response(256);
|
|
response["type"] = "deleteResult";
|
|
response["success"] = false;
|
|
response["message"] = "Cannot delete file currently being logged";
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
return;
|
|
}
|
|
}
|
|
|
|
String fullPath = "/" + String(filename);
|
|
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
bool success = false;
|
|
String message = "";
|
|
|
|
if (SD.exists(fullPath)) {
|
|
if (SD.remove(fullPath)) {
|
|
success = true;
|
|
message = "File deleted successfully";
|
|
Serial.printf("✓ 파일 삭제: %s\n", filename);
|
|
} else {
|
|
message = "Failed to delete file";
|
|
}
|
|
} else {
|
|
message = "File not found";
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
|
|
DynamicJsonDocument response(256);
|
|
response["type"] = "deleteResult";
|
|
response["success"] = success;
|
|
response["message"] = message;
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
|
|
if (success) {
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
|
|
DynamicJsonDocument filesDoc(6144);
|
|
filesDoc["type"] = "files";
|
|
JsonArray files = filesDoc.createNestedArray("list");
|
|
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
File root = SD.open("/");
|
|
File file = root.openNextFile();
|
|
|
|
while (file) {
|
|
if (!file.isDirectory()) {
|
|
const char* fname = file.name();
|
|
|
|
if (fname[0] != '.' &&
|
|
strcmp(fname, "System Volume Information") != 0) {
|
|
|
|
JsonObject fileObj = files.createNestedObject();
|
|
fileObj["name"] = fname;
|
|
fileObj["size"] = file.size();
|
|
|
|
const char* comment = getFileComment(fname);
|
|
if (strlen(comment) > 0) {
|
|
fileObj["comment"] = comment;
|
|
}
|
|
}
|
|
}
|
|
file = root.openNextFile();
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
|
|
String filesJson;
|
|
serializeJson(filesDoc, filesJson);
|
|
webSocket.sendTXT(num, filesJson);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "addComment") == 0) {
|
|
const char* filename = doc["filename"];
|
|
const char* comment = doc["comment"];
|
|
|
|
if (filename && comment) {
|
|
addFileComment(filename, comment);
|
|
Serial.printf("✓ 커멘트 추가: %s\n", filename);
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
DynamicJsonDocument response(6144);
|
|
response["type"] = "files";
|
|
JsonArray files = response.createNestedArray("list");
|
|
|
|
File root = SD.open("/");
|
|
File file = root.openNextFile();
|
|
|
|
while (file) {
|
|
if (!file.isDirectory()) {
|
|
const char* fname = file.name();
|
|
|
|
if (fname[0] != '.' &&
|
|
strcmp(fname, "System Volume Information") != 0) {
|
|
|
|
JsonObject fileObj = files.createNestedObject();
|
|
fileObj["name"] = fname;
|
|
fileObj["size"] = file.size();
|
|
|
|
const char* fcomment = getFileComment(fname);
|
|
if (strlen(fcomment) > 0) {
|
|
fileObj["comment"] = fcomment;
|
|
}
|
|
}
|
|
}
|
|
file = root.openNextFile();
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "addTx") == 0) {
|
|
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
|
if (!txMessages[i].active) {
|
|
txMessages[i].id = strtoul(doc["id"], NULL, 16);
|
|
txMessages[i].extended = doc["ext"] | false;
|
|
txMessages[i].dlc = doc["dlc"] | 8;
|
|
|
|
JsonArray dataArray = doc["data"];
|
|
for (int j = 0; j < 8; j++) {
|
|
txMessages[i].data[j] = dataArray[j] | 0;
|
|
}
|
|
|
|
txMessages[i].interval = doc["interval"] | 1000;
|
|
txMessages[i].active = true;
|
|
txMessages[i].lastSent = 0;
|
|
|
|
Serial.printf("✓ TX 메시지 추가: ID=0x%X\n", txMessages[i].id);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "removeTx") == 0) {
|
|
uint32_t id = strtoul(doc["id"], NULL, 16);
|
|
|
|
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
|
if (txMessages[i].active && txMessages[i].id == id) {
|
|
txMessages[i].active = false;
|
|
Serial.printf("✓ TX 메시지 제거: ID=0x%X\n", id);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "updateTx") == 0) {
|
|
uint32_t id = strtoul(doc["id"], NULL, 16);
|
|
|
|
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
|
if (txMessages[i].active && txMessages[i].id == id) {
|
|
txMessages[i].interval = doc["interval"];
|
|
Serial.printf("✓ TX 주기 변경: ID=0x%X, Interval=%u\n",
|
|
id, txMessages[i].interval);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "sendOnce") == 0) {
|
|
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 dataArray = doc["data"];
|
|
for (int i = 0; i < 8; i++) {
|
|
frame.data[i] = dataArray[i] | 0;
|
|
}
|
|
|
|
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
|
|
totalTxCount++;
|
|
Serial.printf("✓ CAN 메시지 전송: ID=0x%X\n", frame.can_id & 0x1FFFFFFF);
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "addSequence") == 0) {
|
|
if (sequenceCount < MAX_SEQUENCES) {
|
|
CANSequence* seq = &sequences[sequenceCount];
|
|
|
|
strncpy(seq->name, doc["name"] | "Unnamed", sizeof(seq->name) - 1);
|
|
seq->name[sizeof(seq->name) - 1] = '\0';
|
|
|
|
JsonArray stepsArray = doc["steps"];
|
|
seq->stepCount = min((int)stepsArray.size(), 20);
|
|
|
|
for (int i = 0; i < seq->stepCount; i++) {
|
|
JsonObject stepObj = stepsArray[i];
|
|
seq->steps[i].canId = strtoul(stepObj["id"], NULL, 16);
|
|
seq->steps[i].extended = stepObj["ext"] | false;
|
|
seq->steps[i].dlc = stepObj["dlc"] | 8;
|
|
|
|
JsonArray dataArray = stepObj["data"];
|
|
for (int j = 0; j < 8; j++) {
|
|
seq->steps[i].data[j] = dataArray[j] | 0;
|
|
}
|
|
|
|
seq->steps[i].delayMs = stepObj["delay"] | 0;
|
|
}
|
|
|
|
seq->repeatMode = doc["repeatMode"] | 0;
|
|
seq->repeatCount = doc["repeatCount"] | 1;
|
|
|
|
sequenceCount++;
|
|
saveSequences();
|
|
|
|
Serial.printf("✓ 시퀀스 추가: %s (%d steps)\n", seq->name, seq->stepCount);
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "removeSequence") == 0) {
|
|
int index = doc["index"];
|
|
|
|
if (index >= 0 && index < sequenceCount) {
|
|
for (int i = index; i < sequenceCount - 1; i++) {
|
|
sequences[i] = sequences[i + 1];
|
|
}
|
|
sequenceCount--;
|
|
saveSequences();
|
|
|
|
Serial.printf("✓ 시퀀스 삭제: index=%d\n", index);
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "startSequence") == 0) {
|
|
int index = doc["index"];
|
|
|
|
if (index >= 0 && index < sequenceCount && !seqRuntime.running) {
|
|
seqRuntime.running = true;
|
|
seqRuntime.currentStep = 0;
|
|
seqRuntime.currentRepeat = 0;
|
|
seqRuntime.lastStepTime = millis();
|
|
seqRuntime.activeSequenceIndex = index;
|
|
|
|
Serial.printf("✓ 시퀀스 시작: %s\n", sequences[index].name);
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "stopSequence") == 0) {
|
|
if (seqRuntime.running) {
|
|
seqRuntime.running = false;
|
|
Serial.println("✓ 시퀀스 중지");
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "getSequences") == 0) {
|
|
DynamicJsonDocument response(3072);
|
|
response["type"] = "sequences";
|
|
JsonArray seqArray = response.createNestedArray("list");
|
|
|
|
for (int i = 0; i < sequenceCount; i++) {
|
|
JsonObject seqObj = seqArray.createNestedObject();
|
|
seqObj["name"] = sequences[i].name;
|
|
seqObj["steps"] = sequences[i].stepCount;
|
|
seqObj["repeatMode"] = sequences[i].repeatMode;
|
|
seqObj["repeatCount"] = sequences[i].repeatCount;
|
|
}
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// 웹 업데이트 Task (Serial 데이터 전송 추가)
|
|
// ========================================
|
|
|
|
void webUpdateTask(void *parameter) {
|
|
const TickType_t xDelay = pdMS_TO_TICKS(100);
|
|
|
|
Serial.println("✓ Web Update Task 시작");
|
|
|
|
while (1) {
|
|
webSocket.loop();
|
|
|
|
// CAN 데이터 전송
|
|
if (webSocket.connectedClients() > 0) {
|
|
DynamicJsonDocument doc(3072); // 4096 → 3072로 축소
|
|
doc["type"] = "update";
|
|
doc["logging"] = loggingEnabled;
|
|
doc["serialLogging"] = serialLoggingEnabled; // Serial 로깅 상태 추가
|
|
doc["sdReady"] = sdCardReady;
|
|
doc["totalMsg"] = totalMsgCount;
|
|
doc["msgPerSec"] = msgPerSecond;
|
|
doc["totalTx"] = totalTxCount;
|
|
doc["totalSerialRx"] = totalSerialRxCount; // Serial RX 카운터 추가
|
|
doc["totalSerialTx"] = totalSerialTxCount; // Serial TX 카운터 추가
|
|
doc["fileSize"] = currentFileSize;
|
|
doc["serialFileSize"] = currentSerialFileSize; // Serial 파일 크기 추가
|
|
doc["queueUsed"] = uxQueueMessagesWaiting(canQueue);
|
|
doc["queueSize"] = CAN_QUEUE_SIZE;
|
|
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); // Serial Queue 사용량 추가
|
|
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE;
|
|
doc["timeSync"] = timeSyncStatus.synchronized;
|
|
doc["rtcAvail"] = timeSyncStatus.rtcAvailable;
|
|
doc["rtcSyncCnt"] = timeSyncStatus.rtcSyncCount;
|
|
doc["voltage"] = powerStatus.voltage;
|
|
doc["minVoltage"] = powerStatus.minVoltage;
|
|
doc["lowVoltage"] = powerStatus.lowVoltage;
|
|
doc["mcpMode"] = (int)currentMcpMode;
|
|
|
|
time_t now;
|
|
time(&now);
|
|
doc["timestamp"] = (uint64_t)now;
|
|
|
|
JsonArray messages = doc.createNestedArray("messages");
|
|
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
|
if (recentData[i].count > 0) {
|
|
JsonObject msgObj = messages.createNestedObject();
|
|
msgObj["id"] = recentData[i].msg.id;
|
|
msgObj["dlc"] = recentData[i].msg.dlc;
|
|
msgObj["count"] = recentData[i].count;
|
|
|
|
JsonArray dataArray = msgObj.createNestedArray("data");
|
|
for (int j = 0; j < recentData[i].msg.dlc; j++) {
|
|
dataArray.add(recentData[i].msg.data[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Serial 메시지 전송 (추가)
|
|
SerialMessage serialMsg;
|
|
JsonArray serialMessages = doc.createNestedArray("serialMessages");
|
|
int serialCount = 0;
|
|
|
|
while (serialCount < 5 && xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) { // 10 → 5개로 축소
|
|
JsonObject serMsgObj = serialMessages.createNestedObject();
|
|
serMsgObj["timestamp"] = serialMsg.timestamp_us;
|
|
serMsgObj["isTx"] = serialMsg.isTx;
|
|
|
|
// 데이터를 문자열로 변환 (printable characters)
|
|
char dataStr[MAX_SERIAL_LINE_LEN + 1];
|
|
memcpy(dataStr, serialMsg.data, serialMsg.length);
|
|
dataStr[serialMsg.length] = '\0';
|
|
serMsgObj["data"] = dataStr;
|
|
|
|
serialCount++;
|
|
}
|
|
|
|
String json;
|
|
serializeJson(doc, json);
|
|
webSocket.broadcastTXT(json);
|
|
}
|
|
|
|
vTaskDelay(xDelay);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Setup
|
|
// ========================================
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(1000);
|
|
|
|
Serial.println("\n========================================");
|
|
Serial.println(" Byun CAN Logger + Serial Terminal");
|
|
Serial.println(" Version 2.1 - ESP32-S3 Edition");
|
|
Serial.println("========================================\n");
|
|
|
|
loadSettings();
|
|
analogSetPinAttenuation(MONITORING_VOLT, ADC_11db);
|
|
Serial.println("💡 설정 변경: http://[IP]/settings\n");
|
|
|
|
memset(recentData, 0, sizeof(recentData));
|
|
memset(txMessages, 0, sizeof(txMessages));
|
|
memset(fileComments, 0, sizeof(fileComments));
|
|
|
|
pinMode(CAN_INT_PIN, INPUT_PULLUP);
|
|
|
|
// ADC 설정
|
|
analogSetAttenuation(ADC_11db);
|
|
|
|
// SPI 초기화 (먼저 완료)
|
|
Serial.println("SPI 초기화 중...");
|
|
hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
|
|
hspi.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0));
|
|
hspi.endTransaction();
|
|
pinMode(VSPI_CS, OUTPUT);
|
|
digitalWrite(VSPI_CS, HIGH);
|
|
delay(100);
|
|
|
|
vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS);
|
|
vspi.setFrequency(40000000);
|
|
Serial.println("✓ SPI 초기화 완료");
|
|
|
|
// Watchdog 완전 비활성화 (SPI 초기화 후)
|
|
Serial.println("Watchdog 비활성화...");
|
|
esp_task_wdt_deinit();
|
|
Serial.println("✓ Watchdog 비활성화 완료");
|
|
|
|
// MCP2515 초기화 (간소화)
|
|
Serial.println("MCP2515 초기화 중...");
|
|
mcp2515.reset();
|
|
delay(50);
|
|
|
|
// Bitrate만 설정 (모드는 나중에)
|
|
MCP2515::ERROR result = mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
|
|
if (result != MCP2515::ERROR_OK) {
|
|
Serial.printf("! MCP2515 Bitrate 설정 실패: %d (계속 진행)\n", result);
|
|
}
|
|
|
|
// Normal 모드로 직접 설정 (함수 호출 안함)
|
|
mcp2515.setNormalMode();
|
|
Serial.println("✓ MCP2515 초기화 완료");
|
|
|
|
// Serial 통신 초기화 (추가)
|
|
applySerialSettings();
|
|
Serial.println("✓ Serial 통신 초기화 완료 (UART1)");
|
|
|
|
// Mutex 생성
|
|
sdMutex = xSemaphoreCreateMutex();
|
|
rtcMutex = xSemaphoreCreateMutex();
|
|
serialMutex = xSemaphoreCreateMutex();
|
|
|
|
if (sdMutex == NULL || rtcMutex == NULL || serialMutex == NULL) {
|
|
Serial.println("✗ Mutex 생성 실패!");
|
|
while (1) delay(1000);
|
|
}
|
|
|
|
// RTC 초기화
|
|
initRTC();
|
|
|
|
// SD 카드 초기화
|
|
if (SD.begin(VSPI_CS, vspi)) {
|
|
sdCardReady = true;
|
|
Serial.println("✓ SD 카드 초기화 완료");
|
|
loadFileComments();
|
|
} else {
|
|
Serial.println("✗ SD 카드 초기화 실패");
|
|
}
|
|
|
|
// WiFi 설정
|
|
if (enableSTAMode && strlen(staSSID) > 0) {
|
|
Serial.println("\n📶 WiFi APSTA 모드 시작...");
|
|
WiFi.mode(WIFI_AP_STA);
|
|
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); // 채널1, 최대4개연결
|
|
Serial.print("✓ AP SSID: ");
|
|
Serial.println(wifiSSID);
|
|
Serial.print("✓ AP IP: ");
|
|
Serial.println(WiFi.softAPIP());
|
|
|
|
Serial.printf("📡 WiFi 연결 시도: %s\n", staSSID);
|
|
WiFi.begin(staSSID, staPassword);
|
|
|
|
int attempts = 0;
|
|
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
|
delay(500);
|
|
Serial.print(".");
|
|
attempts++;
|
|
}
|
|
Serial.println();
|
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
Serial.println("✓ WiFi 연결 성공!");
|
|
Serial.print("✓ STA IP: ");
|
|
Serial.println(WiFi.localIP());
|
|
} else {
|
|
Serial.println("✗ WiFi 연결 실패 (AP 모드는 정상 동작)");
|
|
}
|
|
} else {
|
|
Serial.println("\n📶 WiFi AP 모드 시작...");
|
|
WiFi.mode(WIFI_AP);
|
|
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); // 채널1, 최대4개연결
|
|
Serial.print("✓ AP SSID: ");
|
|
Serial.println(wifiSSID);
|
|
Serial.print("✓ AP IP: ");
|
|
Serial.println(WiFi.softAPIP());
|
|
}
|
|
|
|
WiFi.setSleep(false);
|
|
esp_wifi_set_max_tx_power(84);
|
|
|
|
// WebSocket 시작
|
|
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, []() { // Serial 페이지 추가
|
|
server.send_P(200, "text/html", serial_terminal_html);
|
|
});
|
|
|
|
server.on("/download", HTTP_GET, []() {
|
|
if (server.hasArg("file")) {
|
|
String filename = "/" + server.arg("file");
|
|
|
|
if (SD.exists(filename)) {
|
|
File file = SD.open(filename, FILE_READ);
|
|
if (file) {
|
|
String displayName = server.arg("file");
|
|
server.sendHeader("Content-Disposition",
|
|
"attachment; filename=\"" + displayName + "\"");
|
|
server.sendHeader("Content-Type", "application/octet-stream");
|
|
server.streamFile(file, "application/octet-stream");
|
|
file.close();
|
|
} else {
|
|
server.send(500, "text/plain", "Failed to open file");
|
|
}
|
|
} else {
|
|
server.send(404, "text/plain", "File not found");
|
|
}
|
|
} else {
|
|
server.send(400, "text/plain", "Bad request");
|
|
}
|
|
});
|
|
|
|
server.on("/delete", HTTP_GET, []() {
|
|
if (server.hasArg("file")) {
|
|
String filename = server.arg("file");
|
|
|
|
if (loggingEnabled && currentFilename[0] != '\0') {
|
|
String currentFileStr = String(currentFilename);
|
|
if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1);
|
|
|
|
if (filename == currentFileStr) {
|
|
server.send(403, "text/plain", "Cannot delete file currently being logged");
|
|
return;
|
|
}
|
|
}
|
|
|
|
String fullPath = "/" + filename;
|
|
|
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
|
if (SD.exists(fullPath)) {
|
|
if (SD.remove(fullPath)) {
|
|
xSemaphoreGive(sdMutex);
|
|
server.send(200, "text/plain", "File deleted successfully");
|
|
Serial.printf("✓ HTTP 파일 삭제: %s\n", filename.c_str());
|
|
} else {
|
|
xSemaphoreGive(sdMutex);
|
|
server.send(500, "text/plain", "Failed to delete file");
|
|
}
|
|
} else {
|
|
xSemaphoreGive(sdMutex);
|
|
server.send(404, "text/plain", "File not found");
|
|
}
|
|
} else {
|
|
server.send(503, "text/plain", "SD card busy");
|
|
}
|
|
} else {
|
|
server.send(400, "text/plain", "Bad request");
|
|
}
|
|
});
|
|
|
|
server.begin();
|
|
|
|
// Queue 생성
|
|
canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage));
|
|
serialQueue = xQueueCreate(SERIAL_QUEUE_SIZE, sizeof(SerialMessage)); // Serial Queue 생성
|
|
|
|
if (canQueue == NULL || serialQueue == NULL) {
|
|
Serial.println("✗ Queue 생성 실패!");
|
|
while (1) delay(1000);
|
|
}
|
|
|
|
// CAN 인터럽트 활성화
|
|
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
|
|
|
|
// Task 생성 (메모리 최적화)
|
|
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 8096, NULL, 5, &canRxTaskHandle, 1); // 4096 → 3072
|
|
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 4072, NULL, 4, &serialRxTaskHandle, 1); // 3072 → 2560
|
|
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 15240, NULL, 3, &sdWriteTaskHandle, 1); // 10240 → 8192
|
|
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 5072, NULL, 1, NULL, 1); // 3072 → 2560
|
|
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0); // 8192 → 6144
|
|
xTaskCreatePinnedToCore(txTask, "TX_TASK", 5072, NULL, 2, NULL, 1); // 3072 → 2560
|
|
xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4072, NULL, 2, NULL, 1); // 3072 → 2560
|
|
|
|
// RTC 동기화 Task
|
|
if (timeSyncStatus.rtcAvailable) {
|
|
xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 4096, NULL, 0, &rtcTaskHandle, 0);
|
|
Serial.println("✓ RTC 자동 동기화 Task 시작");
|
|
}
|
|
|
|
// 시퀀스 로드
|
|
loadSequences();
|
|
|
|
Serial.println("✓ 모든 태스크 시작 완료");
|
|
Serial.println("\n========================================");
|
|
Serial.println(" 웹 인터페이스 접속 방법");
|
|
Serial.println("========================================");
|
|
Serial.printf(" 1. WiFi 연결\n");
|
|
Serial.printf(" - SSID : %s\n", wifiSSID);
|
|
Serial.printf(" - Password : %s\n", wifiPassword);
|
|
Serial.printf(" 2. 웹 브라우저에서 접속\n");
|
|
Serial.print(" - URL : http://");
|
|
Serial.println(WiFi.softAPIP());
|
|
Serial.println(" 3. 페이지 메뉴:");
|
|
Serial.println(" - Monitor : /");
|
|
Serial.println(" - Transmit : /transmit");
|
|
Serial.println(" - Graph : /graph");
|
|
Serial.println(" - Settings : /settings");
|
|
Serial.println(" - Serial : /serial ← NEW!");
|
|
Serial.println("========================================\n");
|
|
}
|
|
|
|
void loop() {
|
|
server.handleClient();
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
|
|
static uint32_t lastPrint = 0;
|
|
if (millis() - lastPrint > 30000) {
|
|
Serial.printf("[상태] CAN큐: %d/%d | Serial큐: %d/%d | CAN로깅: %s | Serial로깅: %s | SD: %s | CAN RX: %lu | CAN TX: %lu | Serial RX: %lu | Serial TX: %lu | 모드: %d\n",
|
|
uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
|
|
uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE,
|
|
loggingEnabled ? "ON " : "OFF",
|
|
serialLoggingEnabled ? "ON " : "OFF",
|
|
sdCardReady ? "OK" : "NO",
|
|
totalMsgCount, totalTxCount,
|
|
totalSerialRxCount, totalSerialTxCount,
|
|
currentMcpMode);
|
|
lastPrint = millis();
|
|
}
|
|
}
|