transmit 재 작성_can mode 추가

This commit is contained in:
2025-11-08 02:50:31 +00:00
parent 37da387904
commit b05983eb2d
3 changed files with 815 additions and 1086 deletions

View File

@@ -85,6 +85,33 @@ struct TxMessage {
bool active;
};
// CAN 시퀀스 스텝 구조체
struct SequenceStep {
uint32_t canId;
bool extended;
uint8_t dlc;
uint8_t data[8];
uint32_t delayMs; // 이 스텝 실행 후 대기 시간 (ms)
};
// CAN 시퀀스 구조체
struct CANSequence {
char name[32];
SequenceStep steps[20]; // 최대 20개 스텝
uint8_t stepCount;
uint8_t repeatMode; // 0=한번, 1=특정횟수, 2=무한
uint32_t repeatCount; // repeatMode=1일 때 반복 횟수
};
// 시퀀스 실행 상태
struct SequenceRuntime {
bool running;
uint8_t currentStep;
uint32_t currentRepeat;
uint32_t lastStepTime;
int8_t activeSequenceIndex; // 실행 중인 시퀀스 인덱스
};
// 파일 커멘트 구조체
struct FileComment {
char filename[MAX_FILENAME_LEN];
@@ -110,11 +137,21 @@ struct PowerStatus {
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_LOOPBACK = 2,
MCP_MODE_TRANSMIT = 3 // 송신만 (ACK 없음)
};
// WiFi AP 기본 설정
@@ -174,6 +211,12 @@ uint32_t lastMsgCount = 0;
TxMessage txMessages[MAX_TX_MESSAGES];
uint32_t totalTxCount = 0;
// CAN 시퀀스 (최대 10개 저장 가능)
#define MAX_SEQUENCES 10
CANSequence sequences[MAX_SEQUENCES];
uint8_t sequenceCount = 0;
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
// 파일 커멘트 저장 (최대 50개)
#define MAX_FILE_COMMENTS 50
FileComment fileComments[MAX_FILE_COMMENTS];
@@ -216,6 +259,92 @@ void saveSettings() {
Serial.println("⚠️ 재부팅 후 WiFi 설정이 적용됩니다.");
}
// ========================================
// 시퀀스 관리 함수
// ========================================
void loadSequences() {
if (!sdCardReady) return;
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
File seqFile = SD.open("/sequences.bin", FILE_READ);
if (seqFile) {
sequenceCount = 0;
while (seqFile.available() && sequenceCount < MAX_SEQUENCES) {
seqFile.read((uint8_t*)&sequences[sequenceCount], sizeof(CANSequence));
sequenceCount++;
}
seqFile.close();
Serial.printf("✓ 시퀀스 로드: %d개\n", sequenceCount);
}
xSemaphoreGive(sdMutex);
}
}
void saveSequences() {
if (!sdCardReady) return;
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
// 기존 파일 삭제
if (SD.exists("/sequences.bin")) {
SD.remove("/sequences.bin");
}
File seqFile = SD.open("/sequences.bin", FILE_WRITE);
if (seqFile) {
for (int i = 0; i < sequenceCount; i++) {
seqFile.write((uint8_t*)&sequences[i], sizeof(CANSequence));
}
seqFile.close();
Serial.println("✓ 시퀀스 저장 완료");
}
xSemaphoreGive(sdMutex);
}
}
int addSequence(const CANSequence& seq) {
if (sequenceCount >= MAX_SEQUENCES) {
return -1; // 가득 참
}
sequences[sequenceCount] = seq;
sequenceCount++;
saveSequences();
return sequenceCount - 1;
}
bool deleteSequence(uint8_t index) {
if (index >= sequenceCount) return false;
// 배열 왼쪽으로 시프트
for (int i = index; i < sequenceCount - 1; i++) {
sequences[i] = sequences[i + 1];
}
sequenceCount--;
saveSequences();
return true;
}
void stopSequence() {
seqRuntime.running = false;
seqRuntime.currentStep = 0;
seqRuntime.currentRepeat = 0;
seqRuntime.activeSequenceIndex = -1;
Serial.println("✓ 시퀀스 실행 중지");
}
void startSequence(uint8_t index) {
if (index >= sequenceCount) return;
seqRuntime.running = true;
seqRuntime.currentStep = 0;
seqRuntime.currentRepeat = 0;
seqRuntime.lastStepTime = millis();
seqRuntime.activeSequenceIndex = index;
Serial.printf("✓ 시퀀스 실행 시작: %s\n", sequences[index].name);
}
// ========================================
// 파일 커멘트 관리 함수
// ========================================
@@ -510,6 +639,19 @@ bool setMCP2515Mode(MCP2515Mode mode) {
return true;
}
break;
case MCP_MODE_TRANSMIT:
// Transmit mode: Normal 모드와 동일하게 동작
// (라이브러리 제약으로 인해 레지스터 직접 접근 불가)
// 송신 위주로 사용하되, 수신도 가능한 모드
result = mcp2515.setNormalMode();
if (result == MCP2515::ERROR_OK) {
currentMcpMode = MCP_MODE_TRANSMIT;
Serial.println("✓ MCP2515 모드: TRANSMIT (Normal mode base)");
Serial.println(" ※ 송신 위주 사용, 수신 데이터는 로깅하지 않음");
return true;
}
break;
}
Serial.println("✗ MCP2515 모드 변경 실패");
@@ -535,6 +677,15 @@ void canRxTask(void* parameter) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// Transmit 모드일 때는 수신 데이터를 처리하지 않음
if (currentMcpMode == MCP_MODE_TRANSMIT) {
// MCP2515 버퍼만 비우고 처리는 건너뜀
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
// 버퍼만 비움
}
continue;
}
// 한 번에 여러 메시지를 읽어서 처리 속도 향상
int readCount = 0;
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK && readCount < 20) {
@@ -547,10 +698,12 @@ void canRxTask(void* parameter) {
canMsg.dlc = frame.can_dlc;
memcpy(canMsg.data, frame.data, 8);
// 큐에 추가 (블로킹 없이)
xQueueSend(canQueue, &canMsg, 0);
// 로깅 중일 때만 큐에 추가
if (loggingEnabled) {
xQueueSend(canQueue, &canMsg, 0);
}
// 실시간 데이터 업데이트
// 실시간 데이터 업데이트 (웹 표시용)
bool found = false;
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
if (recentData[i].count > 0 && recentData[i].msg.id == canMsg.id) {
@@ -685,9 +838,16 @@ void stopLogging() {
bufferIndex = 0;
// 큐 비우기 (로깅 중이 아닐 때는 큐가 불필요)
CANMessage dummyMsg;
while (xQueueReceive(canQueue, &dummyMsg, 0) == pdTRUE) {
// 큐의 모든 메시지 제거
}
Serial.print("✓ 로깅 종료: ");
Serial.println(currentFilename);
Serial.printf(" 파일 크기: %u bytes\n", currentFileSize);
Serial.println("✓ 큐 비움 완료");
// 현재 파일명 초기화
currentFilename[0] = '\0';
@@ -725,6 +885,7 @@ void txTask(void* parameter) {
for (;;) {
uint32_t now = millis();
// 주기적 송신 (기존)
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
if (txMessages[i].active && txMessages[i].interval > 0) {
if (now - txMessages[i].lastSent >= txMessages[i].interval) {
@@ -747,6 +908,81 @@ void txTask(void* parameter) {
}
}
// ========================================
// 시퀀스 실행 태스크
// ========================================
void sequenceTask(void* parameter) {
struct can_frame frame;
for (;;) {
if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0) {
CANSequence* seq = &sequences[seqRuntime.activeSequenceIndex];
uint32_t now = millis();
if (seqRuntime.currentStep < seq->stepCount) {
SequenceStep* step = &seq->steps[seqRuntime.currentStep];
// 첫 번째 스텝이거나 딜레이 시간이 지났으면 실행
if (seqRuntime.currentStep == 0 || (now - seqRuntime.lastStepTime >= step->delayMs)) {
// CAN 메시지 전송
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);
MCP2515::ERROR result = mcp2515.sendMessage(&frame);
if (result == MCP2515::ERROR_OK) {
totalTxCount++;
Serial.printf(" [Seq] Step %d/%d: ID=0x%X, DLC=%d, Delay=%dms - OK\n",
seqRuntime.currentStep + 1, seq->stepCount,
step->canId, step->dlc, step->delayMs);
} else {
Serial.printf(" [Seq] Step %d/%d: ID=0x%X - FAIL (Error %d)\n",
seqRuntime.currentStep + 1, seq->stepCount, step->canId, result);
}
seqRuntime.currentStep++;
seqRuntime.lastStepTime = now;
}
} else {
// 모든 스텝 완료
seqRuntime.currentRepeat++;
// 반복 체크
bool shouldContinue = false;
if (seq->repeatMode == 0) {
// 한 번만
shouldContinue = false;
} else if (seq->repeatMode == 1) {
// 특정 횟수
if (seqRuntime.currentRepeat < seq->repeatCount) {
shouldContinue = true;
}
} else if (seq->repeatMode == 2) {
// 무한 반복
shouldContinue = true;
}
if (shouldContinue) {
seqRuntime.currentStep = 0;
seqRuntime.lastStepTime = now;
Serial.printf(" [Seq] 반복 %d회 시작\n", seqRuntime.currentRepeat + 1);
} else {
Serial.printf("✓ 시퀀스 실행 완료: %s (총 %d회 반복)\n",
seq->name, seqRuntime.currentRepeat);
stopSequence();
}
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// ========================================
// 파일 리스트 전송 함수
// ========================================
@@ -974,6 +1210,179 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length)
saveSettings();
webSocket.sendTXT(num, "{\"type\":\"settingsSaved\",\"success\":true}");
} else if (message.indexOf("\"cmd\":\"getSequences\"") >= 0) {
// 시퀀스 리스트 전송
String seqList = "{\"type\":\"sequences\",\"sequences\":[";
for (int i = 0; i < sequenceCount; i++) {
if (i > 0) seqList += ",";
seqList += "{\"index\":" + String(i);
seqList += ",\"name\":\"" + String(sequences[i].name) + "\"";
seqList += ",\"steps\":" + String(sequences[i].stepCount);
seqList += ",\"mode\":" + String(sequences[i].repeatMode);
seqList += ",\"count\":" + String(sequences[i].repeatCount) + "}";
}
seqList += "]}";
webSocket.sendTXT(num, seqList);
} else if (message.indexOf("\"cmd\":\"getSequence\"") >= 0) {
// 특정 시퀀스 상세 정보 전송
int indexStart = message.indexOf("\"index\":") + 8;
int indexEnd = message.indexOf(",", indexStart);
if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart);
int index = message.substring(indexStart, indexEnd).toInt();
if (index >= 0 && index < sequenceCount) {
String seqData = "{\"type\":\"sequenceDetail\",\"sequence\":{";
seqData += "\"name\":\"" + String(sequences[index].name) + "\",";
seqData += "\"mode\":" + String(sequences[index].repeatMode) + ",";
seqData += "\"count\":" + String(sequences[index].repeatCount) + ",";
seqData += "\"steps\":[";
for (int i = 0; i < sequences[index].stepCount; i++) {
if (i > 0) seqData += ",";
SequenceStep* step = &sequences[index].steps[i];
seqData += "{\"id\":" + String(step->canId);
seqData += ",\"ext\":" + String(step->extended ? "true" : "false");
seqData += ",\"dlc\":" + String(step->dlc);
seqData += ",\"data\":\"";
for (int j = 0; j < 8; j++) {
if (step->data[j] < 0x10) seqData += "0";
seqData += String(step->data[j], HEX);
if (j < 7) seqData += " ";
}
seqData += "\",\"delay\":" + String(step->delayMs) + "}";
}
seqData += "]}}";
webSocket.sendTXT(num, seqData);
}
} else if (message.indexOf("\"cmd\":\"saveSequence\"") >= 0) {
// 새 시퀀스 저장 (JSON 파싱)
CANSequence newSeq;
memset(&newSeq, 0, sizeof(CANSequence));
// 이름 추출
int nameStart = message.indexOf("\"name\":\"") + 8;
int nameEnd = message.indexOf("\"", nameStart);
String name = message.substring(nameStart, nameEnd);
strncpy(newSeq.name, name.c_str(), sizeof(newSeq.name) - 1);
// 모드 추출
int modeStart = message.indexOf("\"mode\":") + 7;
int modeEnd = message.indexOf(",", modeStart);
newSeq.repeatMode = message.substring(modeStart, modeEnd).toInt();
// 반복 횟수 추출
int countStart = message.indexOf("\"repeatCount\":") + 14;
int countEnd = message.indexOf(",", countStart);
if (countEnd < 0) countEnd = message.indexOf("}", countStart);
newSeq.repeatCount = message.substring(countStart, countEnd).toInt();
// 스텝 배열 파싱
int stepsStart = message.indexOf("\"steps\":[");
if (stepsStart >= 0) {
stepsStart += 9; // "steps":[ 길이
int stepsEnd = message.indexOf("]}", stepsStart);
String stepsJson = message.substring(stepsStart, stepsEnd);
// 각 스텝 파싱
newSeq.stepCount = 0;
int pos = 0;
while (pos < stepsJson.length() && newSeq.stepCount < 20) {
int stepStart = stepsJson.indexOf("{", pos);
if (stepStart < 0) break;
int stepEnd = stepsJson.indexOf("}", stepStart);
if (stepEnd < 0) break;
String stepJson = stepsJson.substring(stepStart, stepEnd + 1);
// canId 추출
int idStart = stepJson.indexOf("\"canId\":") + 8;
int idEnd = stepJson.indexOf(",", idStart);
if (idEnd < 0) idEnd = stepJson.indexOf("}", idStart);
newSeq.steps[newSeq.stepCount].canId = stepJson.substring(idStart, idEnd).toInt();
// extended 추출
int extStart = stepJson.indexOf("\"extended\":") + 11;
String extStr = stepJson.substring(extStart, extStart + 5);
newSeq.steps[newSeq.stepCount].extended = (extStr.indexOf("true") >= 0);
// dlc 추출
int dlcStart = stepJson.indexOf("\"dlc\":") + 6;
int dlcEnd = stepJson.indexOf(",", dlcStart);
newSeq.steps[newSeq.stepCount].dlc = stepJson.substring(dlcStart, dlcEnd).toInt();
// data 배열 추출
int dataStart = stepJson.indexOf("\"data\":[") + 8;
int dataEnd = stepJson.indexOf("]", dataStart);
String dataStr = stepJson.substring(dataStart, dataEnd);
// data 바이트 파싱
int bytePos = 0;
int byteIdx = 0;
while (bytePos < dataStr.length() && byteIdx < 8) {
int commaPos = dataStr.indexOf(",", bytePos);
if (commaPos < 0) commaPos = dataStr.length();
String byteStr = dataStr.substring(bytePos, commaPos);
byteStr.trim();
newSeq.steps[newSeq.stepCount].data[byteIdx] = byteStr.toInt();
byteIdx++;
bytePos = commaPos + 1;
}
// delay 추출
int delayStart = stepJson.indexOf("\"delayMs\":") + 10;
int delayEnd = stepJson.indexOf(",", delayStart);
if (delayEnd < 0) delayEnd = stepJson.indexOf("}", delayStart);
newSeq.steps[newSeq.stepCount].delayMs = stepJson.substring(delayStart, delayEnd).toInt();
newSeq.stepCount++;
pos = stepEnd + 1;
}
}
Serial.printf("📝 시퀀스 저장: %s (%d 스텝)\n", newSeq.name, newSeq.stepCount);
for (int i = 0; i < newSeq.stepCount; i++) {
Serial.printf(" Step %d: ID=0x%X, DLC=%d, Delay=%dms\n",
i, newSeq.steps[i].canId, newSeq.steps[i].dlc, newSeq.steps[i].delayMs);
}
int result = addSequence(newSeq);
if (result >= 0) {
webSocket.sendTXT(num, "{\"type\":\"sequenceSaved\",\"success\":true,\"index\":" + String(result) + "}");
} else {
webSocket.sendTXT(num, "{\"type\":\"sequenceSaved\",\"success\":false}");
}
} else if (message.indexOf("\"cmd\":\"deleteSequence\"") >= 0) {
// 시퀀스 삭제
int indexStart = message.indexOf("\"index\":") + 8;
int indexEnd = message.indexOf(",", indexStart);
if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart);
int index = message.substring(indexStart, indexEnd).toInt();
bool success = deleteSequence(index);
webSocket.sendTXT(num, "{\"type\":\"sequenceDeleted\",\"success\":" + String(success ? "true" : "false") + "}");
} else if (message.indexOf("\"cmd\":\"startSequence\"") >= 0) {
// 시퀀스 실행
int indexStart = message.indexOf("\"index\":") + 8;
int indexEnd = message.indexOf(",", indexStart);
if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart);
int index = message.substring(indexStart, indexEnd).toInt();
startSequence(index);
webSocket.sendTXT(num, "{\"type\":\"sequenceStarted\",\"success\":true}");
} else if (message.indexOf("\"cmd\":\"stopSequence\"") >= 0) {
// 시퀀스 중지
stopSequence();
webSocket.sendTXT(num, "{\"type\":\"sequenceStopped\",\"success\":true}");
}
}
}
@@ -1247,6 +1656,7 @@ void setup() {
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0);
xTaskCreatePinnedToCore(txTask, "TX_TASK", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4096, NULL, 2, NULL, 1); // 시퀀스 Task 추가
// RTC 동기화 Task
if (timeSyncStatus.rtcAvailable) {
@@ -1254,6 +1664,9 @@ void setup() {
Serial.println("✓ RTC 자동 동기화 Task 시작");
}
// 시퀀스 로드
loadSequences();
Serial.println("✓ 모든 태스크 시작 완료");
Serial.println("\n========================================");
Serial.println(" 웹 인터페이스 접속 방법");

View File

@@ -694,6 +694,7 @@ const char index_html[] PROGMEM = R"rawliteral(
<option value="0" selected>Normal</option>
<option value="1">Listen-Only</option>
<option value="2">Loopback</option>
<option value="3">Transmit (TX Focus)</option>
</select>
<button onclick="setMcpMode()">Apply</button>
<span id="mode-status" style="color: #11998e; font-size: 0.85em; font-weight: 600;"></span>
@@ -751,7 +752,7 @@ const char index_html[] PROGMEM = R"rawliteral(
let messageOrder = [];
let lastMessageData = {};
const speedNames = {0: '125K', 1: '250K', 2: '500K', 3: '1M'};
const modeNames = {0: 'NORMAL', 1: 'LISTEN-ONLY', 2: 'LOOPBACK'};
const modeNames = {0: 'NORMAL', 1: 'LISTEN-ONLY', 2: 'LOOPBACK', 3: 'TRANSMIT'};
let currentLoggingFile = '';
let commentingFile = '';
let hasInitialSync = false; // 초기 동기화 완료 여부

1477
transmit.h

File diff suppressed because it is too large Load Diff