초기화
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user