#include "modbus.h" #include "config.h" // --- 물리적 GPIO 및 통신 상수 정의 (현장 배선 사양 적용) --- #define MODBUS_TX_PIN 27 #define MODBUS_RX_PIN 26 #define MODBUS_DE_PIN 4 #define MODBUS_BAUDRATE 115200 // Forward declarations for MQTT recovery/reboot functions void trigger_plc_reboot_event(); void trigger_plc_recovery(); void publish_ack(const char* command_id, const char* status); // M 모멘터리 펌스의 ON~OFF 실제 소요 시간 추적용 static bool m_pulse_release_pending = false; static unsigned long m_pulse_started_at = 0; static char m_pulse_command_id[40] = ""; // --- [G1] Modbus RTU / RS-485 초기화 (상수 정의 사용) --- void setup_modbus() { Serial2.begin(MODBUS_BAUDRATE, SERIAL_8N1, MODBUS_RX_PIN, MODBUS_TX_PIN); pinMode(MODBUS_DE_PIN, OUTPUT); digitalWrite(MODBUS_DE_PIN, LOW); // 기본 상태: 수신 모드 Serial.printf("[MODBUS SETUP] Serial2 initialized at %d bps (RX:%d, TX:%d, DE:%d)\n", MODBUS_BAUDRATE, MODBUS_RX_PIN, MODBUS_TX_PIN, MODBUS_DE_PIN); } // --- Modbus RTU CRC-16 계산 함수 --- uint16_t calculateCRC(uint8_t* buffer, int length) { uint16_t crc = 0xFFFF; for (int pos = 0; pos < length; pos++) { crc ^= (uint16_t)buffer[pos]; for (int i = 8; i != 0; i--) { if ((crc & 0x0001) != 0) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; } // --- RS-485 물리 프레임 전송 함수 (DE 핀 전환 제어 및 TX 프레임 로그 출력) --- void send_modbus_frame(uint8_t* frame, int length) { // TX 프레임 Hex 디버그 로그 출력 Serial.print("[MODBUS TX] "); for (int i = 0; i < length; i++) { Serial.printf("%02X ", frame[i]); } Serial.println(); digitalWrite(MODBUS_DE_PIN, HIGH); // 송신 모드로 전환 delayMicroseconds(100); // 안정화를 위한 미세 지연 Serial2.write(frame, length); Serial2.flush(); // 데이터가 시리얼 버스를 통해 나갈 때까지 대기 delayMicroseconds(100); digitalWrite(MODBUS_DE_PIN, LOW); // 다시 수신 모드로 전환 } // --- [G3] Modbus Holding Register 읽기 공통 함수 --- bool modbus_read_registers(uint16_t start_addr, uint16_t quantity, uint16_t* dest_buffer) { uint8_t frame[8]; frame[0] = PLC_SLAVE_ID; frame[1] = 0x03; // Read Holding Registers frame[2] = (start_addr >> 8) & 0xFF; frame[3] = start_addr & 0xFF; frame[4] = (quantity >> 8) & 0xFF; frame[5] = quantity & 0xFF; uint16_t crc = calculateCRC(frame, 6); frame[6] = crc & 0xFF; frame[7] = (crc >> 8) & 0xFF; while (Serial2.available()) Serial2.read(); send_modbus_frame(frame, 8); unsigned long start = millis(); int expected_bytes = 5 + (quantity * 2); uint8_t response[60]; int bytes_read = 0; while (millis() - start < 200) { if (Serial2.available()) { response[bytes_read++] = Serial2.read(); if (bytes_read >= expected_bytes || bytes_read >= sizeof(response)) { break; } } delay(1); } // RX 프레임 Hex 디버그 로그 출력 if (bytes_read > 0) { Serial.print("[MODBUS RX FC03] "); for (int i = 0; i < bytes_read; i++) { Serial.printf("%02X ", response[i]); } Serial.println(); } else { Serial.println("[MODBUS RX FC03 ERROR] No response (Timeout)"); } if (bytes_read < expected_bytes) { Serial.printf("[MODBUS RX FC03 ERROR] Bytes read mismatch (expected: %d, got: %d)\n", expected_bytes, bytes_read); return false; } uint16_t rx_crc = calculateCRC(response, bytes_read - 2); uint16_t pkt_crc = response[bytes_read - 2] | (response[bytes_read - 1] << 8); if (rx_crc != pkt_crc) { Serial.printf("[MODBUS RX FC03 ERROR] CRC mismatch (calc: 0x%04X, packet: 0x%04X)\n", rx_crc, pkt_crc); return false; } if (response[0] != PLC_SLAVE_ID || response[1] != 0x03) { Serial.printf("[MODBUS RX FC03 ERROR] Invalid Slave ID or Function Code (slave: %d, FC: 0x%02X)\n", response[0], response[1]); return false; } for (int i = 0; i < quantity; i++) { dest_buffer[i] = (response[3 + i * 2] << 8) | response[4 + i * 2]; } return true; } // --- 임시 비트(Coil) 읽기 함수 (FC 01) --- bool modbus_read_coils(uint16_t start_addr, uint16_t coil_qty, uint16_t* dest_word_buffer) { uint8_t frame[8]; frame[0] = PLC_SLAVE_ID; frame[1] = 0x01; // Read Coils frame[2] = (start_addr >> 8) & 0xFF; frame[3] = start_addr & 0xFF; frame[4] = (coil_qty >> 8) & 0xFF; frame[5] = coil_qty & 0xFF; uint16_t crc = calculateCRC(frame, 6); frame[6] = crc & 0xFF; frame[7] = (crc >> 8) & 0xFF; while (Serial2.available()) Serial2.read(); send_modbus_frame(frame, 8); unsigned long start = millis(); int byte_count = (coil_qty + 7) / 8; int expected_bytes = 5 + byte_count; uint8_t response[60]; int bytes_read = 0; while (millis() - start < 200) { if (Serial2.available()) { response[bytes_read++] = Serial2.read(); if (bytes_read >= expected_bytes || bytes_read >= sizeof(response)) { break; } } delay(1); } // RX 프레임 Hex 디버그 로그 출력 if (bytes_read > 0) { Serial.print("[MODBUS RX FC01] "); for (int i = 0; i < bytes_read; i++) { Serial.printf("%02X ", response[i]); } Serial.println(); } else { Serial.println("[MODBUS RX FC01 ERROR] No response (Timeout)"); } if (bytes_read < expected_bytes) { Serial.printf("[MODBUS RX FC01 ERROR] Bytes read mismatch (expected: %d, got: %d)\n", expected_bytes, bytes_read); return false; } uint16_t rx_crc = calculateCRC(response, bytes_read - 2); uint16_t pkt_crc = response[bytes_read - 2] | (response[bytes_read - 1] << 8); if (rx_crc != pkt_crc) { Serial.printf("[MODBUS RX FC01 ERROR] CRC mismatch (calc: 0x%04X, packet: 0x%04X)\n", rx_crc, pkt_crc); return false; } if (response[0] != PLC_SLAVE_ID || response[1] != 0x01) { Serial.printf("[MODBUS RX FC01 ERROR] Invalid Slave ID or Function Code (slave: %d, FC: 0x%02X)\n", response[0], response[1]); return false; } int word_qty = (coil_qty + 15) / 16; for (int i = 0; i < word_qty; i++) { // PLC Modbus RTU는 빅엔디안으로 한 워드의 상위 바이트를 먼저 수신함 uint8_t first_byte = response[3 + i * 2]; uint8_t second_byte = (i * 2 + 1 < byte_count) ? response[3 + i * 2 + 1] : 0x00; // 바이트 순서 교정: 첫 번째 수신 바이트(낮은 코일 주소)가 하위 8비트, 두 번째 바이트가 상위 8비트가 되도록 조립 dest_word_buffer[i] = (second_byte << 8) | first_byte; } return true; } // --- 임시 비트(Coil) 다중 쓰기 함수 (FC 0F) --- bool modbus_write_coils(uint16_t start_addr, uint16_t coil_qty, uint16_t* src_word_buffer) { int byte_count = (coil_qty + 7) / 8; int payload_len = 7 + byte_count; uint8_t frame[40]; frame[0] = PLC_SLAVE_ID; frame[1] = 0x0F; // Write Multiple Coils frame[2] = (start_addr >> 8) & 0xFF; frame[3] = start_addr & 0xFF; frame[4] = (coil_qty >> 8) & 0xFF; frame[5] = coil_qty & 0xFF; frame[6] = byte_count; int word_qty = (coil_qty + 15) / 16; for (int i = 0; i < word_qty; i++) { // PLC Cnet 모듈은 전송받은 바이트 순서 그대로 코일 주소에 대입하므로 스왑 없이 하위 바이트를 먼저 보냄 frame[7 + i * 2] = src_word_buffer[i] & 0xFF; if (i * 2 + 1 < byte_count) { frame[7 + i * 2 + 1] = (src_word_buffer[i] >> 8) & 0xFF; } } uint16_t crc = calculateCRC(frame, payload_len); frame[payload_len] = crc & 0xFF; frame[payload_len + 1] = (crc >> 8) & 0xFF; while (Serial2.available()) Serial2.read(); send_modbus_frame(frame, payload_len + 2); unsigned long start = millis(); int expected_bytes = 8; uint8_t response[10]; int bytes_read = 0; while (millis() - start < 200) { if (Serial2.available()) { response[bytes_read++] = Serial2.read(); if (bytes_read >= expected_bytes) { break; } } delay(1); } // RX 프레임 Hex 디버그 로그 출력 if (bytes_read > 0) { Serial.print("[MODBUS RX FC0F] "); for (int i = 0; i < bytes_read; i++) { Serial.printf("%02X ", response[i]); } Serial.println(); } else { Serial.println("[MODBUS RX FC0F ERROR] No response (Timeout)"); } if (bytes_read < expected_bytes) { Serial.printf("[MODBUS RX FC0F ERROR] Bytes read mismatch (expected: %d, got: %d)\n", expected_bytes, bytes_read); return false; } uint16_t rx_crc = calculateCRC(response, expected_bytes - 2); uint16_t pkt_crc = response[expected_bytes - 2] | (response[expected_bytes - 1] << 8); if (rx_crc != pkt_crc) { Serial.printf("[MODBUS RX FC0F ERROR] CRC mismatch (calc: 0x%04X, packet: 0x%04X)\n", rx_crc, pkt_crc); return false; } if (response[0] != PLC_SLAVE_ID || response[1] != 0x0F) { Serial.printf("[MODBUS RX FC0F ERROR] Invalid Slave ID or Function Code (slave: %d, FC: 0x%02X)\n", response[0], response[1]); return false; } return true; } // --- [G4] Modbus Holding Register 다중 쓰기 공통 함수 (FC 16) --- bool modbus_write_registers(uint16_t start_addr, uint16_t quantity, uint16_t* src_buffer) { int payload_len = 7 + (quantity * 2); uint8_t frame[35]; frame[0] = PLC_SLAVE_ID; frame[1] = 0x10; // Write Multiple Registers (FC16) frame[2] = (start_addr >> 8) & 0xFF; frame[3] = start_addr & 0xFF; frame[4] = (quantity >> 8) & 0xFF; frame[5] = quantity & 0xFF; frame[6] = quantity * 2; for (int i = 0; i < quantity; i++) { frame[7 + i * 2] = (src_buffer[i] >> 8) & 0xFF; frame[8 + i * 2] = src_buffer[i] & 0xFF; } uint16_t crc = calculateCRC(frame, payload_len); frame[payload_len] = crc & 0xFF; frame[payload_len + 1] = (crc >> 8) & 0xFF; while (Serial2.available()) Serial2.read(); send_modbus_frame(frame, payload_len + 2); unsigned long start = millis(); uint8_t response[10]; int bytes_read = 0; int expected_bytes = 8; while (millis() - start < 200) { if (Serial2.available()) { response[bytes_read++] = Serial2.read(); if (bytes_read == 2) { if (response[0] == PLC_SLAVE_ID && response[1] == 0x90) { expected_bytes = 5; } } if (bytes_read >= expected_bytes) { break; } } delay(1); } // RX 프레임 Hex 디버그 로그 출력 if (bytes_read > 0) { Serial.print("[MODBUS RX FC10] "); for (int i = 0; i < bytes_read; i++) { Serial.printf("%02X ", response[i]); } Serial.println(); } else { Serial.println("[MODBUS RX FC10 ERROR] No response (Timeout)"); } if (bytes_read < expected_bytes) { Serial.printf("[MODBUS RX FC10 ERROR] Bytes read mismatch (expected: %d, got: %d)\n", expected_bytes, bytes_read); return false; } uint16_t rx_crc = calculateCRC(response, expected_bytes - 2); uint16_t pkt_crc = response[expected_bytes - 2] | (response[expected_bytes - 1] << 8); if (rx_crc != pkt_crc) { Serial.printf("[MODBUS RX FC10 ERROR] CRC mismatch (calc: 0x%04X, packet: 0x%04X)\n", rx_crc, pkt_crc); return false; } if (response[1] == 0x90) { Serial.println("[MODBUS RX FC10 ERROR] Exception Response (0x90)"); return false; } if (response[0] != PLC_SLAVE_ID || response[1] != 0x10) { Serial.printf("[MODBUS RX FC10 ERROR] Invalid Slave ID or Function Code (slave: %d, FC: 0x%02X)\n", response[0], response[1]); return false; } return true; } // --- [G3] PLC 읽기 슬롯 처리 함수 --- void run_plc_read() { bool success_M = modbus_read_coils(M_IN_START_ADDR, M_IN_COILS, M_In_New); bool success_D = modbus_read_registers(D_IN_START_ADDR, D_IN_WORDS, D_In_New); if (success_M && success_D) { if (modbusErrorCount > 0) { Serial.println("[MODBUS] 통신 복구 완료! 에러 카운트를 리셋합니다."); } modbusErrorCount = 0; if (!plcOnline) { plcOnline = true; trigger_plc_recovery(); } } else { if (modbusErrorCount < MODBUS_MAX_RETRY) { modbusErrorCount++; Serial.printf("[ERROR] Modbus Read 실패! (M:%d, D:%d) (연속 %d회 실패 / 최대 10회)\n", success_M, success_D, modbusErrorCount); } if (modbusErrorCount >= MODBUS_MAX_RETRY && plcOnline) { plcOnline = false; Serial.printf("[ERROR] Modbus Read 장기 실패! (연속 %d회 실패 / 최대 10회)\n", modbusErrorCount); trigger_plc_reboot_event(); } } } // --- [G4] PLC 쓰기 슬롯 처리 함수 --- void run_plc_write() { if (!plcOnline) return; // 1. M 영역 (Coils) 쓰기 판단 및 실행 bool need_write_M = force_write_M_once || (pending_command_active && pending_command_type_is_m) || (memcmp(M_Out_New, M_Out_Old, sizeof(M_Out_Old)) != 0); if (!need_write_M) { for (int i = 0; i < M_OUT_WORDS; i++) { if (M_Out_Pending_Write[i] != 0 || M_Out_Pending_Clear[i] != 0) { need_write_M = true; break; } } } if (need_write_M) { bool is_command_apply = pending_command_active && pending_command_type_is_m; bool is_auto_off = !is_command_apply && m_pulse_release_pending; const char* phase = force_write_M_once ? "INIT_FORCE" : (is_command_apply ? "COMMAND_ON" : (is_auto_off ? "AUTO_OFF" : "INTERNAL_RETRY")); char command_id_for_trace[40] = ""; if (is_command_apply) { strncpy(command_id_for_trace, pending_command_id, sizeof(command_id_for_trace) - 1); command_id_for_trace[sizeof(command_id_for_trace) - 1] = '\0'; } else if (m_pulse_command_id[0] != '\0') { strncpy(command_id_for_trace, m_pulse_command_id, sizeof(command_id_for_trace) - 1); command_id_for_trace[sizeof(command_id_for_trace) - 1] = '\0'; } else { strncpy(command_id_for_trace, "none", sizeof(command_id_for_trace) - 1); } Serial.printf("[M PULSE TRACE] phase=%s command_id=%s started_ms=%lu\n", phase, command_id_for_trace, millis()); if (is_command_apply && m_pulse_release_pending) { Serial.printf("[M PULSE WARN] command_id=%s arrived before previous pulse AUTO_OFF completed; repeated HIGH may not create a new PLC rising edge.\n", command_id_for_trace); } for (int i = 0; i < M_OUT_WORDS; i++) { if (M_Out_New[i] != M_Out_Old[i] || M_Out_Pending_Write[i] != 0) { Serial.printf("[M PULSE TRACE] word=%d plc=M%04d0~M%04dF old=0x%04X outgoing=0x%04X pending=0x%04X\n", i, 200 + i, 200 + i, M_Out_Old[i], M_Out_New[i], M_Out_Pending_Write[i]); } } bool success_M = modbus_write_coils(M_OUT_START_ADDR, M_OUT_COILS, M_Out_New); if (!success_M) { Serial.printf("[ERROR] M_Out Coil Write 실패! (addr=%d, qty=%d, FC0F)\n", M_OUT_START_ADDR, M_OUT_COILS); if (pending_command_active && pending_command_type_is_m) { rollback_pending_command_buffers(); publish_ack(pending_command_id, "FAILED"); pending_command_active = false; pending_command_id[0] = '\0'; pending_command_time = 0; } else { // 모멘터리 OFF 등 내부 쓰기는 목표 버퍼를 유지해 다음 WRITE 슬롯에서 재시도한다. Serial.println("[MODBUS WRITE M] Internal write will be retried without rollback."); } } else { Serial.println("[MODBUS WRITE M] M_Out write sequence completed successfully."); if (is_auto_off) { Serial.printf("[M PULSE TRACE] phase=AUTO_OFF_COMPLETE command_id=%s pulse_width_ms=%lu\n", command_id_for_trace, millis() - m_pulse_started_at); m_pulse_release_pending = false; m_pulse_command_id[0] = '\0'; // D Apply 펄스의 OFF가 방금 완료되었는지 확인. // d_apply_pending이 true인 D 명령에 한해 이 시점에 PLC_APPLIED를 발행한다(상태 경합 방지). if (d_apply_pending && pending_command_active && !pending_command_type_is_m) { Serial.printf("[D APPLY] command_id=%s Apply OFF 완료 -> PLC_APPLIED 발행\n", pending_command_id); publish_ack(pending_command_id, "PLC_APPLIED"); pending_command_active = false; pending_command_id[0] = '\0'; pending_command_time = 0; d_apply_pending = false; d_apply_word_mask = 0; d_apply_word_idx = -1; } } if (pending_command_active && pending_command_type_is_m) { publish_ack(pending_command_id, "PLC_APPLIED"); pending_command_active = false; pending_command_id[0] = '\0'; pending_command_time = 0; } // 쓰기 성공 시 Old 버퍼 동기화 및 플래그 해제 memcpy(M_Out_Old, M_Out_New, sizeof(M_Out_Old)); bool scheduled_auto_off = false; for (int i = 0; i < M_OUT_WORDS; i++) { if (M_Out_Pending_Write[i] != 0) { for (int j = 0; j < 16; j++) { if ((M_Out_Pending_Write[i] >> j) & 1) { scheduled_auto_off = true; M_Out_Pending_Write[i] &= ~(1 << j); M_Out_New[i] &= ~(1 << j); Serial.printf("[MODBUS WRITE RESET] Automatically cleared absolute bit %d to 0 for momentary emulation\n", i * 16 + j); Serial.printf("[M PULSE TRACE] command_id=%s plc_target=M%04d%X state=HIGH_APPLIED auto_off=SCHEDULED\n", command_id_for_trace, 200 + i, j); } } } M_Out_Pending_Clear[i] = 0; } if (scheduled_auto_off) { m_pulse_release_pending = true; m_pulse_started_at = millis(); strncpy(m_pulse_command_id, command_id_for_trace, sizeof(m_pulse_command_id) - 1); m_pulse_command_id[sizeof(m_pulse_command_id) - 1] = '\0'; Serial.printf("[M PULSE TRACE] phase=ON_COMPLETE command_id=%s auto_off_pending=1\n", command_id_for_trace); } force_write_M_once = false; // M 영역 최초 쓰기 완료 } } // 2. D 영역 (Registers) 쓰기 판단 및 실행 bool need_write_D = D_Out_Pending_Groups != 0; if (need_write_D) { uint8_t groups_to_write = D_Out_Pending_Groups; bool success_D = true; for (int group_id = 0; group_id < 4; group_id++) { if ((groups_to_write & (1U << group_id)) == 0) continue; if (!modbus_write_registers(D_OUT_START_ADDR + group_id * 2, 2, &D_Out_New[group_id * 2])) { success_D = false; break; } } if (!success_D) { Serial.printf("[ERROR] D_Out Write 실패! (addr=%d, qty=%d, FC16)\n", D_OUT_START_ADDR, D_OUT_WORDS); if (pending_command_active && !pending_command_type_is_m) { rollback_pending_command_buffers(); publish_ack(pending_command_id, "FAILED"); pending_command_active = false; pending_command_id[0] = '\0'; pending_command_time = 0; } else { Serial.println("[MODBUS WRITE D] Internal write will be retried without rollback."); } } else { Serial.println("[MODBUS WRITE D] Pending REAL groups written successfully."); // 쓰기 성공 시 Old 버퍼 동기화 memcpy(D_Out_Old, D_Out_New, sizeof(D_Out_Old)); D_Out_Pending_Groups &= ~groups_to_write; force_write_D_once = false; // D 영역 최초 쓰기 완료 // D203x 쓰기가 끝났으므로, 이번에 쓴 그룹들의 Apply 비트(M211C~F)를 기존 M 모멘터리 엔진에 예약한다. // PLC는 이 A접이 ON인 동안 DMOV로 실제 설정값을 복사한다. ACK(PLC_APPLIED)는 Apply OFF 완료까지 지연한다. uint16_t apply_mask = 0; for (int group_id = 0; group_id < 4; group_id++) { if (groups_to_write & (1U << group_id)) { int apply_bit = D_APPLY_BIT_BASE + group_id; // 188~191 = M211C~F int w = apply_bit / 16; // 워드 인덱스 (모두 11) int b = apply_bit % 16; // 워드 내 비트 위치 M_Out_New[w] |= (1 << b); M_Out_Pending_Write[w] |= (1 << b); // 다음 WRITE 슬롯에서 FC0F ON → 이후 자동 OFF M_Out_Pending_Clear[w] &= ~(1 << b); apply_mask |= (1 << b); d_apply_word_idx = w; // M211C~F: word=11, bit=12~15 → PLC 표기 M2%02d%X Serial.printf("[D APPLY] Group %d -> Apply bit M2%02d%X (offset %d) ON 예약\n", group_id, 200 + w, b, apply_bit); } } if (apply_mask != 0 && pending_command_active && !pending_command_type_is_m) { // Apply 펄스가 예약된 D 명령: OFF 완료 시점에 PLC_APPLIED를 발행하도록 대기 상태 진입. d_apply_pending = true; d_apply_word_mask = apply_mask; Serial.printf("[D APPLY] command_id=%s Apply 펄스 예약 완료, PLC_APPLIED는 Apply OFF 후 발행\n", pending_command_id); } else if (pending_command_active && !pending_command_type_is_m) { // 예약된 Apply 비트가 없는 예외 상황: 기존 동작대로 즉시 완료 처리. publish_ack(pending_command_id, "PLC_APPLIED"); pending_command_active = false; pending_command_id[0] = '\0'; pending_command_time = 0; } } } }