/**
* PROJECT : Industrial Lamp Control System (ILCS)
* VERSION : V4 (Enhanced Reliability)
* FIX : Added debounce, EEPROM schema version, error recovery, LCD refresh, lamp state caching.
*/
#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#define NUM_LAMPS 6
#define NUM_COMBOS (1 << NUM_LAMPS)
#define NUM_PROFILES 6
#define EEPROM_MAGIC 0xFEEDDEAD
#define EEPROM_SCHEMA 0x0001 // Tambahan versi schema
const int lampPins[NUM_LAMPS] = {5, 18, 19, 25, 26, 27};
#define BTN_NEXT 4
#define BTN_PREV 15
#define BTN_SAVE 32
#define BTN_MODE 13
#define BUZZER_PIN 23
enum SystemMode { MODE_COMBO, MODE_PROFILE, MODE_SLEEP };
enum DisplayCmd { CMD_REFRESH, CMD_START_SAVE, CMD_WAKE, CMD_SLEEP, CMD_ERROR };
enum BuzzType { BUZZ_NAV = 50, BUZZ_OK = 200, BUZZ_ERR = 500 };
struct ConfigData {
uint32_t magic;
uint16_t schema;
int profiles[NUM_PROFILES];
uint16_t crc;
};
struct DisplayState {
SystemMode mode;
int currentCombo;
int activeProfile;
bool backlight;
bool busy;
};
QueueHandle_t buzzQueue;
QueueHandle_t lcdQueue;
SemaphoreHandle_t dataMutex;
ConfigData gConfig;
DisplayState gState = {MODE_COMBO, 1, 0, true, false};
int lampCache[NUM_LAMPS] = {0}; // cache untuk state lampu
// --- CRC ---
uint16_t calcCRC(const ConfigData& data) {
uint16_t crc = 0xFFFF;
const uint8_t* p = (const uint8_t*)&data;
for (size_t i = 0; i < offsetof(ConfigData, crc); i++) {
crc ^= p[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
}
return crc;
}
// --- Helpers ---
void sendBuzzer(BuzzType type) { xQueueSend(buzzQueue, &type, pdMS_TO_TICKS(10)); }
void updateDisplayReq(DisplayCmd cmd) { xQueueSend(lcdQueue, &cmd, pdMS_TO_TICKS(50)); }
void applyLamps(int combo) {
for (int i = 0; i < NUM_LAMPS; i++) {
int val = (combo >> i) & 1;
if (lampCache[i] != val) {
lampCache[i] = val;
digitalWrite(lampPins[i], val);
}
}
}
// --- DISPLAY TASK ---
void display_task(void *pv) {
LiquidCrystal_I2C lcd(0x27, 16, 2);
lcd.init(); lcd.backlight();
DisplayCmd cmd;
char buf[17];
uint32_t timer = 0;
bool animActive = false, errorActive = false;
for (;;) {
if (xQueueReceive(lcdQueue, &cmd, pdMS_TO_TICKS(50))) {
switch (cmd) {
case CMD_WAKE: lcd.backlight(); break;
case CMD_SLEEP: lcd.noBacklight(); break;
case CMD_START_SAVE: animActive = true; timer = millis(); break;
case CMD_ERROR: errorActive = true; timer = millis(); break;
case CMD_REFRESH: lcd.clear(); break; // forced redraw
default: break;
}
}
xSemaphoreTake(dataMutex, portMAX_DELAY);
DisplayState s = gState;
int savedVal = gConfig.profiles[s.activeProfile];
xSemaphoreGive(dataMutex);
if (errorActive) {
lcd.setCursor(0,0); lcd.print("EEPROM FAILURE! ");
if (millis() - timer > 2000) errorActive = false;
}
else if (animActive) {
uint32_t elapsed = millis() - timer;
if (elapsed < 1500) {
snprintf(buf, 17, "SAVING TO P%d %s", s.activeProfile, (elapsed/300 % 2 == 0) ? ".." : "...");
lcd.setCursor(0,0); lcd.print(buf);
} else {
lcd.setCursor(0,0); lcd.print("SUCCESS SAVED! ");
if (elapsed > 2500) {
animActive = false;
xSemaphoreTake(dataMutex, portMAX_DELAY);
gState.busy = false;
xSemaphoreGive(dataMutex);
}
}
}
else if (s.backlight) {
if (s.mode == MODE_COMBO) {
lcd.setCursor(0,0); lcd.print("MODE: SET COMBO ");
snprintf(buf, 17, "Combo: %-3d ", s.currentCombo);
lcd.setCursor(0,1); lcd.print(buf);
} else if (s.mode == MODE_PROFILE) {
snprintf(buf, 17, "TARGET: PROF P%d", s.activeProfile);
lcd.setCursor(0,0); lcd.print(buf);
snprintf(buf, 17, "Old:%-3d New:%-3d", savedVal, s.currentCombo);
lcd.setCursor(0,1); lcd.print(buf);
} else {
lcd.setCursor(0,0); lcd.print("SYSTEM SLEEP ");
lcd.setCursor(0,1); lcd.print("Press Any Key ");
}
}
}
}
// --- BUTTON TASK ---
void button_task(void *pv) {
uint32_t lastAct = millis();
bool lastBtn[4] = {1, 1, 1, 1};
uint32_t debounceTimer[4] = {0,0,0,0};
const int pins[4] = {BTN_NEXT, BTN_PREV, BTN_MODE, BTN_SAVE};
for (;;) {
xSemaphoreTake(dataMutex, portMAX_DELAY);
bool isBusy = gState.busy;
xSemaphoreGive(dataMutex);
if (!isBusy) {
for (int i = 0; i < 4; i++) {
bool state = digitalRead(pins[i]);
if (state == LOW && lastBtn[i] == HIGH && millis() - debounceTimer[i] > 150) {
debounceTimer[i] = millis();
xSemaphoreTake(dataMutex, portMAX_DELAY);
lastAct = millis();
if (gState.mode == MODE_SLEEP) {
gState.mode = MODE_COMBO; gState.backlight = true;
updateDisplayReq(CMD_WAKE);
} else {
switch(i) {
case 0: // NEXT
if (gState.mode == MODE_COMBO) {
gState.currentCombo = (gState.currentCombo % (NUM_COMBOS-1)) + 1;
applyLamps(gState.currentCombo);
} else gState.activeProfile = (gState.activeProfile + 1) % NUM_PROFILES;
break;
case 1: // PREV
if (gState.mode == MODE_COMBO) {
gState.currentCombo = (gState.currentCombo <= 1) ? NUM_COMBOS-1 : gState.currentCombo-1;
applyLamps(gState.currentCombo);
} else gState.activeProfile = (gState.activeProfile <= 0) ? NUM_PROFILES-1 : gState.activeProfile-1;
break;
case 2: // MODE
if (gState.mode == MODE_COMBO) {
gState.mode = MODE_PROFILE;
} else {
gState.mode = MODE_COMBO;
gState.currentCombo = gConfig.profiles[gState.activeProfile];
applyLamps(gState.currentCombo);
}
break;
case 3: // SAVE
if (gState.mode == MODE_PROFILE) {
gState.busy = true;
gConfig.profiles[gState.activeProfile] = gState.currentCombo;
gConfig.crc = calcCRC(gConfig);
EEPROM.put(0, gConfig);
if (EEPROM.commit()) {
updateDisplayReq(CMD_START_SAVE);
sendBuzzer(BUZZ_OK);
} else {
updateDisplayReq(CMD_ERROR);
gState.busy = false;
sendBuzzer(BUZZ_ERR);
}
}
break;
}
sendBuzzer(BUZZ_NAV);
}
updateDisplayReq(CMD_REFRESH);
xSemaphoreGive(dataMutex);
}
lastBtn[i] = state;
}
}
if (millis() - lastAct > 30000) {
xSemaphoreTake(dataMutex, portMAX_DELAY);
if (gState.mode != MODE_SLEEP && !gState.busy) {
gState.mode = MODE_SLEEP; gState.backlight = false;
updateDisplayReq(CMD_SLEEP);
}
xSemaphoreGive(dataMutex);
}
vTaskDelay(pdMS_TO_TICKS(25));
}
}
// --- BUZZER TASK ---
void buzzer_task(void *pv) {
BuzzType type;
for (;;) {
if (xQueueReceive(buzzQueue, &type, portMAX_DELAY)) {
digitalWrite(BUZZER_PIN, HIGH);
vTaskDelay(pdMS_TO_TICKS((int)type));
digitalWrite(BUZZER_PIN, LOW);
}
}
}
// --- SETUP ---
void setup() {
EEPROM.begin(sizeof(ConfigData));
dataMutex = xSemaphoreCreateMutex();
buzzQueue = xQueueCreate(5, sizeof(BuzzType));
lcdQueue = xQueueCreate(10, sizeof(DisplayCmd));
for (int i=0; i<NUM_LAMPS; i++) {
pinMode(lampPins[i], OUTPUT);
lampCache[i] = 0;
}
pinMode(BTN_NEXT, INPUT_PULLUP);
pinMode(BTN_PREV, INPUT_PULLUP);
pinMode(BTN_SAVE, INPUT_PULLUP);
pinMode(BTN_MODE, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
EEPROM.get(0, gConfig);
if (gConfig.magic != EEPROM_MAGIC || gConfig.schema != EEPROM_SCHEMA || gConfig.crc != calcCRC(gConfig)) {
gConfig.magic = EEPROM_MAGIC;
gConfig.schema = EEPROM_SCHEMA;
for(int i=0; i<NUM_PROFILES; i++) gConfig.profiles[i] = 1;
gConfig.crc = calcCRC(gConfig);
EEPROM.put(0, gConfig);
EEPROM.commit();
}
gState.currentCombo = gConfig.profiles[0];
applyLamps(gState.currentCombo);
xTaskCreatePinnedToCore(display_task, "Disp", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(buzzer_task, "Buzz", 1024, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(button_task, "Ctrl", 4096, NULL, 1, NULL, 1);
}
// --- LOOP ---
void loop() {
vTaskDelete(NULL);
}