초기화
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user