first commit

This commit is contained in:
cristine 2025-08-19 07:48:14 +07:00
commit 95cc051f4d
5 changed files with 657 additions and 0 deletions

445
Arduino/Arduino.ino Normal file
View File

@ -0,0 +1,445 @@
#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 = &microphone_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);
}
}
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

212
index.js Normal file
View File

@ -0,0 +1,212 @@
import pkg, { DisconnectReason } from '@whiskeysockets/baileys';
import pino from 'pino';
import mqtt from 'mqtt';
import fs from 'fs';
const {
makeWASocket,
useMultiFileAuthState,
fetchLatestBaileysVersion,
} = pkg;
const allowedNumbers = [
'62882009051780@s.whatsapp.net',
'6287862977046@s.whatsapp.net',
'6289521851079@s.whatsapp.net',
];
// Status lokal perabotan (backup jika diperlukan)
const status = {
'lampu kamar': false,
'lampu teras': false,
'kipas': false,
'pintu': false,
};
// Penyimpan siapa yang meminta status
const pendingStatusRequests = {};
// Fungsi konversi status jadi teks
function getStatusString(statusObj) {
return Object.entries(statusObj)
.map(([device, value]) => {
if (device === 'pintu') {
return `- ${device} : ${value === 'dibuka' ? '🔓 Dibuka' : '🔒 Dikunci'}`;
} else {
return `- ${device} : ${value === true || value === 'true' ? '✅ Menyala' : '❌ Mati'}`;
}
})
.join('\n');
// MQTT setup
const mqttClient = mqtt.connect('mqtt://test.mosquitto.org');
mqttClient.on('connect', () => {
console.log('📡 Terhubung ke MQTT broker: test.mosquitto.org');
});
mqttClient.on('error', (err) => {
console.error('❌ MQTT Error:', err);
});
// Tangani balasan dari ESP32 ke topik status
mqttClient.on('message', async (topic, message) => {
if (topic === 'cristine/status') {
try {
const parsedStatus = JSON.parse(message.toString());
const now = Date.now();
for (const [jid, time] of Object.entries(pendingStatusRequests)) {
if (now - time <= 10000) { // valid 10 detik
await sock.sendMessage(jid, {
text: `📊 Status Real-time:\n${getStatusString(parsedStatus)}`
});
}
}
// Kosongkan request setelah dibalas
for (const jid of Object.keys(pendingStatusRequests)) {
delete pendingStatusRequests[jid];
}
} catch (e) {
console.error('❌ Gagal parsing status dari ESP32:', e);
}
}
});
mqttClient.subscribe('cristine/status');
// Fungsi utama bot
let sock;
async function startBot() {
const { version } = await fetchLatestBaileysVersion();
const { state, saveCreds } = await useMultiFileAuthState('./auth_info_baileys');
sock = makeWASocket({
version,
auth: state,
printQRInTerminal: true,
logger: pino({ level: 'silent' }),
});
sock.ev.on('creds.update', saveCreds);
sock.ev.on('connection.update', async ({ connection, lastDisconnect }) => {
if (connection === 'close') {
const reasonCode = lastDisconnect?.error?.output?.statusCode;
const isLoggedOut = reasonCode === DisconnectReason.loggedOut;
console.log('⚠️ Koneksi terputus:', reasonCode);
if (isLoggedOut) {
fs.rmSync('./auth_info_baileys', { recursive: true, force: true });
console.log('🔁 Silakan restart untuk login ulang.');
} else {
startBot();
}
}
if (connection === 'open') {
console.log('✅ Bot WhatsApp berhasil online!');
}
});
sock.ev.on('messages.upsert', async ({ messages }) => {
const msg = messages[0];
if (!msg.message || msg.key.fromMe) return;
const sender = msg.key.remoteJid;
if (!allowedNumbers.includes(sender)) return;
const text =
msg.message.conversation ||
msg.message?.extendedTextMessage?.text ||
msg.message?.imageMessage?.caption ||
msg.message?.buttonsResponseMessage?.selectedButtonId ||
msg.message?.listResponseMessage?.singleSelectReply?.selectedRowId;
if (!text) return;
const command = text.trim().toLowerCase();
console.log(`📩 Pesan dari ${sender}:`, command);
switch (command) {
case 'menu':
await sock.sendMessage(sender, {
text: '📋 Menu Kontrol:\n' +
'- nyalakan lampu kamar\n' +
'- matikan lampu kamar\n' +
'- nyalakan lampu teras\n' +
'- matikan lampu teras\n' +
'- nyalakan kipas\n' +
'- matikan kipas\n' +
'- buka pintu\n' +
'- kunci pintu\n' +
'- status',
});
break;
case 'nyalakan lampu kamar':
status['lampu kamar'] = true;
mqttClient.publish('cristine/control/lampu_kamar', 'ON');
await sock.sendMessage(sender, { text: '💡 Lampu kamar *dinyalakan*.' });
break;
case 'matikan lampu kamar':
status['lampu kamar'] = false;
mqttClient.publish('cristine/control/lampu_kamar', 'OFF');
await sock.sendMessage(sender, { text: '💡 Lampu kamar *dimatikan*.' });
break;
case 'nyalakan lampu teras':
status['lampu teras'] = true;
mqttClient.publish('cristine/control/lampu_teras', 'ON');
await sock.sendMessage(sender, { text: '💡 Lampu teras *dinyalakan*.' });
break;
case 'matikan lampu teras':
status['lampu teras'] = false;
mqttClient.publish('cristine/control/lampu_teras', 'OFF');
await sock.sendMessage(sender, { text: '💡 Lampu teras *dimatikan*.' });
break;
case 'nyalakan kipas':
status['kipas'] = true;
mqttClient.publish('cristine/control/kipas', 'ON');
await sock.sendMessage(sender, { text: '🌀 Kipas *dinyalakan*.' });
break;
case 'matikan kipas':
status['kipas'] = false;
mqttClient.publish('cristine/control/kipas', 'OFF');
await sock.sendMessage(sender, { text: '🌀 Kipas *dimatikan*.' });
break;
case 'buka pintu':
status['pintu'] = true;
mqttClient.publish('cristine/control/pintu', 'ON');
await sock.sendMessage(sender, { text: '🚪 Pintu *dibuka*.' });
break;
case 'kunci pintu':
status['pintu'] = false;
mqttClient.publish('cristine/control/pintu', 'OFF');
await sock.sendMessage(sender, { text: '🚪 Pintu *dikunci*.' });
break;
case 'status':
pendingStatusRequests[sender] = Date.now();
mqttClient.publish('cristine/request/status', 'REQUEST');
await sock.sendMessage(sender, { text: '⏳ Meminta status dari perangkat, mohon tunggu...' });
break;
default:
await sock.sendMessage(sender, {
text: `⚠️ Perintah tidak dikenali.\nKetik *menu* untuk melihat daftar perintah.`
});
break;
}
});
}
}
startBot();