IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Voice Assistant offline tiếng Việt với Raspberry Pi – Bài 5: Gửi lệnh qua MQTT để điều khiển hệ thống IoT

Ở bài 23.4, chúng ta đã điều khiển được đèn, quạt và relay ngay trên Raspberry Pi thông qua GPIO. Đó là một bước rất quan trọng vì hệ thống đã có thể:

  • nghe giọng nói
  • nhận diện lệnh
  • map sang command
  • thực thi hành động thật

Tuy nhiên, trong một hệ thống IoT thực tế, Raspberry Pi không phải lúc nào cũng là nơi trực tiếp bật thiết bị. Nhiều trường hợp, Raspberry Pi chỉ đóng vai trò như bộ điều phối lệnh bằng giọng nói, sau đó gửi lệnh sang:

  • ESP32
  • gateway khác
  • dashboard
  • hệ thống automation
  • thiết bị ở phòng khác

Đây chính là lúc MQTT trở thành mảnh ghép rất phù hợp.

Trong bài này, chúng ta sẽ nâng cấp Voice Assistant offline tiếng Việt trên Raspberry Pi từ mô hình điều khiển local sang mô hình gửi lệnh qua MQTT để điều khiển hệ thống IoT.

Bạn sẽ làm được gì sau bài này

Sau khi hoàn thành bài này, bạn sẽ có thể:

  • nhận diện lệnh giọng nói tiếng Việt bằng Vosk
  • chuyển câu lệnh thành command nội bộ
  • publish command đó lên MQTT broker
  • để ESP32 hoặc thiết bị IoT khác subscribe và thực thi

Nói ngắn gọn:

Bạn nói trên Raspberry Pi, nhưng thiết bị ở nơi khác vẫn có thể hành động.

Vì sao nên gửi lệnh qua MQTT thay vì chỉ điều khiển local

Điều khiển GPIO trực tiếp rất tốt cho demo, nhưng nếu muốn đi xa hơn, bạn sẽ sớm gặp các nhu cầu như:

  • đèn và quạt nằm trên ESP32 chứ không nằm trên Pi
  • một câu lệnh phải điều khiển nhiều thiết bị
  • cần lưu log command
  • cần hiển thị trạng thái lên dashboard
  • cần mở rộng sang smart home, farm, lab hoặc gateway nhiều node

Nếu mọi thứ chỉ chạy local trong một file Python thì hệ thống sẽ nhanh chóng khó mở rộng.

MQTT giúp tách hệ thống thành các phần rõ ràng

Ví dụ:

  • Raspberry Pi lo phần nghe và hiểu lệnh
  • ESP32 lo phần điều khiển relay
  • dashboard lo phần hiển thị trạng thái
  • backend lo phần ghi log / automation

Khi đó, Raspberry Pi chỉ cần publish một message như:

{
  "command": "LIGHT_ON"
}

Bên nào quan tâm thì subscribe topic tương ứng và xử lý.

Đây là tư duy rất hợp với IoT: tách nhận lệnh, truyền lệnh và thực thi lệnh thành các thành phần riêng.

Kiến trúc tổng thể

Luồng xử lý của hệ thống trong bài này sẽ là:

Micro -> Vosk -> Text -> Command Parser -> MQTT Publish -> ESP32 / Device / Dashboard

Ví dụ thực tế:

“bật đèn phòng khách” -> LIGHT_ON -> publish MQTT -> ESP32 nhận lệnh -> relay bật

Nếu nhìn theo vai trò từng thành phần:

  • Raspberry Pi: Voice Assistant offline
  • MQTT Broker: trung gian truyền lệnh
  • ESP32 / thiết bị IoT: nơi thực thi
  • Dashboard: nơi theo dõi command hoặc trạng thái

Chuẩn bị trước khi làm

Bạn nên có sẵn:

  • Raspberry Pi đã cài micro và test audio input
  • project Vosk từ bài 23.3
  • command parser từ bài 23.4
  • một MQTT broker để test
  • một MQTT client khác để quan sát message

Có thể dùng broker nào

Bạn có thể dùng:

  • Mosquitto cài local trên Raspberry Pi
  • broker trong mạng LAN
  • broker cloud
  • hoặc broker MQTT riêng của bạn

Nếu chỉ muốn test nhanh, bạn có thể dùng một broker local để nhìn rõ luồng command.

Thiết kế topic MQTT cho voice command

Đây là phần rất quan trọng.

Nếu topic thiết kế quá sơ sài, sau này rất khó mở rộng. Vì vậy, ngay từ đầu ta nên đặt topic đủ rõ nghĩa.

Cách đơn giản để demo

home/voice/command

Message ví dụ:

{
  "command": "LIGHT_ON"
}

Cách tốt hơn để dễ mở rộng

iotlabs/home/voice/commands

Hoặc nếu muốn điều khiển theo khu vực:

iotlabs/home/livingroom/commands
iotlabs/home/bedroom/commands
iotlabs/farm/gateway01/commands

Nếu muốn gửi rõ hơn theo thiết bị

iotlabs/home/light01/set
iotlabs/home/fan01/set

Trong bài này, để dễ hiểu, chúng ta sẽ dùng topic trung gian:

iotlabs/voice/commands

Thiết kế payload command

Payload không nên chỉ là một chuỗi text đơn giản, vì sau này bạn sẽ muốn thêm:

  • timestamp
  • nguồn gửi
  • target device
  • trạng thái
  • id của command

Payload tối thiểu

{
  "command": "LIGHT_ON"
}

Payload nên dùng hơn

{
  "source": "rpi-voice-assistant",
  "command": "LIGHT_ON",
  "target": "light01",
  "ts": 1712800000
}

Ví dụ với quạt

{
  "source": "rpi-voice-assistant",
  "command": "FAN_OFF",
  "target": "fan01",
  "ts": 1712800050
}

Thiết kế kiểu này giúp hệ thống có thể debug và mở rộng dễ hơn nhiều.

Cài thư viện MQTT cho Python

Chúng ta sẽ dùng thư viện Paho MQTT.

Cài bằng lệnh:

pip install paho-mqtt

Tạo file publish command MQTT

Tạo file:

mqtt_publisher.py

Code mẫu publish MQTT

import json
import time
import paho.mqtt.client as mqtt

BROKER_HOST = "127.0.0.1"
BROKER_PORT = 1883
TOPIC = "iotlabs/voice/commands"

client = mqtt.Client()

def connect_mqtt():
    client.connect(BROKER_HOST, BROKER_PORT, 60)

def publish_command(command, target="device01"):
    payload = {
        "source": "rpi-voice-assistant",
        "command": command,
        "target": target,
        "ts": int(time.time())
    }

    client.publish(TOPIC, json.dumps(payload))
    print("Published:", payload)

if __name__ == "__main__":
    connect_mqtt()
    publish_command("LIGHT_ON", "light01")

Test publish trước khi tích hợp voice

Chạy thử:

python mqtt_publisher.py

Nếu broker đang hoạt động, bạn sẽ thấy log:

Published: {'source': 'rpi-voice-assistant', 'command': 'LIGHT_ON', 'target': 'light01', 'ts': 1712800000}

Dùng MQTT subscriber để kiểm tra

Ở terminal khác, subscribe thử topic:

mosquitto_sub -h 127.0.0.1 -t iotlabs/voice/commands

Kết quả mong đợi:

{"source": "rpi-voice-assistant", "command": "LIGHT_ON", "target": "light01", "ts": 1712800000}

Nếu đã thấy message này, nghĩa là phần publish MQTT của bạn đã ổn.

Tích hợp MQTT vào Voice Assistant

Giờ chúng ta sẽ kết nối phần nhận diện lệnh với phần publish MQTT.

Ở bài 23.4, bạn đã có logic kiểu như:

def parse_command(text):
    if "bat den" in text:
        return "LIGHT_ON"
    elif "tat den" in text:
        return "LIGHT_OFF"
    elif "bat quat" in text:
        return "FAN_ON"
    elif "tat quat" in text:
        return "FAN_OFF"
    return None

Bây giờ, thay vì gọi GPIO local ngay lập tức, ta sẽ publish command lên MQTT.

Hàm publish command

import json
import time
import paho.mqtt.client as mqtt

BROKER_HOST = "127.0.0.1"
BROKER_PORT = 1883
TOPIC = "iotlabs/voice/commands"

mqtt_client = mqtt.Client()
mqtt_client.connect(BROKER_HOST, BROKER_PORT, 60)

def send_command_mqtt(command, target):
    payload = {
        "source": "rpi-voice-assistant",
        "command": command,
        "target": target,
        "ts": int(time.time())
    }

    mqtt_client.publish(TOPIC, json.dumps(payload))
    print("MQTT sent:", payload)

Map command sang target device

Ví dụ bạn muốn:

  • LIGHT_ON gửi đến light01
  • FAN_ON gửi đến fan01

Ta có thể viết:

def resolve_target(command):
    if command in ["LIGHT_ON", "LIGHT_OFF"]:
        return "light01"
    elif command in ["FAN_ON", "FAN_OFF"]:
        return "fan01"
    return "unknown"

Tích hợp vào loop chính

Trong luồng nhận diện Vosk:

while True:
    data = q.get()

    if recognizer.AcceptWaveform(data):
        result = json.loads(recognizer.Result())
        text = normalize(result.get("text", ""))

        print("Bạn nói:", text)

        command = parse_command(text)

        if command:
            target = resolve_target(command)
            send_command_mqtt(command, target)

Demo kết quả

Khi bạn nói:

bật đèn

Terminal trên Raspberry Pi:

Bạn nói: bat den
MQTT sent: {'source': 'rpi-voice-assistant', 'command': 'LIGHT_ON', 'target': 'light01', 'ts': 1712800000}

Terminal subscribe:

{"source": "rpi-voice-assistant", "command": "LIGHT_ON", "target": "light01", "ts": 1712800000}

Từ đây, bất kỳ ESP32 hay service nào subscribe topic phù hợp đều có thể nhận lệnh và xử lý.

Viết một subscriber Python đơn giản để mô phỏng thiết bị nhận lệnh

Để test nhanh mà chưa cần ESP32, bạn có thể viết một subscriber Python.

Tạo file:

device_simulator.py

Code mẫu

import json
import paho.mqtt.client as mqtt

BROKER_HOST = "127.0.0.1"
BROKER_PORT = 1883
TOPIC = "iotlabs/voice/commands"

def on_connect(client, userdata, flags, rc):
    print("Connected to broker")
    client.subscribe(TOPIC)

def on_message(client, userdata, msg):
    payload = json.loads(msg.payload.decode())
    print("Received:", payload)

    command = payload.get("command")
    target = payload.get("target")

    if target == "light01":
        if command == "LIGHT_ON":
            print("Simulate: Light ON")
        elif command == "LIGHT_OFF":
            print("Simulate: Light OFF")

    elif target == "fan01":
        if command == "FAN_ON":
            print("Simulate: Fan ON")
        elif command == "FAN_OFF":
            print("Simulate: Fan OFF")

client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message

client.connect(BROKER_HOST, BROKER_PORT, 60)
client.loop_forever()

Kết quả mô phỏng

Khi Raspberry Pi gửi lệnh:

bật quạt

Subscriber sẽ nhận được:

Received: {'source': 'rpi-voice-assistant', 'command': 'FAN_ON', 'target': 'fan01', 'ts': 1712800200}
Simulate: Fan ON

Đây chính là nguyên lý cơ bản của một hệ thống IoT command-driven.

Cách mở rộng để điều khiển ESP32 thật

Nếu muốn điều khiển ESP32 thật, bạn chỉ cần để ESP32 subscribe đúng topic và parse payload JSON.

Ví dụ logic trên ESP32 sẽ là:

  • subscribe topic iotlabs/voice/commands
  • parse field target
  • nếu target đúng với device của nó thì thực thi relay
  • nếu không đúng thì bỏ qua

Ví dụ:

  • ESP32 ở phòng khách nhận light01
  • ESP32 ở bàn làm việc nhận fan01

Nhờ vậy, một Raspberry Pi duy nhất có thể gửi lệnh đến nhiều node IoT khác nhau.

Nên dùng 1 topic chung hay nhiều topic riêng

Có hai cách phổ biến.

Cách 1: Một topic chung

iotlabs/voice/commands

Ưu điểm:

  • đơn giản
  • dễ test
  • dễ bắt đầu

Nhược điểm:

  • thiết bị nào cũng phải đọc rồi lọc target

Cách 2: Topic riêng theo thiết bị

iotlabs/devices/light01/set
iotlabs/devices/fan01/set

Ưu điểm:

  • rõ ràng
  • thiết bị subscribe đúng topic của nó
  • mở rộng sạch hơn

Nhược điểm:

  • bên publish phải map topic kỹ hơn

Gợi ý cho giai đoạn học

  • demo nhanh: dùng 1 topic chung
  • hệ thống lớn hơn: chuyển sang topic riêng theo thiết bị

Thêm phản hồi trạng thái để hệ thống hoàn chỉnh hơn

Một điểm rất hay là sau khi nhận command, thiết bị có thể publish trạng thái ngược lại.

Ví dụ:

  • Raspberry Pi gửi: LIGHT_ON
  • ESP32 bật relay xong
  • ESP32 publish lên topic trạng thái:
iotlabs/devices/light01/status

Payload:

{
  "device": "light01",
  "status": "on",
  "ts": 1712800300
}

Nhờ đó:

  • dashboard biết đèn đang bật
  • backend có thể log lại
  • Voice Assistant có thể đọc phản hồi sau này

Các lỗi thường gặp

Không publish được MQTT

Nguyên nhân:

  • broker chưa chạy
  • sai host / port
  • firewall chặn
  • client chưa connect

Cách xử lý:

  • test broker trước bằng mosquitto_sub
  • kiểm tra host, port
  • in log khi connect

Publish thành công nhưng thiết bị không nhận

Nguyên nhân:

  • subscribe sai topic
  • payload không đúng format
  • target không khớp

Cách xử lý:

  • in payload raw ở bên subscriber
  • xác nhận topic hoàn toàn giống nhau
  • kiểm tra lại field target

Voice nhận đúng nhưng không có command

Nguyên nhân:

  • parse command chưa đủ các biến thể
  • text Vosk trả về khác mong đợi

Cách xử lý:

  • in text ra terminal
  • bổ sung mapping như mo den, cho den sang, bat quat

Thiết bị nhận được command nhưng không đổi trạng thái

Nguyên nhân:

  • logic relay sai
  • GPIO đảo mức HIGH/LOW
  • code subscriber chưa thực thi đúng

Cách xử lý:

  • test relay local trước
  • log từng bước xử lý command
  • xác minh target đúng với device

Cách tổ chức project cho sạch hơn

Khi tới bài này, project đã bắt đầu có nhiều phần. Bạn nên tách code ra:

voice_assistant/
├── main.py
├── voice/
│   ├── recognizer.py
│   └── audio_input.py
├── commands/
│   ├── parser.py
│   └── mapper.py
├── mqtt/
│   └── publisher.py
└── devices/
    └── gpio_controller.py

Cách này giúp sau này bạn dễ:

  • mở rộng thêm command
  • đổi broker
  • thay từ GPIO local sang MQTT
  • kết nối dashboard hoặc backend

Hoàn thiện mini series Voice Assistant offline tiếng Việt với Raspberry Pi.

Đến đây, bạn đã có một hệ thống khá hoàn chỉnh:

Nghĩa là bây giờ hệ thống của bạn đã đi được trọn một vòng:

Nghe giọng nói -> hiểu lệnh -> phát lệnh -> điều khiển thiết bị IoT


Kết luận

Bài này là bước nâng cấp rất quan trọng vì nó biến Voice Assistant trên Raspberry Pi từ một demo điều khiển local thành một trung tâm điều phối lệnh giọng nói cho hệ thống IoT.

Thay vì chỉ bật đèn gắn trực tiếp vào Raspberry Pi, giờ đây bạn có thể:

  • điều khiển ESP32 ở xa
  • gửi lệnh đến nhiều thiết bị
  • tích hợp dashboard
  • ghi log command
  • xây dựng smart home hoặc lab mini theo hướng mở rộng thật sự

Đây cũng là điểm mà MQTT phát huy thế mạnh rõ ràng nhất: tách rời phần nhận lệnh và phần thực thi, giúp hệ thống vừa gọn, vừa linh hoạt, vừa đúng tư duy IoT.