초기화

This commit is contained in:
2026-07-15 18:37:19 +09:00
commit 94abc5461d
1268 changed files with 380198 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
Binary file not shown.
+118
View File
@@ -0,0 +1,118 @@
#include "config.h"
// --- Wi-Fi 및 MQTT 설정 값 정의 ---
const char* ssid = "KT_GiGA_B517"; //KT_GiGA_B517 | esd
const char* password = "axf29xc666"; //axf29xc666 | usdd6595
const char* mqtt_server = "218.155.105.34";
const char* mqtt_user = "ctnt_root";
const char* mqtt_password = "Umsang6595!!";
const char* clientName = "ESP32_PLC_Gateway";
// --- 통신 버퍼 정의 ---
uint16_t M_In_New[M_IN_WORDS];
uint16_t M_In_Old[M_IN_WORDS];
uint16_t M_Out_New[M_OUT_WORDS];
uint16_t M_Out_Old[M_OUT_WORDS];
uint16_t M_Out_Pending_Write[M_OUT_WORDS];
uint16_t M_Out_Pending_Clear[M_OUT_WORDS];
uint16_t D_In_New[D_IN_WORDS];
uint16_t D_In_Old[D_IN_WORDS];
uint16_t D_Out_New[D_OUT_WORDS];
uint16_t D_Out_Old[D_OUT_WORDS];
uint8_t D_Out_Pending_Groups = 0;
// 제어 명령 적용 직전 상태. 실패/타임아웃 시 Old가 아닌 이 스냅샷으로 복원한다.
static uint16_t M_Out_Command_Backup[M_OUT_WORDS];
static uint16_t M_Out_Pending_Write_Backup[M_OUT_WORDS];
static uint16_t M_Out_Pending_Clear_Backup[M_OUT_WORDS];
static uint16_t D_Out_Command_Backup[D_OUT_WORDS];
static uint8_t D_Out_Pending_Groups_Backup = 0;
// --- 시스템 상태 변수 정의 ---
SystemState currentState = STATE_INIT;
unsigned long lastStateChange = 0;
const unsigned long SLOT_DURATION = 200; // 0.2초 (200ms)
const unsigned long INIT_DURATION = 5000; // 5초 (5000ms)
bool readExecuted = false;
bool writeExecuted = false;
unsigned long lastInitReadTime = 0;
int modbusErrorCount = 0;
const int MODBUS_MAX_RETRY = 10; // PLC 재부팅 감지 임계값 (10회)
bool plcOnline = true;
unsigned long lastMqttRetry = 0;
bool initSequenceStarted = false;
bool force_write_M_once = false;
bool force_write_D_once = false;
bool full_sync_pending = false;
char pending_command_id[40] = "";
bool pending_command_active = false;
bool pending_command_type_is_m = false;
unsigned long pending_command_time = 0;
// --- D Apply 펄스 상태 정의 ---
bool d_apply_pending = false;
uint16_t d_apply_word_mask = 0;
int d_apply_word_idx = -1;
// --- [G1] 버퍼 초기화 ---
void setup_buffers() {
memset(M_In_New, 0, sizeof(M_In_New));
memset(M_In_Old, 0, sizeof(M_In_Old));
memset(M_Out_New, 0, sizeof(M_Out_New));
memset(M_Out_Old, 0, sizeof(M_Out_Old));
memset(M_Out_Pending_Write, 0, sizeof(M_Out_Pending_Write));
memset(M_Out_Pending_Clear, 0, sizeof(M_Out_Pending_Clear));
memset(D_In_New, 0, sizeof(D_In_New));
memset(D_In_Old, 0, sizeof(D_In_Old));
memset(D_Out_New, 0, sizeof(D_Out_New));
memset(D_Out_Old, 0, sizeof(D_Out_Old));
D_Out_Pending_Groups = 0;
memset(M_Out_Command_Backup, 0, sizeof(M_Out_Command_Backup));
memset(M_Out_Pending_Write_Backup, 0, sizeof(M_Out_Pending_Write_Backup));
memset(M_Out_Pending_Clear_Backup, 0, sizeof(M_Out_Pending_Clear_Backup));
memset(D_Out_Command_Backup, 0, sizeof(D_Out_Command_Backup));
force_write_M_once = false;
force_write_D_once = false;
full_sync_pending = true; // Full-Sync 개시 플래그 활성화
// 대기 중인 명령 상태 명시적 초기화
pending_command_active = false;
pending_command_id[0] = '\0';
pending_command_time = 0;
// D Apply 펄스 상태 초기화
d_apply_pending = false;
d_apply_word_mask = 0;
d_apply_word_idx = -1;
Serial.println("[INIT] 버퍼 전체 0 초기화 및 강제 쓰기 플래그, Full-Sync 대기 플래그 활성화 완료.");
}
void snapshot_pending_command_buffers(bool is_m_command) {
if (is_m_command) {
memcpy(M_Out_Command_Backup, M_Out_New, sizeof(M_Out_New));
memcpy(M_Out_Pending_Write_Backup, M_Out_Pending_Write, sizeof(M_Out_Pending_Write));
memcpy(M_Out_Pending_Clear_Backup, M_Out_Pending_Clear, sizeof(M_Out_Pending_Clear));
} else {
memcpy(D_Out_Command_Backup, D_Out_New, sizeof(D_Out_New));
D_Out_Pending_Groups_Backup = D_Out_Pending_Groups;
}
}
void rollback_pending_command_buffers() {
if (pending_command_type_is_m) {
memcpy(M_Out_New, M_Out_Command_Backup, sizeof(M_Out_New));
memcpy(M_Out_Pending_Write, M_Out_Pending_Write_Backup, sizeof(M_Out_Pending_Write));
memcpy(M_Out_Pending_Clear, M_Out_Pending_Clear_Backup, sizeof(M_Out_Pending_Clear));
} else {
memcpy(D_Out_New, D_Out_Command_Backup, sizeof(D_Out_New));
D_Out_Pending_Groups = D_Out_Pending_Groups_Backup;
}
}
+97
View File
@@ -0,0 +1,97 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
// --- 펌웨어 버전 식별 ---
#define FIRMWARE_VERSION "d-apply-v2"
// --- Wi-Fi 및 MQTT 설정 ---
extern const char* ssid;
extern const char* password;
extern const char* mqtt_server;
extern const char* mqtt_user;
extern const char* mqtt_password;
extern const char* clientName;
// --- PLC Modbus 주소 및 크기 설정 ---
#define PLC_SLAVE_ID 1 // PLC 국번 (Modbus ID)
// 임시 비트(Coil) 방식 상수
#define M_IN_START_ADDR 1600 // M01000 시작 비트 주소 (100 * 16)
#define M_IN_COILS 208 // M01000 ~ M0112F (208개 비트)
#define M_IN_WORDS 13 // 내부 버퍼 호환용 워드 크기 유지
#define M_OUT_START_ADDR 3200 // M02000 시작 비트 주소 (200 * 16)
#define M_OUT_COILS 192 // M02000 ~ M0211F (192개 비트, D Apply M211C~F 포함하도록 확장)
#define M_OUT_WORDS 12 // 내부 버퍼 호환용 워드 크기 유지
// --- D 워드 원격 제어 Apply 비트 (M211C~M211F, 그룹 0~3 대응) ---
// PLC 래더는 이 A접이 ON인 동안에만 D203x→실제 설정값 DMOV를 수행한다.
// 모멘터리(P접)는 PLC에서 어려워 게이트웨이가 기존 M 모멘터리 엔진으로 ON→OFF 펄스를 생성한다.
#define D_APPLY_BIT_BASE 188 // M211C = M2000 기준 offset 188 (그룹 0), 189/190/191 = 그룹 1/2/3
#define D_IN_START_ADDR 2000 // D2000 시작 주소
#define D_IN_WORDS 20 // D2000 ~ D2018 (20 워드, 10 그룹)
#define D_OUT_START_ADDR 2030 // D2030 시작 주소 (PLC 메모리 맵 매핑 수정)
#define D_OUT_WORDS 8 // D2030 ~ D2036 (8 워드, 4 그룹)
// --- 통신 버퍼 (Old/New Double Buffering) ---
extern uint16_t M_In_New[M_IN_WORDS];
extern uint16_t M_In_Old[M_IN_WORDS];
extern uint16_t M_Out_New[M_OUT_WORDS];
extern uint16_t M_Out_Old[M_OUT_WORDS];
extern uint16_t M_Out_Pending_Write[M_OUT_WORDS];
extern uint16_t M_Out_Pending_Clear[M_OUT_WORDS];
extern uint16_t D_In_New[D_IN_WORDS];
extern uint16_t D_In_Old[D_IN_WORDS];
extern uint16_t D_Out_New[D_OUT_WORDS];
extern uint16_t D_Out_Old[D_OUT_WORDS];
extern uint8_t D_Out_Pending_Groups;
// --- 시스템 상태 및 오류 통계 제어 변수 ---
enum SystemState {
STATE_INIT,
STATE_READ,
STATE_WRITE
};
extern SystemState currentState;
extern unsigned long lastStateChange;
extern const unsigned long SLOT_DURATION; // 0.5초 (500ms)
extern const unsigned long INIT_DURATION; // 5초 (5000ms)
extern bool readExecuted;
extern bool writeExecuted;
extern unsigned long lastInitReadTime;
extern int modbusErrorCount;
extern const int MODBUS_MAX_RETRY; // PLC 재부팅 감지 임계값 (10회)
extern bool plcOnline;
extern unsigned long lastMqttRetry;
extern bool initSequenceStarted;
extern bool force_write_M_once;
extern bool force_write_D_once;
extern bool full_sync_pending;
extern char pending_command_id[40];
extern bool pending_command_active;
extern bool pending_command_type_is_m;
extern unsigned long pending_command_time; // 명령 수신 시점 저장용 타임스탬프
// --- D Apply 펄스 상태 (D 쓰기 성공 후 M211C~F ON→OFF 완료 시점에 PLC_APPLIED 발행) ---
// 기존 M 모멘터리 엔진을 재사용하되, D 명령의 ACK를 Apply OFF 완료까지 지연시키기 위한 최소 상태.
extern bool d_apply_pending; // D 쓰기는 끝났고 Apply 펄스 완료를 기다리는 중
extern uint16_t d_apply_word_mask; // Apply 대상 M211C~F 비트가 속한 워드의 비트 마스크
extern int d_apply_word_idx; // Apply 비트가 속한 워드 인덱스 (M211C~F는 모두 word 11)
// 함수 선언
void setup_buffers();
void snapshot_pending_command_buffers(bool is_m_command);
void rollback_pending_command_buffers();
#endif // CONFIG_H
+168
View File
@@ -0,0 +1,168 @@
#include <Arduino.h>
#include <esp_wifi.h>
#include "config.h"
#include "modbus.h"
#include "mqtt_handler.h"
// --- [G1] Wi-Fi 연결 설정 ---
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("[WIFI] Connecting to ");
Serial.println(ssid);
WiFi.disconnect(true, true);
delay(500);
WiFi.mode(WIFI_STA);
delay(500);
esp_wifi_set_ps(WIFI_PS_NONE);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
attempts++;
if (attempts >= 20) {
Serial.println("\n[WIFI] 연결 지연 발생. 무선 재시작 중...");
WiFi.disconnect();
delay(500);
WiFi.begin(ssid, password);
attempts = 0;
}
}
Serial.println("");
Serial.println("[WIFI] connected");
Serial.print("[WIFI] IP address: ");
Serial.println(WiFi.localIP());
}
// --- [G7] 부팅 초기화 루틴 제어 함수 ---
void handle_init_sequence() {
if (!initSequenceStarted) {
initSequenceStarted = true;
setup_buffers();
Serial.println("[EVENT] id=999 Full-Sync 시작");
if (client.connected()) {
client.publish("/line1/event", "{\"id\":999,\"val\":1}");
}
}
}
// --- [G8] PLC 재부팅 감지 이벤트 처리 함수 ---
void trigger_plc_reboot_event() {
Serial.println("[EVENT] id=998 PLC_REBOOT 감지");
if (client.connected()) {
client.publish("/line1/event", "{\"id\":998,\"val\":1}");
}
currentState = STATE_INIT;
lastStateChange = millis();
initSequenceStarted = false;
}
// --- [G8] PLC 통신 복구 감지 처리 함수 ---
void trigger_plc_recovery() {
Serial.println("[EVENT] PLC 통신 복구 감지 -> Full-Sync 재수행 준비");
currentState = STATE_INIT;
lastStateChange = millis();
initSequenceStarted = false;
}
void setup() {
Serial.begin(115200);
Serial.printf("[SYS] Firmware version: %s\n", FIRMWARE_VERSION);
setup_buffers();
setup_wifi();
setup_mqtt();
setup_modbus();
lastStateChange = millis();
Serial.println("[SYS] Setup 완료.");
}
// --- [G2] 타이밍 스케줄러 메인 루프 ---
void loop() {
maintain_mqtt();
unsigned long now = millis();
switch (currentState) {
case STATE_INIT:
// 초기화 시퀀스 핸들링 (버퍼 클리어 및 999 이벤트 송신)
handle_init_sequence();
// 정상 주기적 PLC 읽기를 진행하지 않고 대기 (정상 로직 일시 중단)
if (now - lastStateChange >= INIT_DURATION) {
Serial.println("[STATE] INIT 완료 -> PLC 상태 조회 및 Full-Sync 데이터 전송 개시");
// 1회 PLC 데이터를 가져와 plcOnline 여부 및 버퍼 업데이트
run_plc_read();
if (plcOnline) {
// full_sync_pending 플래그가 설정되어 있으므로 전체 스냅샷이 송신됨
if (process_read_changes()) {
bool pub_ok = false;
Serial.println("[EVENT] id=997 Full-Sync 완료 이벤트 발행 시도...");
if (client.connected()) {
pub_ok = client.publish("/line1/event", "{\"id\":997,\"val\":1}");
}
if (pub_ok) {
full_sync_pending = false; // Full-Sync 완료 이벤트 발행 성공 시에만 일반 운전 상태로 전환
currentState = STATE_READ;
lastStateChange = now;
readExecuted = false;
Serial.println("[EVENT] id=997 Full-Sync 완료 이벤트 발행 성공, 정상 루틴 진입");
} else {
Serial.println("[WARN] Full-Sync 완료 이벤트(997) 발행 실패. 다음 주기에서 재발행을 재시도합니다.");
// 상태 전환을 하지 않고 STATE_INIT 대기 유지하여 다음 주기에서 재시도
}
} else {
Serial.println("[WARN] Full-Sync 전송 실패. 다음 주기에서 재동기화 재시도합니다.");
// 상태 전환을 하지 않고 STATE_INIT 대기 유지하여 다음 주기에서 재시도
}
} else {
Serial.println("[WARN] PLC 오프라인 상태 - Full-Sync 보류, 정상 루틴 진입");
full_sync_pending = false; // 오프라인이어도 플래그는 일단 정리하여 일반 모드 진입 준비
currentState = STATE_READ;
lastStateChange = now;
readExecuted = false;
}
}
break;
case STATE_READ:
if (now - lastStateChange >= SLOT_DURATION) {
currentState = STATE_WRITE;
lastStateChange = now;
writeExecuted = false;
} else {
if (!readExecuted) {
run_plc_read();
process_read_changes();
readExecuted = true;
}
}
break;
case STATE_WRITE:
if (now - lastStateChange >= SLOT_DURATION) {
currentState = STATE_READ;
lastStateChange = now;
readExecuted = false;
} else {
if (!writeExecuted) {
run_plc_write();
writeExecuted = true;
}
}
break;
}
}
+582
View File
@@ -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;
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef MODBUS_H
#define MODBUS_H
#include <Arduino.h>
void setup_modbus();
uint16_t calculateCRC(uint8_t* buffer, int length);
bool modbus_read_registers(uint16_t start_addr, uint16_t quantity, uint16_t* dest_buffer);
bool modbus_read_coils(uint16_t start_addr, uint16_t coil_qty, uint16_t* dest_word_buffer);
bool modbus_write_coils(uint16_t start_addr, uint16_t coil_qty, uint16_t* src_word_buffer);
bool modbus_write_registers(uint16_t start_addr, uint16_t quantity, uint16_t* src_buffer);
void run_plc_read();
void run_plc_write();
#endif // MODBUS_H
+22
View File
@@ -0,0 +1,22 @@
{
"m_read": {
"start_addr": 1600,
"coil_count": 208,
"word_count": 13
},
"m_write": {
"start_addr": 3200,
"coil_count": 188,
"word_count": 12
},
"d_read": {
"start_addr": 2000,
"word_count": 20,
"groups": 10
},
"d_write": {
"start_addr": 2030,
"word_count": 8,
"groups": 4
}
}
+469
View File
@@ -0,0 +1,469 @@
#include "mqtt_handler.h"
#include "config.h"
#include "modbus.h"
#include <ArduinoJson.h>
#include <esp_wifi.h>
WiFiClient espClient;
PubSubClient client(espClient);
// --- MQTT ACK 전송 함수 ---
void publish_ack(const char* command_id, const char* status) {
if (!client.connected()) return;
JsonDocument doc;
doc["command_id"] = command_id;
doc["status"] = status;
doc["timestamp"] = millis();
char buffer[128];
serializeJson(doc, buffer, sizeof(buffer));
client.publish("/line1/ack", buffer);
Serial.printf("[MQTT ACK PUBLISH] Published status %s for command %s to /line1/ack\n", status, command_id);
}
// --- MQTT 수신 콜백: [[id, val], ...] 배치 파싱 및 제어 적용 ---
void on_mqtt_message(char* topic, byte* payload, unsigned int length) {
// 수신된 페이로드 로그 출력
char log_buf[256];
unsigned int log_len = (length < 255) ? length : 255;
memcpy(log_buf, payload, log_len);
log_buf[log_len] = '\0';
Serial.printf("\n[MQTT RECEIVED] Topic: %s | Len: %d | Payload: %s\n", topic, length, log_buf);
// 명령(cmd) 토픽 처리
if (strcmp(topic, "/line1/cmd") == 0) {
char cmd_str[32];
unsigned int len = (length < 31) ? length : 31;
memcpy(cmd_str, payload, len);
cmd_str[len] = '\0';
char* clean_cmd = cmd_str;
if (clean_cmd[0] == '"') clean_cmd++;
int clean_len = strlen(clean_cmd);
if (clean_len > 0 && clean_cmd[clean_len - 1] == '"') {
clean_cmd[clean_len - 1] = '\0';
}
if (strcmp(clean_cmd, "sync") == 0) {
Serial.println("[CMD PROCESS] Manual Full-Sync Request Received!");
if (pending_command_active) {
publish_ack(pending_command_id, "FAILED");
}
setup_buffers();
currentState = STATE_INIT;
lastStateChange = millis();
initSequenceStarted = false;
if (client.connected()) {
client.publish("/line1/event", "{\"id\":999,\"val\":1}");
Serial.println("[CMD PROCESS] Published event 999 to broker.");
}
return;
}
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, payload, length);
if (error) {
Serial.printf("[MQTT ERROR] JSON Deserialization failed: %s\n", error.c_str());
return;
}
const char* cmd_id = nullptr;
JsonArray arr;
bool has_envelope = false;
// JSON 객체(새로운 Envelope 규격)와 JSON 배열(기존 규격) 둘 다 호환되도록 처리
if (doc.is<JsonObject>()) {
JsonObject obj = doc.as<JsonObject>();
if (obj["command_id"].is<const char*>() && obj["items"].is<JsonArray>()) {
cmd_id = obj["command_id"].as<const char*>();
arr = obj["items"].as<JsonArray>();
has_envelope = true;
Serial.printf("[MQTT ENVELOPE] command_id: %s\n", cmd_id);
} else {
Serial.println("[MQTT ERROR] JSON Object requires string command_id and array items");
return;
}
} else if (doc.is<JsonArray>()) {
arr = doc.as<JsonArray>();
} else {
Serial.println("[MQTT ERROR] Payload is neither JSON Array nor Object");
return;
}
const bool is_m_topic = strcmp(topic, "/line1/m/out") == 0;
const bool is_d_topic = strcmp(topic, "/line1/d/out") == 0;
bool valid_items = (is_m_topic || is_d_topic) && !arr.isNull() && arr.size() > 0;
// ArduinoJson 7에서 중첩 배열을 JsonVariant -> JsonArray로 명시 변환해 검증한다.
for (JsonVariant item_variant : arr) {
if (!item_variant.is<JsonArray>()) {
valid_items = false;
break;
}
JsonArray item = item_variant.as<JsonArray>();
if (item.size() < 2 || !item[0].is<int>()) {
valid_items = false;
break;
}
int id = item[0].as<int>();
if (is_m_topic) {
if (!item[1].is<int>()) {
valid_items = false;
break;
}
int val = item[1].as<int>();
if (id < 0 || id >= M_OUT_COILS || (val != 0 && val != 1)) {
valid_items = false;
break;
}
} else if (is_d_topic) {
if (id < 0 || id >= 4 || !item[1].is<float>()) {
valid_items = false;
break;
}
}
}
if (!valid_items) {
Serial.printf("[MQTT ERROR] Invalid or empty items for topic %s; command will not be written to PLC\n", topic);
if (has_envelope && cmd_id != nullptr) {
publish_ack(cmd_id, "FAILED");
}
return;
}
// Envelope 규격 수신 시 상태를 RECEIVED로 즉시 업데이트하고 전송 대기 정보 설정
if (has_envelope && cmd_id != nullptr) {
if (!plcOnline || currentState == STATE_INIT) {
Serial.printf("[MQTT REJECT] Command %s rejected because PLC is offline or in INIT state\n", cmd_id);
publish_ack(cmd_id, "FAILED");
return;
}
if (pending_command_active) {
Serial.printf("[MQTT WARN] Command %s rejected because command %s is still active (BUSY)\n", cmd_id, pending_command_id);
publish_ack(cmd_id, "BUSY");
return;
}
strncpy(pending_command_id, cmd_id, sizeof(pending_command_id) - 1);
pending_command_id[sizeof(pending_command_id) - 1] = '\0';
pending_command_active = true;
pending_command_time = millis();
pending_command_type_is_m = is_m_topic;
snapshot_pending_command_buffers(pending_command_type_is_m);
publish_ack(pending_command_id, "RECEIVED");
}
Serial.printf("[MQTT PROCESS] Parsed array size: %d\n", arr.size());
if (is_m_topic) {
for (JsonVariant item_variant : arr) {
JsonArray item = item_variant.as<JsonArray>();
if (item.size() >= 2) {
int id = item[0];
int val = item[1];
int word_idx = id / 16;
int bit_idx = id % 16;
if (word_idx >= 0 && word_idx < M_OUT_WORDS) {
uint16_t before_word = M_Out_New[word_idx];
Serial.printf("[M_OUT REG] Requested bit: %d | val: %d | Calculated word_idx: %d, bit_idx: %d\n", id, val, word_idx, bit_idx);
if (val) {
M_Out_New[word_idx] |= (1 << bit_idx);
M_Out_Pending_Write[word_idx] |= (1 << bit_idx);
M_Out_Pending_Clear[word_idx] &= ~(1 << bit_idx);
} else {
M_Out_New[word_idx] &= ~(1 << bit_idx);
M_Out_Pending_Write[word_idx] &= ~(1 << bit_idx);
M_Out_Pending_Clear[word_idx] &= ~(1 << bit_idx);
}
Serial.printf("[M_OUT PROCESS completed] Updated M_Out_New[%d] to 0x%04X (Pending Write: 0x%04X)\n", word_idx, M_Out_New[word_idx], M_Out_Pending_Write[word_idx]);
Serial.printf(
"[M CMD TRACE] command_id=%s ui_bit=M%d plc_target=M%04d%X coil_addr=%d requested=%d word_before=0x%04X word_after=0x%04X pending=0x%04X\n",
pending_command_active ? pending_command_id : "legacy",
id,
200 + word_idx,
bit_idx,
M_OUT_START_ADDR + id,
val,
before_word,
M_Out_New[word_idx],
M_Out_Pending_Write[word_idx]
);
} else {
Serial.printf("[M_OUT LIMIT ERROR] word_idx %d is out of bounds (max: %d)\n", word_idx, M_OUT_WORDS);
}
}
}
}
else if (is_d_topic) {
for (JsonVariant item_variant : arr) {
JsonArray item = item_variant.as<JsonArray>();
if (item.size() >= 2) {
int id = item[0];
float val = item[1].as<float>();
if (id >= 0 && id < 4) {
uint32_t raw;
memcpy(&raw, &val, sizeof(raw));
uint16_t w_low = raw & 0xFFFF;
uint16_t w_high = (raw >> 16) & 0xFFFF;
D_Out_New[id * 2] = w_low;
D_Out_New[id * 2 + 1] = w_high;
D_Out_Pending_Groups |= (1U << id);
Serial.printf("[D_OUT REG] Requested Group ID: %d | REAL: %.6g | Low Word: 0x%04X, High Word: 0x%04X\n", id, val, w_low, w_high);
} else {
Serial.printf("[D_OUT LIMIT ERROR] Group ID %d is out of bounds (max: 4)\n", id);
}
}
}
}
}
// --- MQTT 초기화 및 최초 구독 연결 설정 ---
void setup_mqtt() {
client.setServer(mqtt_server, 1883);
client.setCallback(on_mqtt_message);
client.setBufferSize(2048);
Serial.println("[MQTT SETUP] Server configured to 1883. Buffer size set to 2048.");
}
// --- MQTT 연결 유지 및 비차단 재연결 함수 ---
void maintain_mqtt() {
if (WiFi.status() != WL_CONNECTED) {
return;
}
if (!client.connected()) {
unsigned long now = millis();
if (now - lastMqttRetry >= 5000) {
lastMqttRetry = now;
Serial.print("[MQTT LOOP] Attempting connection...");
uint8_t mac[6];
WiFi.macAddress(mac);
char clientId[30];
snprintf(clientId, sizeof(clientId), "ESP32_Gateway_%02X%02X%02X", mac[3], mac[4], mac[5]);
// Last Will & Testament 설정 (어플리케이션 비정상 종료 시 브로커가 OFFLINE 메시지 자동 배포)
if (client.connect(clientId, mqtt_user, mqtt_password, "/line1/status", 1, true, "{\"gateway\": \"OFFLINE\"}")) {
Serial.println(" OK (connected)");
// 연결 성공 직후 retained SYNCING 상태 전송 (초기화 중이므로)
char status_msg[160];
snprintf(status_msg, sizeof(status_msg),
"{\"gateway\": \"SYNCING\", \"plc_state\": \"%s\", \"firmware\": \"%s\"}",
plcOnline ? "ONLINE" : "OFFLINE", FIRMWARE_VERSION);
client.publish("/line1/status", status_msg, true);
// MQTT 연결 복구 시 Full-Sync 강제 수행 상태로 전이 (동기 정합성 확보)
Serial.println("[MQTT LOOP] Reconnected successfully. Triggering Full-Sync...");
setup_buffers();
currentState = STATE_INIT;
lastStateChange = millis();
initSequenceStarted = false;
client.publish("/line1/event", "{\"id\":999,\"val\":1}");
client.subscribe("/line1/m/out");
client.subscribe("/line1/d/out");
client.subscribe("/line1/cmd");
Serial.println("[MQTT LOOP] Subscribed to /line1/m/out, /line1/d/out, /line1/cmd successfully.");
} else {
Serial.print(" FAILED, rc=");
Serial.println(client.state());
}
}
} else {
client.loop();
// 주기적인 (5초 간격) Retained Heartbeat 발행
static unsigned long lastHeartbeat = 0;
if (millis() - lastHeartbeat >= 5000) {
char status_msg[160];
const char* gw_state = (currentState == STATE_INIT) ? "SYNCING" : "ONLINE";
snprintf(status_msg, sizeof(status_msg),
"{\"gateway\": \"%s\", \"plc_state\": \"%s\", \"firmware\": \"%s\"}",
gw_state, plcOnline ? "ONLINE" : "OFFLINE", FIRMWARE_VERSION);
client.publish("/line1/status", status_msg, true);
lastHeartbeat = millis();
}
}
// --- 명령 처리 타임아웃 감시 ---
// M 명령: 3초. D 명령: D 쓰기 + Apply ON + Apply OFF의 여러 통신 슬롯을 포함하므로 6초.
unsigned long cmd_timeout = pending_command_type_is_m ? 3000 : 6000;
if (pending_command_active && (millis() - pending_command_time > cmd_timeout)) {
Serial.printf("[TIMEOUT] Command %s expired (%lums). Force unlocking and rolling back buffers.\n",
pending_command_id, cmd_timeout);
publish_ack(pending_command_id, "FAILED");
// Old는 모멘터리 ON 상태일 수 있으므로, 명령 수신 직전 스냅샷으로 복원한다.
rollback_pending_command_buffers();
// D Apply 펄스가 걸려 있었다면 Apply 비트와 대기 상태를 정리한다.
if (d_apply_pending && d_apply_word_idx >= 0 && d_apply_word_idx < M_OUT_WORDS) {
M_Out_New[d_apply_word_idx] &= ~d_apply_word_mask;
M_Out_Pending_Write[d_apply_word_idx] &= ~d_apply_word_mask;
M_Out_Pending_Clear[d_apply_word_idx] &= ~d_apply_word_mask;
}
d_apply_pending = false;
d_apply_word_mask = 0;
d_apply_word_idx = -1;
pending_command_active = false;
pending_command_id[0] = '\0';
pending_command_time = 0;
}
}
// --- M영역 변경 감지 및 배치 MQTT 전송 (JsonDocument 힙 고갈 방지를 위해 3개 워드씩 Chunk 분할 전송, 워드 경계 일치로 변경 유실 차단) ---
bool detect_and_publish_M() {
if (!client.connected()) return false;
bool overall_success = true;
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
int pending_words[M_IN_WORDS];
int pending_word_count = 0;
for (int i = 0; i < M_IN_WORDS; i++) {
// Full-Sync 대기 상태인 경우 0xFFFF를 대입하여 전체 비트 송신 강제
uint16_t diff = full_sync_pending ? 0xFFFF : (M_In_New[i] ^ M_In_Old[i]);
if (diff != 0) {
pending_words[pending_word_count++] = i;
for (int j = 0; j < 16; j++) {
if ((diff >> j) & 1) {
int bit_id = i * 16 + j;
int val = (M_In_New[i] >> j) & 1;
Serial.printf("[LOG M_IN CHANGE] PLC M-Read Bit Offset %d (M%d) changed to %d\n", bit_id, 1000 + bit_id, val);
JsonArray pair = arr.add<JsonArray>();
pair.add(bit_id);
pair.add(val);
}
}
}
// 워드 경계 정렬을 위해 루프 본문 완료 시점에 3개 워드 단위로 청크 발행 판단
if (pending_word_count >= 3) {
char buffer[2048];
serializeJson(doc, buffer, sizeof(buffer));
// 발행 성공 시에만 해당 청크에 완전히 포함된 워드들의 Old 버퍼를 New와 동기화
bool ok = client.publish("/line1/m/in", buffer);
Serial.printf("[M_IN PUBLISH Chunk] size=%d success=%d payload=%s\n", arr.size(), ok, buffer);
if (ok) {
for (int w = 0; w < pending_word_count; w++) {
M_In_Old[pending_words[w]] = M_In_New[pending_words[w]];
}
} else {
overall_success = false;
}
pending_word_count = 0;
doc.clear();
arr = doc.to<JsonArray>();
}
}
if (arr.size() > 0) {
char buffer[2048];
serializeJson(doc, buffer, sizeof(buffer));
// 발행 성공 시에만 남은 워드들의 Old 버퍼를 New와 동기화
bool ok = client.publish("/line1/m/in", buffer);
Serial.printf("[M_IN PUBLISH Chunk Last] size=%d success=%d payload=%s\n", arr.size(), ok, buffer);
if (ok) {
for (int w = 0; w < pending_word_count; w++) {
M_In_Old[pending_words[w]] = M_In_New[pending_words[w]];
}
} else {
overall_success = false;
}
}
return overall_success;
}
// --- D영역 변경 감지 및 배치 MQTT 전송 ---
bool detect_and_publish_D() {
if (!client.connected()) return false;
bool overall_success = true;
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
int pending_groups[10];
int pending_group_count = 0;
for (int g = 0; g < 10; g++) {
uint16_t w_low_new = D_In_New[g * 2];
uint16_t w_high_new = D_In_New[g * 2 + 1];
uint16_t w_low_old = D_In_Old[g * 2];
uint16_t w_high_old = D_In_Old[g * 2 + 1];
// Full-Sync 대기 상태인 경우 무조건 송신 강제
if (full_sync_pending || w_low_new != w_low_old || w_high_new != w_high_old) {
JsonArray pair = arr.add<JsonArray>();
pair.add(g);
if (g == 0 || g == 5) {
// Pump restart time is an unsigned 16-bit value in the first word.
// The second word is still read to preserve the fixed 32-bit group layout, but is not part of the value.
pair.add(w_low_new);
Serial.printf("[LOG D_IN CHANGE] PLC D-Read Group %d (D%d only, UINT16) changed from %u to %u\n",
g, 2000 + g * 2, w_low_old, w_low_new);
} else {
// All other groups contain an IEEE-754 REAL split into low/high PLC words.
uint32_t raw_new = ((uint32_t)w_high_new << 16) | w_low_new;
uint32_t raw_old = ((uint32_t)w_high_old << 16) | w_low_old;
float val;
float old_val;
memcpy(&val, &raw_new, sizeof(val));
memcpy(&old_val, &raw_old, sizeof(old_val));
pair.add(val);
Serial.printf("[LOG D_IN CHANGE] PLC D-Read Group %d (D%d-D%d, REAL) changed from %.6g to %.6g\n",
g, 2000 + g * 2, 2000 + g * 2 + 1, old_val, val);
}
pending_groups[pending_group_count++] = g;
}
}
if (arr.size() > 0) {
char buffer[1024];
serializeJson(doc, buffer, sizeof(buffer));
// 발행 성공 시에만 해당 그룹의 Old 버퍼를 New와 동기화
bool ok = client.publish("/line1/d/in", buffer);
Serial.printf("[D_IN PUBLISH Batch] size=%d success=%d payload=%s\n", arr.size(), ok, buffer);
if (ok) {
for (int w = 0; w < pending_group_count; w++) {
int idx = pending_groups[w];
D_In_Old[idx * 2] = D_In_New[idx * 2];
D_In_Old[idx * 2 + 1] = D_In_New[idx * 2 + 1];
}
} else {
overall_success = false;
}
}
return overall_success;
}
// --- Read 변경사항 종합 프로세스 실행 ---
bool process_read_changes() {
bool success_m = detect_and_publish_M();
bool success_d = detect_and_publish_D();
return success_m && success_d;
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef MQTT_HANDLER_H
#define MQTT_HANDLER_H
#include <Arduino.h>
#include <PubSubClient.h>
extern PubSubClient client;
void setup_mqtt();
void maintain_mqtt();
bool detect_and_publish_M();
bool detect_and_publish_D();
bool process_read_changes();
void publish_ack(const char* command_id, const char* status);
#endif // MQTT_HANDLER_H
+18
View File
@@ -0,0 +1,18 @@
[platformio]
src_dir = .
build_dir = C:/aaa
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
upload_port = COM7
monitor_port = COM7
COM4monitor_encoding = UTF-8 ; 인코딩 강제 지정
; 이 프로젝트에서 사용하는 3가지 라이브러리를 정확히 등록합니다.
lib_deps =
bblanchon/ArduinoJson @ ^7.0.0
knolleary/PubSubClient @ ^2.8
robtillaart/CRC @ ^1.0.3