// File Shared Master & Slave
#pragma once
#include <Arduino.h>
// ============================================================
// RS485 PROTOCOL DEFINITIONS (Master & Slave Shared Contract)
// ============================================================
constexpr uint8_t START_BYTE = 0xAA;
constexpr size_t PACKET_SIZE = 9; // 9 Byte: Start, Src, Dest, Cmd, Floor, Mode, Dir/Fault, CRC_Lo, CRC_Hi
// Indeks Byte Paket
constexpr uint8_t PKT_IDX_START = 0;
constexpr uint8_t PKT_IDX_SRC = 1; // Alamat pengirim (Master / Slave ID)
constexpr uint8_t PKT_IDX_DEST = 2; // Alamat penerima tujuan
constexpr uint8_t PKT_IDX_CMD = 3; // Kode perintah / status
constexpr uint8_t PKT_IDX_FLOOR = 4; // Lantai target / saat ini
constexpr uint8_t PKT_IDX_MODE = 5; // Mode sistem (Auto / Inspection dll)
constexpr uint8_t PKT_IDX_DIR_FAULT = 6; // Arah gerak & status *critical error*
constexpr uint8_t PKT_IDX_CRC_LO = 7; // CRC Low Byte
constexpr uint8_t PKT_IDX_CRC_HI = 8; // CRC High Byte
// Daftar Command / Perintah Protokol
constexpr uint8_t CMD_POLL_STATUS = 0x10;
constexpr uint8_t CMD_CAR_CALL = 0x20;
constexpr uint8_t CMD_DOOR_OPEN = 0x30;
constexpr uint8_t CMD_DOOR_CLOSE = 0x31;
constexpr uint8_t CMD_JOG_DOOR_OPEN = 0x40;
constexpr uint8_t CMD_JOG_DOOR_CLOSE= 0x41;
constexpr uint8_t CMD_EMERGENCY = 0x50;
constexpr uint8_t CMD_LIGHT_TOGGLE = 0x60;
constexpr uint8_t ACK = 0x70;
// Slave Side
#include "RS485Manager.h"
extern SystemState systemState;
extern BuzzerManager buzzerMgr;
extern ButtonManager buttonMgr;
RS485Manager::RS485Manager() : lastByteTime(0), rxIdx(0) {
for (size_t i = 0; i < PACKET_SIZE; ++i) {
rxBuf[i] = 0;
}
}
void RS485Manager::init() {
Serial2.begin(9600, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN);
#ifdef RS485_DE_RE_PIN
pinMode(RS485_DE_RE_PIN, OUTPUT);
digitalWrite(RS485_DE_RE_PIN, LOW);
#endif
}
void RS485Manager::sendResponse() {
uint8_t pkt[PACKET_SIZE];
pkt[PKT_IDX_START] = START_BYTE;
pkt[PKT_IDX_SRC] = SLAVE_ADDRESS;
pkt[PKT_IDX_DEST] = 0x01; // Alamat Master
// Susun status data tombol/perintah ke byte CMD dan Floor
if (buttonMgr.emergencyPressed) {
pkt[PKT_IDX_CMD] = CMD_EMERGENCY;
pkt[PKT_IDX_FLOOR] = 0x01;
} else if (buttonMgr.isDoorJogOpenHeld) {
pkt[PKT_IDX_CMD] = CMD_JOG_DOOR_OPEN;
pkt[PKT_IDX_FLOOR] = 0x00;
} else if (buttonMgr.isDoorJogCloseHeld) {
pkt[PKT_IDX_CMD] = CMD_JOG_DOOR_CLOSE;
pkt[PKT_IDX_FLOOR] = 0x00;
} else if (buttonMgr.pendingLightToggle) {
pkt[PKT_IDX_CMD] = CMD_LIGHT_TOGGLE;
pkt[PKT_IDX_FLOOR] = 0x00;
} else if (buttonMgr.pendingCall > 0) {
pkt[PKT_IDX_CMD] = CMD_CAR_CALL;
pkt[PKT_IDX_FLOOR] = buttonMgr.pendingCall;
} else if (buttonMgr.pendingDoorCmd > 0) {
pkt[PKT_IDX_CMD] = buttonMgr.pendingDoorCmd;
pkt[PKT_IDX_FLOOR] = 0x00;
} else {
pkt[PKT_IDX_CMD] = CMD_POLL_STATUS;
pkt[PKT_IDX_FLOOR] = systemState.getFloor();
}
pkt[PKT_IDX_MODE] = static_cast<uint8_t>(systemState.getMode());
pkt[PKT_IDX_DIR_FAULT] = (static_cast<uint8_t>(systemState.getDir()) & 0x0F) | (systemState.isFault() ? 0x80 : 0x00);
const uint16_t crc = CRCManager::calculate(pkt, PACKET_SIZE - 2);
pkt[PKT_IDX_CRC_LO] = crc & 0xFF;
pkt[PKT_IDX_CRC_HI] = (crc >> 8) & 0xFF;
#ifdef RS485_DE_RE_PIN
digitalWrite(RS485_DE_RE_PIN, HIGH);
delayMicroseconds(50);
#endif
Serial2.write(pkt, PACKET_SIZE);
Serial2.flush();
#ifdef RS485_DE_RE_PIN
delayMicroseconds(50);
digitalWrite(RS485_DE_RE_PIN, LOW);
#endif
}
void RS485Manager::handleIncoming() {
while (Serial2.available()) {
uint8_t b = Serial2.read();
unsigned long now = millis();
if (now - lastByteTime > 50) {
rxIdx = 0;
}
lastByteTime = now;
if (rxIdx == 0 && b != START_BYTE) {
continue; // Abaikan sampai ketemu Start Byte
}
if (rxIdx < PACKET_SIZE) {
rxBuf[rxIdx++] = b;
}
if (rxIdx == PACKET_SIZE) {
uint16_t crcRec = rxBuf[PKT_IDX_CRC_LO] | (rxBuf[PKT_IDX_CRC_HI] << 8);
if (crcRec == CRCManager::calculate(rxBuf, PACKET_SIZE - 2) && rxBuf[PKT_IDX_DEST] == SLAVE_ADDRESS) {
const bool wasFault = systemState.isFault();
const Mode wasMode = systemState.getMode();
systemState.setOnline(true);
systemState.setLastRxTime(millis());
const Mode newMode = static_cast<Mode>(rxBuf[PKT_IDX_MODE]);
const uint8_t dirFaultByte = rxBuf[PKT_IDX_DIR_FAULT];
const Direction newDir = static_cast<Direction>(dirFaultByte & 0x0F);
const bool newFault = ((dirFaultByte & 0x80) != 0);
const uint8_t newFloor = rxBuf[PKT_IDX_FLOOR];
systemState.setMode(newMode);
systemState.setFault(newFault);
if (newFault && !wasFault) {
buzzerMgr.startEmergency();
} else if (!newFault && wasFault) {
buzzerMgr.stopEmergency();
}
if (buttonMgr.lastCalledFloor != 0 && newFloor == buttonMgr.lastCalledFloor) {
buzzerMgr.trigger(2, ARRIVAL_BEEP_ON_MS, ARRIVAL_BEEP_OFF_MS);
}
if (newFloor >= 1 && newFloor <= TOTAL_FLOORS && newFloor < 4 && buttonMgr.isLedFloorActive(newFloor)) {
digitalWrite(newFloor == 1 ? LED_F1 : newFloor == 2 ? LED_F2 : LED_F3, LOW);
buttonMgr.setLedFloorActive(newFloor, false);
buttonMgr.setLedCallTimestamp(newFloor, 0);
}
if (newFault) {
buttonMgr.turnOffAllCallLEDs();
}
if (newFault != wasFault || newMode != wasMode) {
systemState.setDisplayRefreshNeeded(true);
}
systemState.setFloor(newFloor);
systemState.setDir(newDir);
// Kirim respons balik ke Master setelah menerima polling/perintah
sendResponse();
}
rxIdx = 0;
}
}
}
// Master Side