446 lines
15 KiB
C++
446 lines
15 KiB
C++
#include <WiFi.h>
|
|
#include <PubSubClient.h>
|
|
#include <kontrol_suara_inferencing.h>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "driver/i2s.h"
|
|
|
|
// === WiFi & MQTT ===
|
|
const char* ssid = "Koala";
|
|
const char* password = "joinprayerdulu";
|
|
const char* mqtt_server = "test.mosquitto.org";
|
|
WiFiClient espClient;
|
|
PubSubClient client(espClient);
|
|
bool mqtt_connected = false;
|
|
|
|
|
|
// === Variabel LED saat rekaman window audio ===
|
|
bool window_led_active = false;
|
|
unsigned long window_listen_start_time = 0;
|
|
const unsigned long window_duration_ms = (EI_CLASSIFIER_RAW_SAMPLE_COUNT * 1000UL) / EI_CLASSIFIER_FREQUENCY;
|
|
bool voiceAutoLockInProgress = false;
|
|
unsigned long voiceAutoLockStart = 0;
|
|
unsigned long ledper_last_blink = 0;
|
|
bool ledper_state = false;
|
|
const unsigned long ledper_blink_interval = 100;
|
|
bool skip_blink_active = false;
|
|
|
|
|
|
// === Mode Wake Word dan Perintah ===
|
|
bool wake_word_active = false;
|
|
unsigned long command_start_time = 0;
|
|
const unsigned long command_window_ms =20000;
|
|
|
|
|
|
// === Output Pin ===
|
|
#define PIN_LAMPU_KAMAR 15
|
|
#define PIN_LAMPU_TERAS 4
|
|
#define PIN_KIPAS 5
|
|
#define PIN_SOLENOID 18
|
|
#define PIN_LED 19
|
|
#define PIN_LEDPER 21
|
|
|
|
// === Inferensi Audio ===
|
|
typedef struct {
|
|
signed short* buffers[2];
|
|
unsigned char buf_select;
|
|
unsigned char buf_ready;
|
|
unsigned int buf_count;
|
|
unsigned int n_samples;
|
|
} inference_t;
|
|
|
|
static inference_t inference;
|
|
static const uint32_t sample_buffer_size = 2048;
|
|
static signed short sampleBuffer[sample_buffer_size];
|
|
static bool debug_nn = false;
|
|
static int print_results = -(EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW);
|
|
static bool record_status = true;
|
|
static TaskHandle_t captureTaskHandle = NULL;
|
|
|
|
// === I2S Audio ===
|
|
static int i2s_init(uint32_t sampling_rate) {
|
|
i2s_config_t i2s_config = {
|
|
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
|
|
.sample_rate = sampling_rate,
|
|
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
|
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
|
|
.communication_format = I2S_COMM_FORMAT_I2S,
|
|
.intr_alloc_flags = 0,
|
|
.dma_buf_count = 8,
|
|
.dma_buf_len = 512,
|
|
.use_apll = false,
|
|
.tx_desc_auto_clear = false,
|
|
.fixed_mclk = -1,
|
|
};
|
|
i2s_pin_config_t pin_config = {
|
|
.bck_io_num = 26,
|
|
.ws_io_num = 32,
|
|
.data_out_num = -1,
|
|
.data_in_num = 33,
|
|
};
|
|
|
|
esp_err_t ret = i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
|
|
if (ret != ESP_OK) ei_printf("i2s_driver_install error\n");
|
|
ret = i2s_set_pin(I2S_NUM_1, &pin_config);
|
|
if (ret != ESP_OK) ei_printf("i2s_set_pin error\n");
|
|
i2s_zero_dma_buffer(I2S_NUM_1);
|
|
return ret == ESP_OK ? 0 : -1;
|
|
}
|
|
|
|
static void audio_inference_callback(uint32_t n_bytes) {
|
|
for (int i = 0; i < n_bytes >> 1; i++) {
|
|
inference.buffers[inference.buf_select][inference.buf_count++] = sampleBuffer[i];
|
|
if (inference.buf_count >= inference.n_samples) {
|
|
inference.buf_select ^= 1;
|
|
inference.buf_count = 0;
|
|
inference.buf_ready = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void capture_samples(void* arg) {
|
|
size_t bytes_read = sample_buffer_size;
|
|
while (record_status) {
|
|
i2s_read(I2S_NUM_1, (void*)sampleBuffer, sample_buffer_size, &bytes_read, 100);
|
|
if (bytes_read > 0) {
|
|
for (int x = 0; x < bytes_read / 2; x++) {
|
|
sampleBuffer[x] = (int16_t)(sampleBuffer[x]) * 16;
|
|
}
|
|
audio_inference_callback(bytes_read);
|
|
}
|
|
}
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
static bool microphone_inference_start(uint32_t n_samples) {
|
|
inference.buffers[0] = (short*)malloc(n_samples * sizeof(short));
|
|
inference.buffers[1] = (short*)malloc(n_samples * sizeof(short));
|
|
if (!inference.buffers[0] || !inference.buffers[1]) return false;
|
|
|
|
inference.buf_select = 0;
|
|
inference.buf_count = 0;
|
|
inference.n_samples = n_samples;
|
|
inference.buf_ready = 0;
|
|
|
|
if (i2s_init(EI_CLASSIFIER_FREQUENCY) != 0) return false;
|
|
record_status = true;
|
|
|
|
if (captureTaskHandle == NULL) {
|
|
xTaskCreate(capture_samples, "CaptureSamples", 1024 * 32, NULL, 10, &captureTaskHandle);
|
|
ei_printf("🎙 Task audio capture dimulai\n");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool microphone_inference_record(void) {
|
|
if (inference.buf_ready == 1) return false;
|
|
while (inference.buf_ready == 0) delay(1);
|
|
inference.buf_ready = 0;
|
|
return true;
|
|
}
|
|
|
|
static int microphone_audio_signal_get_data(size_t offset, size_t length, float* out_ptr) {
|
|
numpy::int16_to_float(&inference.buffers[inference.buf_select ^ 1][offset], out_ptr, length);
|
|
return 0;
|
|
}
|
|
|
|
// === WiFi Setup ===
|
|
void setup_wifi() {
|
|
WiFi.begin(ssid, password);
|
|
while (WiFi.status() != WL_CONNECTED) {
|
|
delay(500);
|
|
Serial.print(".");
|
|
}
|
|
Serial.println("\n✅ WiFi terhubung!");
|
|
}
|
|
|
|
// === MQTT Callback ===
|
|
bool autoLockInProgress = false;
|
|
unsigned long autoLockStart = 0;
|
|
|
|
void publishMQTT(const char* topic, const char* message) {
|
|
if (client.connected()) client.publish(topic, message, true);
|
|
}
|
|
|
|
void publishStatus(const char* device, bool state) {
|
|
String topic = String("cristine/status/") + device;
|
|
const char* message = state ? "on" : "off";
|
|
publishMQTT(topic.c_str(), message);
|
|
}
|
|
|
|
void callback(char* topic, byte* payload, unsigned int length) {
|
|
String message;
|
|
for (unsigned int i = 0; i < length; i++) message += (char)payload[i];
|
|
message.trim();
|
|
message.toLowerCase();
|
|
|
|
if (String(topic) == "cristine/control/lampu_kamar") {
|
|
bool state = (message == "on");
|
|
digitalWrite(PIN_LAMPU_KAMAR, state ? HIGH : LOW);
|
|
publishStatus("lampu_kamar", state);
|
|
ei_printf("📡 Lampu kamar %s via MQTT\n", state ? "dinyalakan" : "dimatikan");
|
|
} else if (String(topic) == "cristine/control/lampu_teras") {
|
|
bool state = (message == "on");
|
|
digitalWrite(PIN_LAMPU_TERAS, state ? HIGH : LOW);
|
|
publishStatus("lampu_teras", state);
|
|
ei_printf("📡 Lampu teras %s via MQTT\n", state ? "dinyalakan" : "dimatikan");
|
|
} else if (String(topic) == "cristine/control/kipas") {
|
|
bool state = (message == "on");
|
|
digitalWrite(PIN_KIPAS, state ? HIGH : LOW);
|
|
publishStatus("kipas", state);
|
|
ei_printf("📡 Kipas %s via MQTT\n", state ? "dinyalakan" : "dimatikan");
|
|
} else if (String(topic) == "cristine/control/pintu") {
|
|
if (message == "on" && !autoLockInProgress) {
|
|
ei_printf("📡 Pintu dibuka via MQTT\n");
|
|
digitalWrite(PIN_SOLENOID, HIGH);
|
|
publishStatus("pintu", true);
|
|
autoLockStart = millis();
|
|
autoLockInProgress = true;
|
|
}
|
|
} else if (String(topic) == "cristine/request/status") {
|
|
String statusPayload = "{";
|
|
statusPayload += "\"lampu kamar\": " + String(digitalRead(PIN_LAMPU_KAMAR) == HIGH ? "true" : "false") + ",";
|
|
statusPayload += "\"lampu teras\": " + String(digitalRead(PIN_LAMPU_TERAS) == HIGH ? "true" : "false") + ",";
|
|
statusPayload += "\"kipas\": " + String(digitalRead(PIN_KIPAS) == HIGH ? "true" : "false") + ",";
|
|
statusPayload += "\"pintu\": \"" + String(digitalRead(PIN_SOLENOID) == HIGH ? "dibuka" : "dikunci") + "\"";
|
|
statusPayload += "}";
|
|
client.publish("cristine/status", statusPayload.c_str());
|
|
ei_printf("📤 Status terkirim ke bot WhatsApp\n");
|
|
}
|
|
}
|
|
|
|
// === MQTT Reconnect ===
|
|
void reconnect() {
|
|
while (!client.connected()) {
|
|
Serial.println("🔁 Reconnecting MQTT...");
|
|
String clientId = "ESP32Client-" + String(random(0xffff), HEX);
|
|
if (client.connect(clientId.c_str())) {
|
|
Serial.println("✅ MQTT Connected!");
|
|
mqtt_connected = true;
|
|
|
|
client.subscribe("cristine/control/lampu_kamar");
|
|
client.subscribe("cristine/control/lampu_teras");
|
|
client.subscribe("cristine/control/kipas");
|
|
client.subscribe("cristine/control/pintu");
|
|
client.subscribe("cristine/request/status");
|
|
} else {
|
|
Serial.print("❌ MQTT gagal, rc=");
|
|
Serial.println(client.state());
|
|
mqtt_connected = false;
|
|
delay(5000);
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Stop Inference ===
|
|
void stop_inference() {
|
|
record_status = false;
|
|
if (captureTaskHandle != NULL) {
|
|
vTaskDelete(captureTaskHandle);
|
|
captureTaskHandle = NULL;
|
|
}
|
|
i2s_driver_uninstall(I2S_NUM_1);
|
|
free(inference.buffers[0]);
|
|
free(inference.buffers[1]);
|
|
ei_printf("🛑 Inferensi dihentikan\n");
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
setup_wifi();
|
|
client.setServer(mqtt_server, 1883);
|
|
client.setCallback(callback);
|
|
|
|
pinMode(PIN_LAMPU_KAMAR, OUTPUT);
|
|
pinMode(PIN_LAMPU_TERAS, OUTPUT);
|
|
pinMode(PIN_KIPAS, OUTPUT);
|
|
pinMode(PIN_SOLENOID, OUTPUT);
|
|
pinMode(PIN_LED, OUTPUT);
|
|
pinMode(PIN_LEDPER, OUTPUT);
|
|
digitalWrite(PIN_LED, LOW);
|
|
digitalWrite(PIN_LEDPER, LOW);
|
|
|
|
run_classifier_init();
|
|
ei_printf("\n🎬 Menunggu koneksi MQTT...\n");
|
|
}
|
|
|
|
void loop() {
|
|
client.loop();
|
|
if (!client.connected()) {
|
|
mqtt_connected = false;
|
|
if (captureTaskHandle != NULL) stop_inference();
|
|
reconnect();
|
|
}
|
|
|
|
if (autoLockInProgress && millis() - autoLockStart >= 10000) {
|
|
digitalWrite(PIN_SOLENOID, LOW);
|
|
publishStatus("pintu", false);
|
|
ei_printf("🔒 Pintu dikunci otomatis setelah 10 detik\n");
|
|
autoLockInProgress = false;
|
|
}
|
|
|
|
if (voiceAutoLockInProgress && millis() - voiceAutoLockStart >= 10000) {
|
|
digitalWrite(PIN_SOLENOID, LOW);
|
|
publishStatus("pintu", false);
|
|
ei_printf("🔒 Pintu dikunci otomatis setelah 10 detik (via suara)\n");
|
|
voiceAutoLockInProgress = false;
|
|
}
|
|
|
|
if (mqtt_connected && captureTaskHandle == NULL) {
|
|
ei_printf("🎙 Mulai inferensi\n");
|
|
if (!microphone_inference_start(EI_CLASSIFIER_SLICE_SIZE)) return;
|
|
}
|
|
|
|
if (!microphone_inference_record()) return;
|
|
|
|
// === LED Perintah: Berkedip saat skip aktif
|
|
if (skip_blink_active) {
|
|
if (millis() - ledper_last_blink >= ledper_blink_interval) {
|
|
ledper_last_blink = millis();
|
|
ledper_state = !ledper_state;
|
|
digitalWrite(PIN_LEDPER, ledper_state);
|
|
}
|
|
}
|
|
|
|
// === LED Indikator Window Audio
|
|
if (!window_led_active) {
|
|
window_listen_start_time = millis();
|
|
window_led_active = true;
|
|
digitalWrite(PIN_LED, HIGH);
|
|
}
|
|
if (window_led_active && millis() - window_listen_start_time >= window_duration_ms) {
|
|
window_led_active = false;
|
|
digitalWrite(PIN_LED, LOW);
|
|
}
|
|
|
|
// === Inference Voice Command
|
|
signal_t signal;
|
|
signal.total_length = EI_CLASSIFIER_SLICE_SIZE;
|
|
signal.get_data = µphone_audio_signal_get_data;
|
|
ei_impulse_result_t result = { 0 };
|
|
EI_IMPULSE_ERROR r = run_classifier_continuous(&signal, &result, debug_nn);
|
|
if (r != EI_IMPULSE_OK) return;
|
|
|
|
static int print_results = -(EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW);
|
|
static bool skip_next_command_window = false;
|
|
static unsigned long perintah_start_time = 0;
|
|
|
|
if (++print_results >= EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW) {
|
|
print_results = 0;
|
|
|
|
const char* top_label = "";
|
|
float top_score = 0.0f;
|
|
float silent_score = 0.0f, unknown_score = 0.0f;
|
|
|
|
for (size_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
|
|
float score = result.classification[i].value;
|
|
if (score > top_score) {
|
|
top_score = score;
|
|
top_label = result.classification[i].label;
|
|
}
|
|
|
|
if (strcmp(result.classification[i].label, "silent") == 0) silent_score = score;
|
|
if (strcmp(result.classification[i].label, "unknown") == 0) unknown_score = score;
|
|
}
|
|
|
|
bool noise_only = (silent_score > 0.7f || unknown_score > 0.7f);
|
|
|
|
// === Logging Confidence
|
|
if (!wake_word_active && !skip_next_command_window) {
|
|
ei_printf("[Menunggu Wake Word] %s: %.2f\n", top_label, top_score);
|
|
} else if (wake_word_active && skip_next_command_window && print_results == 0) {
|
|
ei_printf("[Lewati 1 Window] %s: %.2f\n", top_label, top_score);
|
|
} else if (wake_word_active && !skip_next_command_window) {
|
|
ei_printf("[Mode Perintah] %s: %.2f\n", top_label, top_score);
|
|
}
|
|
|
|
// === Wake Word Detection
|
|
if (!wake_word_active && strcmp(top_label, "wake_word") == 0 && top_score > 0.90f && !noise_only) {
|
|
ei_printf("🟢 Wake word terdeteksi, masuk ke mode perintah...\n");
|
|
wake_word_active = true;
|
|
skip_next_command_window = true;
|
|
skip_blink_active = true;
|
|
perintah_start_time = millis();
|
|
}
|
|
|
|
// === Mode Perintah
|
|
else if (wake_word_active) {
|
|
unsigned long elapsed = millis() - perintah_start_time;
|
|
|
|
// LED perintah: berkedip saat skip, nyala tetap saat mode perintah aktif
|
|
if (skip_next_command_window) {
|
|
if (millis() - ledper_last_blink >= ledper_blink_interval) {
|
|
ledper_last_blink = millis();
|
|
ledper_state = !ledper_state;
|
|
digitalWrite(PIN_LEDPER, ledper_state);
|
|
}
|
|
|
|
// Pastikan hanya 1 window dilewati (durasi window)
|
|
if (elapsed >= window_duration_ms) {
|
|
skip_next_command_window = false;
|
|
skip_blink_active = false;
|
|
ei_printf("✅ Mode perintah aktif, siap menerima perintah\n");
|
|
digitalWrite(PIN_LEDPER, HIGH); // nyala tetap setelah skip
|
|
}
|
|
} else {
|
|
digitalWrite(PIN_LEDPER, HIGH);
|
|
}
|
|
|
|
if (elapsed >= command_window_ms) {
|
|
ei_printf("⏱ Timeout! Kembali ke mode wake word\n");
|
|
wake_word_active = false;
|
|
skip_next_command_window = false;
|
|
digitalWrite(PIN_LEDPER, LOW);
|
|
}
|
|
|
|
else if (!skip_next_command_window) {
|
|
if (strcmp(top_label, "silent") != 0 &&
|
|
strcmp(top_label, "unknown") != 0 &&
|
|
strcmp(top_label, "wake_word") != 0 &&
|
|
top_score > 0.88f) {
|
|
|
|
ei_printf("🗣 Perintah terdeteksi: %s (%.2f)\n", top_label, top_score);
|
|
|
|
if (strcmp(top_label, "nyalakan_lampu_kamar") == 0) {
|
|
digitalWrite(PIN_LAMPU_KAMAR, HIGH);
|
|
publishMQTT("cristine/control/lampu_kamar", "on");
|
|
publishStatus("lampu_kamar", true);
|
|
} else if (strcmp(top_label, "matikan_lampu_kamar") == 0) {
|
|
digitalWrite(PIN_LAMPU_KAMAR, LOW);
|
|
publishMQTT("cristine/control/lampu_kamar", "off");
|
|
publishStatus("lampu_kamar", false);
|
|
} else if (strcmp(top_label, "nyalakan_lampu_teras") == 0) {
|
|
digitalWrite(PIN_LAMPU_TERAS, HIGH);
|
|
publishMQTT("cristine/control/lampu_teras", "on");
|
|
publishStatus("lampu_teras", true);
|
|
} else if (strcmp(top_label, "matikan_lampu_teras") == 0) {
|
|
digitalWrite(PIN_LAMPU_TERAS, LOW);
|
|
publishMQTT("cristine/control/lampu_teras", "off");
|
|
publishStatus("lampu_teras", false);
|
|
} else if (strcmp(top_label, "nyalakan_kipas") == 0) {
|
|
digitalWrite(PIN_KIPAS, HIGH);
|
|
publishMQTT("cristine/control/kipas", "on");
|
|
publishStatus("kipas", true);
|
|
} else if (strcmp(top_label, "matikan_kipas") == 0) {
|
|
digitalWrite(PIN_KIPAS, LOW);
|
|
publishMQTT("cristine/control/kipas", "off");
|
|
publishStatus("kipas", false);
|
|
} else if (strcmp(top_label, "buka_pintu") == 0) {
|
|
digitalWrite(PIN_SOLENOID, HIGH);
|
|
publishMQTT("cristine/control/pintu", "on");
|
|
publishStatus("pintu", true);
|
|
voiceAutoLockStart = millis();
|
|
voiceAutoLockInProgress = true;
|
|
} else {
|
|
ei_printf("❓ Perintah '%s' tidak dikenali\n", top_label);
|
|
}
|
|
|
|
wake_word_active = false;
|
|
digitalWrite(PIN_LEDPER, LOW);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|