1390 lines
40 KiB
C++
1390 lines
40 KiB
C++
#include <WiFi.h>
|
|
#include <WiFiManager.h>
|
|
#include <PubSubClient.h>
|
|
#include <ArduinoJson.h>
|
|
#include <Wire.h>
|
|
#include <DFRobotDFPlayerMini.h>
|
|
#include <RTClib.h>
|
|
#include <NTPClient.h>
|
|
#include <HTTPClient.h>
|
|
#include <PCF8575.h>
|
|
#include <driver/i2s.h>
|
|
#include <Preferences.h>
|
|
|
|
// ========== Debug Configuration ==========
|
|
#define SERIAL_DEBUG true
|
|
#define LOG(message) do { \
|
|
if (SERIAL_DEBUG) { \
|
|
Serial.flush(); \
|
|
Serial.print("["); \
|
|
Serial.print(millis()); \
|
|
Serial.print("] "); \
|
|
Serial.println(message); \
|
|
} \
|
|
} while (0)
|
|
|
|
// ========== Server Configuration ==========
|
|
const char* VOICERSS_API_KEY = "90927de8275148d79080facd20fb486c";
|
|
const char* VOICERSS_URL = "http://api.voicerss.org/?key=%s&hl=%s&v=%s&c=WAV&f=22khz_16bit_mono&src=%s";
|
|
|
|
// ========== Configurable Parameters ==========
|
|
char mqtt_server[40] = "192.168.1.5";
|
|
char ip_server_laravel[40] = "192.168.110.10";
|
|
|
|
const char* IP_SERVER_LARAVEL = "192.168.110.10";
|
|
const int SERVER_PORT = 8000;
|
|
const char* API_ENDPOINT = "/api/bell-events";
|
|
|
|
// ========== MQTT Configuration ==========
|
|
const char* MQTT_SERVER = "192.168.1.5";
|
|
const int MQTT_PORT = 1883;
|
|
const char* MQTT_CLIENT_ID = "esp32_bel_sekolah";
|
|
const char* MQTT_USER = "";
|
|
const char* MQTT_PASSWORD = "";
|
|
|
|
// MQTT Topics
|
|
const char* TOPIC_COMMAND_STATUS = "bel/sekolah/command/status";
|
|
const char* TOPIC_COMMAND_RING = "bel/sekolah/command/ring";
|
|
const char* TOPIC_COMMAND_SYNC = "bel/sekolah/command/sync";
|
|
const char* TOPIC_RESPONSE_STATUS = "bel/sekolah/response/status";
|
|
const char* TOPIC_RESPONSE_ACK = "bel/sekolah/response/ack";
|
|
const char* TOPIC_EVENT_SCHEDULE = "bel/sekolah/events/schedule";
|
|
const char* TOPIC_EVENT_MANUAL = "bel/sekolah/events/manual";
|
|
const char* TOPIC_CONTROL_RELAY = "control/relay";
|
|
const char* TOPIC_TTS_PLAY = "tts/play";
|
|
const char* TOPIC_ANNOUNCEMENT_STATUS = "announcement/status";
|
|
|
|
// ========== Hardware Configuration ==========
|
|
#define RELAY_COUNT 48
|
|
#define I2C_SDA_PIN 8
|
|
#define I2C_SCL_PIN 9
|
|
#define DFPLAYER_RX_PIN 17
|
|
#define DFPLAYER_TX_PIN 18
|
|
|
|
// Konfigurasi I2S
|
|
#define I2S_BCK_PIN 12
|
|
#define I2S_WS_PIN 13
|
|
#define I2S_DOUT_PIN 14
|
|
#define SAMPLE_RATE 22050
|
|
|
|
// ========== Hardware Components ==========
|
|
WiFiClient espClient;
|
|
PubSubClient mqttClient(espClient);
|
|
DFRobotDFPlayerMini dfPlayer;
|
|
RTC_DS3231 rtc;
|
|
PCF8575 relayController;
|
|
PCF8575 relayController2;
|
|
PCF8575 relayController3;
|
|
|
|
void setupRTC(uint8_t i2cAddress);
|
|
void setupRelayController(uint8_t i2cAddress);
|
|
|
|
// ========== System State ==========
|
|
struct ActiveSchedule {
|
|
int index = -1;
|
|
unsigned long startTime = 0;
|
|
};
|
|
|
|
struct SystemState {
|
|
bool wifiConnected = false;
|
|
bool rtcConnected = false;
|
|
bool dfPlayerConnected = false;
|
|
bool mqttConnected = false;
|
|
bool relayController1Connected = false;
|
|
bool relayController2Connected = false;
|
|
bool relayController3Connected = false;
|
|
bool isPlaying = false;
|
|
unsigned long lastCommunication = 0;
|
|
unsigned long lastSync = 0;
|
|
unsigned long lastNtpSync = 0;
|
|
int scheduleCount = 0;
|
|
bool isSchedulePlaying = false;
|
|
unsigned long scheduleCooldownStart = 0;
|
|
const unsigned long SCHEDULE_COOLDOWN = 60000;
|
|
int currentPlayingSchedule = -1;
|
|
uint16_t relayStates1 = 0xFFFF;
|
|
uint16_t relayStates2 = 0xFFFF;
|
|
uint16_t relayStates3 = 0xFFFF;
|
|
ActiveSchedule activeSchedules[3];
|
|
unsigned long lastI2CCheck = 0;
|
|
bool i2cStable = true;
|
|
int i2cErrorCount = 0;
|
|
unsigned long lastI2CRecovery = 0;
|
|
};
|
|
|
|
// ========== Global Variables ==========
|
|
bool isTTSPending = false;
|
|
String lastTTSPayload;
|
|
unsigned long streamStartTime = 0;
|
|
const unsigned long STREAM_TIMEOUT = 30000;
|
|
Preferences preferences;
|
|
|
|
|
|
// ========== Schedule Storage ==========
|
|
struct Schedule {
|
|
String day; // "Senin", "Selasa", etc.
|
|
String time; // "HH:MM"
|
|
String fileNumber; // "0001"
|
|
int volume = 15; // 0-30
|
|
int repeat = 1; // 1-5
|
|
bool isActive = true;
|
|
};
|
|
|
|
SystemState state;
|
|
Schedule schedules[50]; // Max 50 schedules
|
|
int scheduleCount = 0;
|
|
|
|
// Konfigurasi I2S
|
|
i2s_config_t i2s_config = {
|
|
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
|
|
.sample_rate = 22050,
|
|
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
|
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // Stereo format, walau mono
|
|
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
|
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
|
.dma_buf_count = 16, // 8 buffer
|
|
.dma_buf_len = 512, // 1024 samples per buffer
|
|
.use_apll = false, // Nonaktifkan APLL, gunakan PLL_D2_CLK
|
|
.tx_desc_auto_clear = true, // Auto clear TX descriptor
|
|
.fixed_mclk = 0 // Biarkan kosong untuk clock default
|
|
};
|
|
|
|
i2s_pin_config_t pin_config = {
|
|
.bck_io_num = I2S_BCK_PIN,
|
|
.ws_io_num = I2S_WS_PIN,
|
|
.data_out_num = I2S_DOUT_PIN,
|
|
.data_in_num = I2S_PIN_NO_CHANGE
|
|
};
|
|
|
|
// ========== NTP Client ==========
|
|
WiFiUDP ntpUDP;
|
|
// Ganti di deklarasi NTPClient
|
|
NTPClient timeClient(ntpUDP, "id.pool.ntp.org", 7 * 3600, 60000); // UTC+7 dengan server Indonesia
|
|
|
|
// ========== Setup Functions ==========
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
Serial.println("\n\n==== BOOT ====");
|
|
LOG("Serial initialized");
|
|
LOG("Starting School Bell System - ESP32-S3 N16R8");
|
|
LOG("Firmware Version: 1.0.0");
|
|
LOG("CPU Frequency: " + String(getCpuFrequencyMhz()) + "MHz");
|
|
// Optimasi khusus ESP32-S3
|
|
setupESP32S3();
|
|
// Hardcode I2C address untuk PCF8575
|
|
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
|
|
Wire.setClock(100000);
|
|
Wire.setTimeOut(250);
|
|
setupRelayController(0x20, 1);
|
|
setupRelayController(0x21, 2);
|
|
setupRelayController(0x22, 3);
|
|
// Tetap coba setup RTC dengan scan
|
|
setupRTC(0x68); // Coba address default DS3231 (0x68)
|
|
scanI2CDevices();
|
|
setupI2S();
|
|
setupDFPlayer();
|
|
setupWiFi();
|
|
syncRTCWithNTP();
|
|
setupMQTT();
|
|
loadSchedulesFromPreferences();
|
|
LOG("System initialization complete");
|
|
}
|
|
|
|
|
|
void setupESP32S3() {
|
|
// Konfigurasi khusus untuk ESP32-S3
|
|
|
|
WiFi.setTxPower(WIFI_POWER_19_5dBm); // Maksimal power untuk jangkauan lebih baik
|
|
WiFi.setSleep(false); // Nonaktifkan sleep mode untuk koneksi lebih stabil
|
|
|
|
// Atur clock CPU ke 240MHz untuk performa maksimal
|
|
setCpuFrequencyMhz(240);
|
|
LOG("CPU Frequency: " + String(getCpuFrequencyMhz()) + "MHz");
|
|
|
|
// Enable brownout detector
|
|
esp_sleep_enable_timer_wakeup(1);
|
|
}
|
|
|
|
void setupWiFi() {
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.setTxPower(WIFI_POWER_19_5dBm);
|
|
WiFi.setAutoReconnect(true);
|
|
WiFi.persistent(true);
|
|
|
|
// Nonaktifkan power save mode
|
|
esp_wifi_set_ps(WIFI_PS_NONE);
|
|
WiFi.setSleep(false);
|
|
|
|
// Baca konfigurasi yang tersimpan
|
|
preferences.begin("wifi_config", true);
|
|
String savedSSID = preferences.getString("ssid", "");
|
|
String savedPass = preferences.getString("pass", "");
|
|
String savedMqtt = preferences.getString("mqtt_server", "");
|
|
String savedLaravel = preferences.getString("ip_server_laravel", "");
|
|
preferences.end();
|
|
|
|
if (savedMqtt.length() > 0) strcpy(mqtt_server, savedMqtt.c_str());
|
|
if (savedLaravel.length() > 0) strcpy(ip_server_laravel, savedLaravel.c_str());
|
|
|
|
// Jika sudah ada kredensial WiFi, coba konek langsung
|
|
if (savedSSID.length() > 0) {
|
|
WiFi.begin(savedSSID.c_str(), savedPass.c_str());
|
|
|
|
unsigned long startAttemptTime = millis();
|
|
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 10000) {
|
|
delay(100);
|
|
}
|
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
state.wifiConnected = true;
|
|
LOG("WiFi connected using saved credentials");
|
|
LOG("IP Address: " + WiFi.localIP().toString());
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Jika tidak berhasil, buka portal config
|
|
startConfigPortal();
|
|
}
|
|
|
|
void startConfigPortal() {
|
|
WiFiManager wifiManager;
|
|
|
|
// Buat parameter custom
|
|
WiFiManagerParameter custom_mqtt_server("mqtt", "MQTT Server", mqtt_server, 40);
|
|
WiFiManagerParameter custom_laravel_server("laravel", "Laravel Server", ip_server_laravel, 40);
|
|
|
|
wifiManager.addParameter(&custom_mqtt_server);
|
|
wifiManager.addParameter(&custom_laravel_server);
|
|
|
|
wifiManager.setConfigPortalTimeout(60);
|
|
wifiManager.setConnectTimeout(30);
|
|
|
|
if (!wifiManager.autoConnect("ESP32-S3-Bell-AP")) {
|
|
LOG("Failed to connect and hit timeout");
|
|
delay(3000);
|
|
ESP.restart();
|
|
}
|
|
|
|
// Simpan parameter yang diinput
|
|
strcpy(mqtt_server, custom_mqtt_server.getValue());
|
|
strcpy(ip_server_laravel, custom_laravel_server.getValue());
|
|
|
|
// Simpan ke preferences
|
|
preferences.begin("wifi_config", false);
|
|
preferences.putString("ssid", WiFi.SSID());
|
|
preferences.putString("pass", WiFi.psk());
|
|
preferences.putString("mqtt_server", mqtt_server);
|
|
preferences.putString("ip_server_laravel", ip_server_laravel);
|
|
preferences.end();
|
|
|
|
state.wifiConnected = true;
|
|
LOG("WiFi connected. IP: " + WiFi.localIP().toString());
|
|
LOG("MQTT Server: " + String(mqtt_server));
|
|
LOG("Laravel Server: " + String(ip_server_laravel));
|
|
}
|
|
|
|
void resetConfiguration() {
|
|
preferences.begin("wifi_config", false);
|
|
preferences.clear();
|
|
preferences.end();
|
|
|
|
LOG("Configuration reset. Restarting...");
|
|
delay(1000);
|
|
ESP.restart();
|
|
}
|
|
|
|
// Modified setupRTC to accept address
|
|
void setupRTC(uint8_t i2cAddress) {
|
|
// Try DS3231 first
|
|
rtc = RTC_DS3231();
|
|
if (rtc.begin()) {
|
|
state.rtcConnected = true;
|
|
LOG("DS3231 RTC initialized at 0x" + String(i2cAddress, HEX));
|
|
syncRTCWithNTP();
|
|
return;
|
|
}
|
|
|
|
// If DS3231 fails, try other RTC types
|
|
LOG("RTC not detected at 0x" + String(i2cAddress, HEX));
|
|
}
|
|
|
|
void scanI2CDevices() {
|
|
byte error, address;
|
|
int nDevices = 0;
|
|
|
|
LOG("Scanning I2C bus...");
|
|
for(address = 1; address < 127; address++) {
|
|
Wire.beginTransmission(address);
|
|
error = Wire.endTransmission();
|
|
|
|
if (error == 0) {
|
|
LOG("I2C device found at 0x" + String(address, HEX));
|
|
nDevices++;
|
|
} else if (error == 4) {
|
|
LOG("Unknown error at 0x" + String(address, HEX));
|
|
}
|
|
}
|
|
|
|
if (nDevices == 0) {
|
|
LOG("No I2C devices found");
|
|
} else {
|
|
LOG("Found " + String(nDevices) + " I2C devices");
|
|
}
|
|
}
|
|
|
|
|
|
void setupDFPlayer() {
|
|
LOG("Initializing DFPlayer...");
|
|
Serial2.begin(9600, SERIAL_8N1, DFPLAYER_RX_PIN, DFPLAYER_TX_PIN);
|
|
|
|
LOG("Checking DFPlayer connection...");
|
|
int retry = 0;
|
|
while (!dfPlayer.begin(Serial2) && retry < 5) {
|
|
LOG("DFPlayer not responding, retrying... (" + String(retry+1) + "/5)");
|
|
retry++;
|
|
}
|
|
|
|
if (retry >= 5) {
|
|
LOG("DFPlayer initialization FAILED!");
|
|
LOG("Possible causes:");
|
|
LOG("1. Wrong RX/TX wiring (harus cross: TX->RX, RX->TX)");
|
|
LOG("2. Power insufficient (butuhkan 3.3V-5V stabil)");
|
|
LOG("3. SD card tidak terdeteksi (format FAT32)");
|
|
state.dfPlayerConnected = false;
|
|
return;
|
|
}
|
|
|
|
state.dfPlayerConnected = true;
|
|
dfPlayer.enableDAC(); // Aktifkan DAC output
|
|
dfPlayer.volume(25);
|
|
dfPlayer.outputDevice(DFPLAYER_DEVICE_SD);
|
|
LOG("DFPlayer initialized successfully");
|
|
LOG("Current volume: " + String(dfPlayer.readVolume()));
|
|
}
|
|
|
|
// setupRelayController to accept address
|
|
void setupRelayController(uint8_t i2cAddress, int controllerNumber) {
|
|
int retryCount = 0;
|
|
const int maxRetries = 3;
|
|
while (retryCount < maxRetries) {
|
|
if (controllerNumber == 1) {
|
|
relayController = PCF8575(i2cAddress);
|
|
if (relayController.begin()) {
|
|
state.relayController1Connected = true;
|
|
relayController.write16(0xFFFF);
|
|
LOG("Relay controller 1 initialized at 0x" + String(i2cAddress, HEX));
|
|
return;
|
|
}
|
|
} else if (controllerNumber == 2) {
|
|
relayController2 = PCF8575(i2cAddress);
|
|
if (relayController2.begin()) {
|
|
state.relayController2Connected = true;
|
|
relayController2.write16(0xFFFF);
|
|
LOG("Relay controller 2 initialized at 0x" + String(i2cAddress, HEX));
|
|
return;
|
|
}
|
|
} else if (controllerNumber == 3) {
|
|
relayController3 = PCF8575(i2cAddress);
|
|
if (relayController3.begin()) {
|
|
state.relayController3Connected = true;
|
|
relayController3.write16(0xFFFF);
|
|
LOG("Relay controller 3 initialized at 0x" + String(i2cAddress, HEX));
|
|
return;
|
|
}
|
|
}
|
|
LOG("PCF8575 " + String(controllerNumber) + " tidak terdeteksi di 0x" + String(i2cAddress, HEX) +
|
|
", retrying (" + String(retryCount+1) + "/" + String(maxRetries) + ")");
|
|
retryCount++;
|
|
delay(100);
|
|
}
|
|
// Jika sampai sini berarti gagal
|
|
if (controllerNumber == 1) {
|
|
state.relayController1Connected = false;
|
|
} else if (controllerNumber == 2) {
|
|
state.relayController2Connected = false;
|
|
} else if (controllerNumber == 3) {
|
|
state.relayController3Connected = false;
|
|
}
|
|
LOG("Gagal menginisialisasi PCF8575 " + String(controllerNumber) + " setelah " + String(maxRetries) + " percobaan");
|
|
}
|
|
|
|
void setupI2S() {
|
|
esp_err_t err;
|
|
|
|
// 1. Uninstall driver hanya jika diperlukan (dengan penanganan error)
|
|
err = i2s_driver_uninstall(I2S_NUM_0);
|
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
|
LOG("Gagal uninstall driver I2S. Error: " + String(err));
|
|
// Lanjutkan saja karena mungkin belum terinstall
|
|
}
|
|
|
|
// 2. Set konfigurasi buffer baru
|
|
i2s_config.dma_buf_count = 16;
|
|
i2s_config.dma_buf_len = 512;
|
|
|
|
// 3. Install driver
|
|
err = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
|
|
if (err != ESP_OK) {
|
|
LOG("Gagal install driver I2S. Error: " + String(err));
|
|
if (err == ESP_ERR_INVALID_ARG) {
|
|
LOG("Invalid configuration");
|
|
}
|
|
else if (err == ESP_ERR_NO_MEM) {
|
|
LOG("Out of memory");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 4. Set pin configuration
|
|
err = i2s_set_pin(I2S_NUM_0, &pin_config);
|
|
if (err != ESP_OK) {
|
|
LOG("Gagal set pin I2S. Error: " + String(err));
|
|
i2s_driver_uninstall(I2S_NUM_0);
|
|
return;
|
|
}
|
|
|
|
// 5. Set clock rate
|
|
err = i2s_set_clk(I2S_NUM_0, SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
|
|
if (err != ESP_OK) {
|
|
LOG("Gagal set clock I2S. Error: " + String(err));
|
|
i2s_driver_uninstall(I2S_NUM_0);
|
|
return;
|
|
}
|
|
|
|
// 6. Clear buffer
|
|
i2s_zero_dma_buffer(I2S_NUM_0);
|
|
|
|
// 7. Log informasi
|
|
LOG("I2S driver berhasil diinisialisasi");
|
|
LOG("Konfigurasi I2S Terbaru:");
|
|
LOG("- Sample Rate: " + String(SAMPLE_RATE) + " Hz");
|
|
LOG("- Bit Depth: 16-bit");
|
|
LOG("- Channel: Mono");
|
|
LOG("- DMA Buffers: " + String(i2s_config.dma_buf_count) + " buffers");
|
|
LOG("- Samples per Buffer: " + String(i2s_config.dma_buf_len));
|
|
LOG("- Total Buffer Size: " + String(i2s_config.dma_buf_count * i2s_config.dma_buf_len * 2) + " bytes");
|
|
}
|
|
|
|
void setupMQTT() {
|
|
mqttClient.setServer(mqtt_server, MQTT_PORT); // Gunakan variabel yang bisa dikonfig
|
|
mqttClient.setCallback([](char* topic, uint8_t* payload, unsigned int length) {
|
|
mqttCallback(topic, payload, length);
|
|
});
|
|
mqttClient.setKeepAlive(60);
|
|
mqttClient.setSocketTimeout(30);
|
|
mqttClient.setBufferSize(4096);
|
|
}
|
|
|
|
// ========== Main Loop ==========
|
|
void loop() {
|
|
maintainMQTTConnection();
|
|
mqttClient.loop();
|
|
|
|
static unsigned long lastSecondTick = 0;
|
|
if (millis() - lastSecondTick >= 1000) {
|
|
lastSecondTick = millis();
|
|
checkSchedules();
|
|
}
|
|
|
|
// Periksa koneksi I2C setiap 5 detik
|
|
if (millis() - state.lastI2CCheck > 30000) {
|
|
state.lastI2CCheck = millis();
|
|
|
|
bool controller1OK = checkI2CConnection(0x20);
|
|
bool controller2OK = checkI2CConnection(0x21);
|
|
bool controller3OK = checkI2CConnection(0x22);
|
|
|
|
if (!controller1OK || !controller2OK) {
|
|
state.i2cErrorCount++;
|
|
state.i2cStable = false;
|
|
|
|
// Jika error mencapai 3 kali, coba reset I2C
|
|
if (state.i2cErrorCount >= 3) {
|
|
recoverI2CBus();
|
|
state.i2cErrorCount = 0;
|
|
}
|
|
} else {
|
|
state.i2cStable = true;
|
|
}
|
|
|
|
logI2CStatus();
|
|
}
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
if (state.activeSchedules[i].index != -1 &&
|
|
millis() - state.activeSchedules[i].startTime >= 60000) {
|
|
LOG("Resetting active schedule index: " + String(i));
|
|
state.activeSchedules[i].index = -1;
|
|
}
|
|
}
|
|
|
|
if (millis() - state.lastNtpSync > 24 * 60 * 60 * 1000) {
|
|
LOG("Syncing RTC with NTP");
|
|
syncRTCWithNTP();
|
|
}
|
|
|
|
state.lastCommunication = millis() / 1000;
|
|
}
|
|
|
|
// ========== Time Functions ==========
|
|
void syncRTCWithNTP() {
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
LOG("WiFi not connected for NTP sync");
|
|
return;
|
|
}
|
|
|
|
// Perbaiki dengan menambahkan pengecekan waktu yang valid
|
|
timeClient.forceUpdate();
|
|
unsigned long epochTime = timeClient.getEpochTime();
|
|
|
|
// Validasi waktu (harus antara tahun 2020-2030)
|
|
if (epochTime > 1577836800 && epochTime < 1893456000) { // 2020-2030
|
|
rtc.adjust(DateTime(epochTime));
|
|
state.lastNtpSync = millis();
|
|
LOG("RTC synced with NTP: " + getCurrentDateTime());
|
|
} else {
|
|
LOG("Invalid NTP time received: " + String(epochTime));
|
|
LOG("Skipping RTC sync to prevent year 2036 problem");
|
|
}
|
|
}
|
|
|
|
String getCurrentDateTime() {
|
|
DateTime now = rtc.now();
|
|
return String(now.year()) + "-" +
|
|
String(now.month()) + "-" +
|
|
String(now.day()) + " " +
|
|
formatTime(now.hour(), now.minute(), now.second());
|
|
}
|
|
|
|
String formatTime(int h, int m, int s) {
|
|
char buf[9]; // Format HH:MM:SS
|
|
sprintf(buf, "%02d:%02d:%02d", h, m, s);
|
|
return String(buf);
|
|
}
|
|
|
|
String getDayName(int dayIndex) {
|
|
const char* days[] = {"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"};
|
|
return String(days[dayIndex]);
|
|
}
|
|
|
|
// ========== MQTT Functions ==========
|
|
void maintainMQTTConnection() {
|
|
if (!mqttClient.connected()) {
|
|
reconnectMQTT();
|
|
}
|
|
}
|
|
|
|
void reconnectMQTT() {
|
|
static unsigned long lastAttempt = 0;
|
|
const unsigned long retryInterval = 5000; // 5 seconds
|
|
|
|
if (millis() - lastAttempt < retryInterval) {
|
|
return;
|
|
}
|
|
|
|
lastAttempt = millis();
|
|
LOG("Attempting MQTT connection...");
|
|
|
|
if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASSWORD)) {
|
|
state.mqttConnected = true;
|
|
subscribeToMQTTTopics();
|
|
sendSystemStatus();
|
|
LOG("MQTT connected and subscribed");
|
|
} else {
|
|
state.mqttConnected = false;
|
|
LOG("MQTT connection failed, rc=" + String(mqttClient.state()));
|
|
}
|
|
}
|
|
|
|
void subscribeToMQTTTopics() {
|
|
mqttClient.subscribe(TOPIC_COMMAND_STATUS, 1); // QoS 1
|
|
mqttClient.subscribe(TOPIC_COMMAND_RING, 1); // QoS 1
|
|
mqttClient.subscribe("bel/sekolah/command/sync", 1); // QoS=1
|
|
mqttClient.subscribe(TOPIC_RESPONSE_ACK, 1); // QoS 1 - Critical for sync confirmation
|
|
mqttClient.subscribe(TOPIC_RESPONSE_STATUS, 1); // QoS 1
|
|
mqttClient.subscribe(TOPIC_CONTROL_RELAY, 1);
|
|
mqttClient.subscribe(TOPIC_TTS_PLAY, 1);
|
|
LOG("Subscribed to all MQTT topics");
|
|
}
|
|
|
|
void mqttCallback(char* topic, uint8_t* payload, unsigned int length) {
|
|
String message;
|
|
for (int i = 0; i < length; i++) {
|
|
message += (char)payload[i];
|
|
}
|
|
|
|
LOG("MQTT [" + String(topic) + "]: " + message);
|
|
|
|
if (strcmp(topic, TOPIC_COMMAND_STATUS) == 0) {
|
|
handleStatusRequest();
|
|
}
|
|
else if (strcmp(topic, TOPIC_COMMAND_RING) == 0) {
|
|
handleRingCommand(message);
|
|
}
|
|
else if (strcmp(topic, TOPIC_COMMAND_SYNC) == 0) {
|
|
handleScheduleSync(message);
|
|
}
|
|
else if (strcmp(topic, TOPIC_CONTROL_RELAY) == 0) {
|
|
handleRelayControl(message);
|
|
}
|
|
else if (strcmp(topic, TOPIC_TTS_PLAY) == 0) {
|
|
handleTTSPlay(message);
|
|
}
|
|
}
|
|
|
|
// ========== Command Handlers ==========
|
|
void handleStatusRequest() {
|
|
sendSystemStatus();
|
|
}
|
|
|
|
void handleRelayControl(String payload) {
|
|
DynamicJsonDocument doc(256);
|
|
DeserializationError error = deserializeJson(doc, payload);
|
|
if (error) {
|
|
LOG("Error parsing relay control: " + String(error.c_str()));
|
|
return;
|
|
}
|
|
|
|
String action = doc["action"] | "";
|
|
String mode = doc["mode"] | "manual";
|
|
JsonArray ruangans = doc["ruang"].as<JsonArray>();
|
|
|
|
if (action == "activate") {
|
|
for (int room : ruangans) {
|
|
if (room >= 0 && room < RELAY_COUNT) {
|
|
setRelayState(room, true);
|
|
}
|
|
}
|
|
}
|
|
else if (action == "deactivate") {
|
|
for (int room : ruangans) {
|
|
if (room >= 0 && room < RELAY_COUNT) {
|
|
setRelayState(room, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mode != "tts") {
|
|
sendRelayStatusUpdate();
|
|
}
|
|
}
|
|
|
|
void handleTTSPlay(String payload) {
|
|
lastTTSPayload = payload;
|
|
DynamicJsonDocument doc(512);
|
|
DeserializationError error = deserializeJson(doc, payload);
|
|
if (error) {
|
|
LOG("Error parsing TTS play: " + String(error.c_str()));
|
|
return;
|
|
}
|
|
String text = doc["teks"] | "";
|
|
if (text.isEmpty()) {
|
|
LOG("Empty TTS text");
|
|
return;
|
|
}
|
|
|
|
String lang = doc["hl"] | "id-id";
|
|
String voice = doc["v"] | "intan";
|
|
|
|
JsonArray ruangans = doc["ruang"].as<JsonArray>();
|
|
|
|
// Nyalakan relay terlebih dahulu
|
|
for (int room : ruangans) {
|
|
if (room >= 0 && room < RELAY_COUNT) {
|
|
setRelayState(room, true);
|
|
}
|
|
}
|
|
sendRelayStatusUpdate();
|
|
|
|
// Mulai streaming TTS
|
|
playTTS(text, lang, voice);
|
|
|
|
// Tunggu sampai pemutaran selesai
|
|
unsigned long startTime = millis();
|
|
while (state.isPlaying && millis() - startTime < STREAM_TIMEOUT) {
|
|
delay(10); // Jangan boros CPU
|
|
}
|
|
|
|
// Setelah selesai, matikan relay
|
|
for (int room : ruangans) {
|
|
if (room >= 0 && room < RELAY_COUNT) {
|
|
setRelayState(room, false);
|
|
}
|
|
}
|
|
sendRelayStatusUpdate();
|
|
|
|
lastTTSPayload = ""; // Kosongkan payload
|
|
}
|
|
|
|
void handleRingCommand(String payload) {
|
|
static String lastPayload;
|
|
static unsigned long lastTime = 0;
|
|
|
|
// Cek duplikat dalam 5 detik terakhir
|
|
if (payload == lastPayload && millis() - lastTime < 5000) {
|
|
LOG("Ignoring duplicate ring command");
|
|
return;
|
|
}
|
|
|
|
lastPayload = payload;
|
|
lastTime = millis();
|
|
DynamicJsonDocument doc(256);
|
|
DeserializationError error = deserializeJson(doc, payload);
|
|
|
|
if (error) {
|
|
LOG("Ring command JSON error: " + String(error.c_str()));
|
|
sendAckResponse("error", "Invalid ring command format");
|
|
return;
|
|
}
|
|
|
|
if (!doc.containsKey("file_number")) {
|
|
sendAckResponse("error", "Missing file_number");
|
|
return;
|
|
}
|
|
|
|
playBellSound(
|
|
doc["file_number"].as<String>(),
|
|
doc["volume"] | 15,
|
|
doc["repeat"] | 1,
|
|
"manual"
|
|
);
|
|
}
|
|
|
|
// ========== Relay Control Functions ==========
|
|
void setRelayState(uint8_t relayNum, bool active) {
|
|
if (relayNum >= RELAY_COUNT) return;
|
|
|
|
// Jika I2C tidak stabil, coba recovery dulu
|
|
if (!state.i2cStable && millis() - state.lastI2CRecovery > 5000) {
|
|
recoverI2CBus();
|
|
}
|
|
|
|
// Coba operasi hingga 3 kali
|
|
for (int attempt = 0; attempt < 3; attempt++) {
|
|
bool success = false;
|
|
if (relayNum < 16) {
|
|
if (!state.relayController1Connected) break;
|
|
if (active) {
|
|
state.relayStates1 &= ~(1 << relayNum);
|
|
} else {
|
|
state.relayStates1 |= (1 << relayNum);
|
|
}
|
|
Wire.beginTransmission(0x20);
|
|
Wire.write(lowByte(state.relayStates1));
|
|
Wire.write(highByte(state.relayStates1));
|
|
byte error = Wire.endTransmission();
|
|
success = (error == 0);
|
|
}
|
|
else if (relayNum < 32) {
|
|
if (!state.relayController2Connected) break;
|
|
uint8_t pin = relayNum - 16;
|
|
if (active) {
|
|
state.relayStates2 &= ~(1 << pin);
|
|
} else {
|
|
state.relayStates2 |= (1 << pin);
|
|
}
|
|
Wire.beginTransmission(0x21);
|
|
Wire.write(lowByte(state.relayStates2));
|
|
Wire.write(highByte(state.relayStates2));
|
|
byte error = Wire.endTransmission();
|
|
success = (error == 0);
|
|
}
|
|
else {
|
|
if (!state.relayController3Connected) break;
|
|
uint8_t pin = relayNum - 32;
|
|
if (active) {
|
|
state.relayStates3 &= ~(1 << pin);
|
|
} else {
|
|
state.relayStates3 |= (1 << pin);
|
|
}
|
|
Wire.beginTransmission(0x22);
|
|
Wire.write(lowByte(state.relayStates3));
|
|
Wire.write(highByte(state.relayStates3));
|
|
byte error = Wire.endTransmission();
|
|
success = (error == 0);
|
|
}
|
|
|
|
if (success) {
|
|
LOG("Relay " + String(relayNum) + " set to " + (active ? "ON" : "OFF"));
|
|
return;
|
|
}
|
|
LOG("Attempt " + String(attempt+1) + " failed for relay " + String(relayNum));
|
|
delay(10);
|
|
}
|
|
LOG("Failed to set relay state after 3 attempts");
|
|
state.i2cStable = false;
|
|
}
|
|
|
|
void setAllRelays(bool active) {
|
|
if (active) {
|
|
state.relayStates1 = 0x0000; // Semua relay controller 1 ON
|
|
state.relayStates2 = 0x0000; // Semua relay controller 2 ON
|
|
state.relayStates3 = 0x0000; // Semua relay controller 3 ON
|
|
} else {
|
|
state.relayStates1 = 0xFFFF; // Semua relay controller 1 OFF
|
|
state.relayStates2 = 0xFFFF; // Semua relay controller 2 OFF
|
|
state.relayStates3 = 0xFFFF; // Semua relay controller 3 OFF
|
|
}
|
|
|
|
// Kirim ke semua controller yang terhubung
|
|
if (state.relayController1Connected) {
|
|
relayController.write16(state.relayStates1);
|
|
delay(10); // Jeda singkat antara operasi I2C
|
|
}
|
|
if (state.relayController2Connected) {
|
|
relayController2.write16(state.relayStates2);
|
|
delay(10);
|
|
}
|
|
if (state.relayController3Connected) {
|
|
relayController3.write16(state.relayStates3);
|
|
delay(10);
|
|
}
|
|
}
|
|
|
|
void sendRelayStatusUpdate() {
|
|
DynamicJsonDocument doc(256);
|
|
doc["status"] = "relay_update";
|
|
|
|
JsonArray activeRelays = doc.createNestedArray("active_relays");
|
|
for (int i = 0; i < RELAY_COUNT; i++) {
|
|
bool isActive = (i < 16) ?
|
|
!(state.relayStates1 & (1 << i)) :
|
|
!(state.relayStates2 & (1 << (i - 16)));
|
|
if (isActive) activeRelays.add(i);
|
|
}
|
|
|
|
String payload;
|
|
serializeJson(doc, payload);
|
|
mqttClient.publish(TOPIC_ANNOUNCEMENT_STATUS, payload.c_str());
|
|
}
|
|
|
|
void handleScheduleSync(String payload) {
|
|
LOG("Received sync payload: " + payload);
|
|
|
|
DynamicJsonDocument doc(2048);
|
|
DeserializationError error = deserializeJson(doc, payload);
|
|
|
|
if (error) {
|
|
LOG("JSON error: " + String(error.c_str()));
|
|
sendAckResponse("error", "Invalid schedule format");
|
|
return;
|
|
}
|
|
|
|
if (!doc.containsKey("schedules")) {
|
|
LOG("Missing schedules array");
|
|
sendAckResponse("error", "No schedules provided");
|
|
return;
|
|
}
|
|
|
|
JsonArray schedulesArray = doc["schedules"];
|
|
LOG("Found " + String(schedulesArray.size()) + " schedules");
|
|
|
|
// Simpan ke Preferences
|
|
saveSchedulesToPreferences(schedulesArray);
|
|
|
|
// Update schedule di memory
|
|
updateSchedules(schedulesArray);
|
|
}
|
|
|
|
void saveSchedulesToPreferences(JsonArray schedulesArray) {
|
|
// Buka namespace preferences
|
|
preferences.begin("bell_schedules", false);
|
|
|
|
// Hapus data lama
|
|
preferences.clear();
|
|
|
|
// Simpan jumlah schedule
|
|
preferences.putUInt("count", schedulesArray.size());
|
|
|
|
// Simpan setiap schedule
|
|
for (int i = 0; i < schedulesArray.size(); i++) {
|
|
JsonObject s = schedulesArray[i];
|
|
String prefix = "s" + String(i) + "_";
|
|
|
|
preferences.putString((prefix + "day").c_str(), s["hari"].as<String>());
|
|
preferences.putString((prefix + "time").c_str(), s["waktu"].as<String>().substring(0, 5));
|
|
preferences.putString((prefix + "file").c_str(), s["file_number"].as<String>());
|
|
preferences.putInt((prefix + "vol").c_str(), s["volume"] | 15);
|
|
preferences.putInt((prefix + "rep").c_str(), s["repeat"] | 1);
|
|
preferences.putBool((prefix + "active").c_str(), s["is_active"] | true);
|
|
}
|
|
|
|
preferences.end();
|
|
LOG("Schedules saved to flash");
|
|
}
|
|
|
|
void loadSchedulesFromPreferences() {
|
|
preferences.begin("bell_schedules", true);
|
|
|
|
scheduleCount = preferences.getUInt("count", 0);
|
|
LOG("Loading " + String(scheduleCount) + " schedules from flash");
|
|
|
|
for (int i = 0; i < scheduleCount; i++) {
|
|
String prefix = "s" + String(i) + "_";
|
|
|
|
schedules[i].day = preferences.getString((prefix + "day").c_str(), "");
|
|
schedules[i].time = preferences.getString((prefix + "time").c_str(), "");
|
|
schedules[i].fileNumber = preferences.getString((prefix + "file").c_str(), "");
|
|
schedules[i].volume = preferences.getInt((prefix + "vol").c_str(), 15);
|
|
schedules[i].repeat = preferences.getInt((prefix + "rep").c_str(), 1);
|
|
schedules[i].isActive = preferences.getBool((prefix + "active").c_str(), true);
|
|
|
|
LOG("Loaded schedule: " + schedules[i].day + " " +
|
|
schedules[i].time + " File:" + schedules[i].fileNumber);
|
|
}
|
|
|
|
preferences.end();
|
|
state.scheduleCount = scheduleCount;
|
|
}
|
|
|
|
void updateSchedules(JsonArray schedulesArray) {
|
|
scheduleCount = 0;
|
|
|
|
for (int i = 0; i < schedulesArray.size() && scheduleCount < 50; i++) {
|
|
JsonObject s = schedulesArray[i];
|
|
|
|
if (!s.containsKey("hari") || !s.containsKey("waktu") || !s.containsKey("file_number")) {
|
|
LOG("Skipping invalid schedule - missing required fields");
|
|
continue;
|
|
}
|
|
|
|
schedules[scheduleCount] = {
|
|
s["hari"].as<String>(), // day
|
|
s["waktu"].as<String>().substring(0, 5), // time
|
|
s["file_number"].as<String>(),// fileNumber
|
|
s["volume"] | 15, // volume
|
|
s["repeat"] | 1, // repeat
|
|
s["is_active"] | true // isActive
|
|
};
|
|
|
|
LOG("Added schedule: " + schedules[scheduleCount].day + " " +
|
|
schedules[scheduleCount].time + " File:" + schedules[scheduleCount].fileNumber);
|
|
|
|
scheduleCount++;
|
|
}
|
|
|
|
state.scheduleCount = scheduleCount;
|
|
state.lastSync = millis() / 1000;
|
|
LOG("Total schedules: " + String(scheduleCount));
|
|
}
|
|
|
|
// ========== Fungsi Audio ==========
|
|
void playTTS(String text, String language, String voice) {
|
|
LOG("playTTS() started");
|
|
|
|
// [1] Persiapan Awal
|
|
esp_wifi_set_ps(WIFI_PS_NONE);
|
|
WiFi.setSleep(false);
|
|
|
|
LOG("WiFi power settings adjusted");
|
|
|
|
// [2] Setup I2S
|
|
i2s_zero_dma_buffer(I2S_NUM_0);
|
|
i2s_stop(I2S_NUM_0);
|
|
LOG("I2S driver reset");
|
|
|
|
i2s_set_clk(I2S_NUM_0, SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
|
|
i2s_start(I2S_NUM_0);
|
|
LOG("I2S clock started");
|
|
|
|
// [3] Download Audio
|
|
String url = "http://api.voicerss.org/?key=" + String(VOICERSS_API_KEY) +
|
|
"&hl=" + language + "&v=" + voice +
|
|
"&c=WAV&f=22khz_16bit_mono&src=" + urlEncode(text);
|
|
LOG("Fetching TTS URL: " + url);
|
|
|
|
HTTPClient http;
|
|
http.setReuse(true);
|
|
http.setTimeout(60000);
|
|
|
|
if (!http.begin(url)) {
|
|
LOG("HTTP Begin failed");
|
|
return;
|
|
}
|
|
LOG("HTTP client started");
|
|
|
|
int httpCode = http.GET();
|
|
if (httpCode != HTTP_CODE_OK) {
|
|
LOG("HTTP Error: " + String(httpCode));
|
|
http.end();
|
|
return;
|
|
}
|
|
LOG("HTTP Response OK");
|
|
|
|
// [4] Baca Data Sekaligus
|
|
int contentLength = http.getSize();
|
|
LOG("Content length: " + String(contentLength));
|
|
|
|
if (contentLength <= 44) {
|
|
LOG("Invalid content length");
|
|
http.end();
|
|
return;
|
|
}
|
|
|
|
uint8_t* audioBuffer = (uint8_t*)ps_malloc(contentLength);
|
|
if (!audioBuffer) {
|
|
LOG("Memory allocation failed");
|
|
http.end();
|
|
return;
|
|
}
|
|
LOG("Memory allocated: " + String(contentLength));
|
|
|
|
int bytesRead = http.getStreamPtr()->readBytes(audioBuffer, contentLength);
|
|
http.end();
|
|
|
|
if (bytesRead != contentLength) {
|
|
LOG("Download incomplete (" + String(bytesRead) + "/" + String(contentLength) + ")");
|
|
free(audioBuffer);
|
|
return;
|
|
}
|
|
LOG("Audio data downloaded");
|
|
|
|
// [5] Proses Audio Data
|
|
uint8_t* audioData = audioBuffer + 44; // Skip header WAV
|
|
size_t audioSize = contentLength - 44;
|
|
|
|
// [6] Pre-fill DMA Buffer (SOLUSI UTAMA)
|
|
size_t prefillSize = 4096; // Isi sebagian buffer DMA terlebih dahulu
|
|
if (audioSize > prefillSize) {
|
|
size_t bytesWritten;
|
|
i2s_write(I2S_NUM_0, audioData, prefillSize, &bytesWritten, portMAX_DELAY);
|
|
audioData += prefillSize;
|
|
audioSize -= prefillSize;
|
|
}
|
|
|
|
// [7] Mainkan Sisa Audio
|
|
state.isPlaying = true;
|
|
isTTSPending = true;
|
|
|
|
size_t totalWritten = 0;
|
|
while (audioSize > 0 && state.isPlaying) {
|
|
size_t bytesWritten;
|
|
esp_err_t err = i2s_write(I2S_NUM_0, audioData, audioSize, &bytesWritten, portMAX_DELAY);
|
|
|
|
if (err != ESP_OK) {
|
|
LOG("I2S Write Error: " + String(err));
|
|
break;
|
|
}
|
|
|
|
audioData += bytesWritten;
|
|
audioSize -= bytesWritten;
|
|
totalWritten += bytesWritten;
|
|
|
|
// Beri jeda kecil untuk memastikan DAC tidak overflow
|
|
if (bytesWritten > 2048) {
|
|
delay(1);
|
|
}
|
|
}
|
|
|
|
// [9] Cleanup
|
|
free(audioBuffer);
|
|
state.isPlaying = false; // Audio sudah dikirim
|
|
LOG("Audio stream selesai");
|
|
}
|
|
|
|
// ========== Fungsi Pendukung ==========
|
|
String urlEncode(String str) {
|
|
String encodedString = "";
|
|
char c;
|
|
char code0;
|
|
char code1;
|
|
|
|
for (unsigned int i = 0; i < str.length(); i++) {
|
|
c = str.charAt(i);
|
|
|
|
if (c == ' ') {
|
|
encodedString += '+';
|
|
} else if (isalnum(c)) {
|
|
encodedString += c;
|
|
} else {
|
|
code1 = (c & 0xf) + '0';
|
|
if ((c & 0xf) > 9) {
|
|
code1 = (c & 0xf) - 10 + 'A';
|
|
}
|
|
c = (c >> 4) & 0xf;
|
|
code0 = c + '0';
|
|
if (c > 9) {
|
|
code0 = c - 10 + 'A';
|
|
}
|
|
encodedString += '%';
|
|
encodedString += code0;
|
|
encodedString += code1;
|
|
}
|
|
}
|
|
|
|
return encodedString;
|
|
}
|
|
|
|
bool checkI2CConnection(uint8_t address) {
|
|
Wire.beginTransmission(address);
|
|
byte error = Wire.endTransmission();
|
|
if (error == 0) {
|
|
return true;
|
|
} else {
|
|
LOG("I2C error at 0x" + String(address, HEX) + ": " + getI2CErrorString(error));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
String getI2CErrorString(byte error) {
|
|
switch(error) {
|
|
case 0: return "Success";
|
|
case 1: return "Data too long";
|
|
case 2: return "NACK on address";
|
|
case 3: return "NACK on data";
|
|
case 4: return "Other error";
|
|
default: return "Unknown error";
|
|
}
|
|
}
|
|
|
|
void recoverI2CBus() {
|
|
LOG("Attempting I2C bus recovery...");
|
|
|
|
// 1. Stop I2C
|
|
Wire.end();
|
|
delay(100);
|
|
|
|
// 2. Kembalikan pin ke default
|
|
pinMode(I2C_SDA_PIN, INPUT_PULLUP);
|
|
pinMode(I2C_SCL_PIN, INPUT_PULLUP);
|
|
delay(100);
|
|
|
|
// 3. Coba clock out any stuck bits
|
|
for (int i = 0; i < 10; i++) {
|
|
pinMode(I2C_SCL_PIN, OUTPUT);
|
|
digitalWrite(I2C_SCL_PIN, LOW);
|
|
delayMicroseconds(10);
|
|
digitalWrite(I2C_SCL_PIN, HIGH);
|
|
pinMode(I2C_SCL_PIN, INPUT_PULLUP);
|
|
delayMicroseconds(10);
|
|
}
|
|
|
|
// 4. Re-init I2C
|
|
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
|
|
Wire.setClock(100000);
|
|
Wire.setTimeOut(250);
|
|
|
|
LOG("I2C bus recovery completed");
|
|
state.lastI2CRecovery = millis();
|
|
|
|
// Set ulang status relay controller
|
|
if (state.relayController1Connected) {
|
|
relayController.write16(state.relayStates1);
|
|
}
|
|
if (state.relayController2Connected) {
|
|
relayController2.write16(state.relayStates2);
|
|
}
|
|
if (state.relayController3Connected) {
|
|
relayController3.write16(state.relayStates3);
|
|
}
|
|
}
|
|
|
|
void logI2CStatus() {
|
|
DynamicJsonDocument doc(512); // Perbesar ukuran buffer
|
|
doc["i2c_stable"] = state.i2cStable;
|
|
doc["i2c_error_count"] = state.i2cErrorCount;
|
|
doc["controller1_connected"] = state.relayController1Connected;
|
|
doc["controller2_connected"] = state.relayController2Connected;
|
|
doc["controller3_connected"] = state.relayController3Connected;
|
|
doc["last_recovery"] = state.lastI2CRecovery;
|
|
String payload;
|
|
serializeJson(doc, payload);
|
|
LOG("I2C Status: " + payload);
|
|
}
|
|
|
|
// ========== Bell Functions ==========
|
|
void playBellSound(String fileNumber, int volume, int repeat, const char* triggerType) {
|
|
if (!state.dfPlayerConnected) {
|
|
LOG("DFPlayer not connected - cannot play sound");
|
|
sendAckResponse("error", "DFPlayer not connected");
|
|
return;
|
|
}
|
|
|
|
// Set volume ke maksimal (30)
|
|
int finalVolume = 25; // Ubah ini dari variabel parameter ke nilai tetap 30
|
|
|
|
LOG("Playing bell: " + fileNumber + " Vol:" + String(finalVolume) + " Repeat:" + String(repeat));
|
|
|
|
dfPlayer.volume(finalVolume); // Gunakan finalVolume yang sudah di-set ke 30
|
|
int repeatCount = constrain(repeat, 1, 5);
|
|
|
|
// Nyalakan semua relay
|
|
setAllRelays(true);
|
|
sendRelayStatusUpdate();
|
|
|
|
for (int i = 0; i < repeatCount; i++) {
|
|
dfPlayer.play(fileNumber.toInt());
|
|
LOG("Putar ke-" + String(i+1) + " dimulai");
|
|
|
|
// Tunggu selama 30 detik untuk setiap pemutaran
|
|
unsigned long startTime = millis();
|
|
while (millis() - startTime < 30000) {
|
|
delay(10);
|
|
}
|
|
|
|
LOG("Putar ke-" + String(i+1) + " selesai (setelah 30 detik)");
|
|
}
|
|
|
|
// Matikan semua relay
|
|
setAllRelays(false);
|
|
sendRelayStatusUpdate();
|
|
|
|
logBellEvent(fileNumber, finalVolume, repeatCount, triggerType);
|
|
sendAckResponse("success", "Bel berbunyi");
|
|
}
|
|
|
|
void logBellEvent(String fileNumber, int volume, int repeat, const char* triggerType) {
|
|
DateTime now = rtc.now();
|
|
|
|
DynamicJsonDocument doc(256);
|
|
doc["hari"] = getDayName(now.dayOfTheWeek());
|
|
doc["waktu"] = formatTime(now.hour(), now.minute(), now.second());
|
|
doc["file_number"] = fileNumber;
|
|
doc["trigger_type"] = triggerType;
|
|
doc["volume"] = volume;
|
|
doc["repeat"] = repeat;
|
|
|
|
String payload;
|
|
serializeJson(doc, payload);
|
|
|
|
// Tentukan topik berdasarkan trigger type
|
|
const char* topic = (strcmp(triggerType, "schedule") == 0) ?
|
|
TOPIC_EVENT_SCHEDULE : TOPIC_EVENT_MANUAL;
|
|
|
|
// Kirim MQTT
|
|
bool mqttSuccess = mqttClient.publish(topic, payload.c_str());
|
|
|
|
// Kirim HTTP
|
|
sendBellEventViaHTTP(payload, triggerType);
|
|
|
|
LOG(mqttSuccess ? "MQTT publish success" : "MQTT publish failed");
|
|
}
|
|
|
|
// Fungsi HTTP async sederhana
|
|
void sendBellEventViaHTTP(String jsonPayload, const char* triggerType) {
|
|
static WiFiClient client;
|
|
static HTTPClient http;
|
|
|
|
if (http.connected()) {
|
|
http.end();
|
|
}
|
|
|
|
// Gunakan endpoint berbeda untuk manual dan schedule
|
|
String endpoint = (strcmp(triggerType, "schedule") == 0) ?
|
|
"/api/bell-events/schedule" : "/api/bell-events/manual";
|
|
|
|
String url = "http://" + String(ip_server_laravel) + ":" + String(SERVER_PORT) + endpoint;
|
|
http.begin(client, url);
|
|
http.addHeader("Content-Type", "application/json");
|
|
|
|
LOG("Sending HTTP to: " + url);
|
|
int httpCode = http.POST(jsonPayload);
|
|
|
|
if (httpCode > 0) {
|
|
LOG("HTTP response: " + String(httpCode));
|
|
} else {
|
|
LOG("HTTP failed: " + String(httpCode));
|
|
}
|
|
}
|
|
|
|
// ========== Response Functions ==========
|
|
void sendSystemStatus() {
|
|
DynamicJsonDocument doc(256);
|
|
doc["wifi"] = state.wifiConnected;
|
|
doc["rtc"] = state.rtcConnected;
|
|
doc["dfplayer"] = state.dfPlayerConnected;
|
|
doc["mqtt"] = state.mqttConnected;
|
|
|
|
if (state.rtcConnected) {
|
|
DateTime now = rtc.now();
|
|
doc["rtc_time"] = now.unixtime();
|
|
doc["rtc_formatted"] = getCurrentDateTime();
|
|
}
|
|
|
|
doc["last_communication"] = state.lastCommunication;
|
|
doc["last_sync"] = state.lastSync;
|
|
doc["schedule_count"] = state.scheduleCount;
|
|
doc["ip_address"] = WiFi.localIP().toString();
|
|
|
|
// Tambahkan status I2C
|
|
doc["i2c_stable"] = state.i2cStable;
|
|
doc["i2c_error_count"] = state.i2cErrorCount;
|
|
doc["last_i2c_check"] = state.lastI2CCheck;
|
|
doc["last_i2c_recovery"] = state.lastI2CRecovery;
|
|
|
|
String payload;
|
|
serializeJson(doc, payload);
|
|
|
|
if (!mqttClient.publish(TOPIC_RESPONSE_STATUS, payload.c_str())) {
|
|
LOG("Failed to publish status");
|
|
}
|
|
}
|
|
|
|
void sendAckResponse(const char* status, const char* message) {
|
|
DynamicJsonDocument doc(128);
|
|
doc["status"] = status;
|
|
doc["message"] = message;
|
|
doc["timestamp"] = state.lastCommunication;
|
|
|
|
String payload;
|
|
serializeJson(doc, payload);
|
|
|
|
if (!mqttClient.publish(TOPIC_RESPONSE_ACK, payload.c_str())) {
|
|
LOG("Failed to publish ack");
|
|
}
|
|
}
|
|
|
|
// ========== Schedule Checking ==========
|
|
// Di checkSchedules():
|
|
void checkSchedules() {
|
|
static unsigned long lastCheck = 0;
|
|
if (millis() - lastCheck < 1000) return;
|
|
lastCheck = millis();
|
|
|
|
if (!state.rtcConnected || scheduleCount == 0) return;
|
|
|
|
DateTime now = rtc.now();
|
|
int currentHour = now.hour();
|
|
int currentMinute = now.minute();
|
|
String currentDay = getDayName(now.dayOfTheWeek());
|
|
|
|
for (int i = 0; i < scheduleCount; i++) {
|
|
if (!schedules[i].isActive) continue;
|
|
|
|
// Cek jika schedule sudah aktif
|
|
bool alreadyActive = false;
|
|
for (int j = 0; j < 3; j++) {
|
|
if (state.activeSchedules[j].index == i) {
|
|
alreadyActive = true;
|
|
break;
|
|
}
|
|
}
|
|
if (alreadyActive) continue;
|
|
|
|
// Parse waktu schedule
|
|
int schedHour = schedules[i].time.substring(0,2).toInt();
|
|
int schedMinute = schedules[i].time.substring(3,5).toInt();
|
|
|
|
if (schedules[i].day == currentDay &&
|
|
currentHour == schedHour &&
|
|
currentMinute == schedMinute) {
|
|
|
|
// Cari slot kosong
|
|
for (int j = 0; j < 3; j++) {
|
|
if (state.activeSchedules[j].index == -1) {
|
|
state.activeSchedules[j].index = i;
|
|
state.activeSchedules[j].startTime = millis();
|
|
|
|
playBellSound(
|
|
schedules[i].fileNumber,
|
|
schedules[i].volume,
|
|
schedules[i].repeat,
|
|
"schedule"
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |