# Robot trợ lý học tập dùng Raspberry Pi Zero 2 W

Tài liệu ghi lại hiện trạng sản phẩm ngày 29/07/2026 để học sinh Trường THCS Nguyễn Tất Thành có thể bảo trì, thuyết trình và tiếp tục phát triển trong cuộc thi sáng tạo thanh thiếu niên. Không ghi API key thật, mật khẩu Wi-Fi thật hoặc dữ liệu riêng tư vào bản nộp công khai.

## 1. Mục tiêu dự án

Dự án xây dựng robot học tập nhỏ, chạy độc lập không cần màn hình HDMI, bàn phím hoặc chuột. Người dùng có thể gọi “robot ơi”, bấm giữ nút để nói hoặc gửi câu hỏi từ web. Robot tự chụp ảnh trang vở, sách, đề bài hoặc hình vẽ khi cần, gửi nội dung lên AI, đọc câu trả lời bằng tiếng Việt, hiển thị ý chính và lưu hội thoại để hỏi tiếp.

Phiên bản đầu chỉ dùng OLED 1.3 inch SH1106 128×64. Màn hình này rẻ, ít tốn điện và đủ để vận hành robot, nhưng diện tích nhỏ nên khó trình bày nội dung khi demo. Phiên bản hiện tại bổ sung TFT 4 inch ST7796S 480×320 làm màn hình chính. Phần mềm vẫn cho phép chọn lại chế độ chỉ dùng OLED; TFT không phải linh kiện bắt buộc.

Luồng thử nghiệm hiện tại:

```text
Người dùng nói “robot ơi” hoặc giữ nút để nói
  -> Raspberry Pi ghi âm bằng mic USB soundcard
  -> STT chuyển giọng nói thành văn bản
  -> daemon nhận câu hỏi và lịch sử hội thoại
  -> nếu câu hỏi cần nhìn/chụp, Pi tự chụp ảnh bằng camera CSI
  -> gửi text hoặc text + ảnh lên AI
  -> nhận JSON gồm answer, display, image_description
  -> lưu câu hỏi, câu trả lời và mô tả ảnh vào hội thoại
  -> TFT hoặc OLED hiển thị trạng thái/tóm tắt
  -> TTS đọc câu trả lời qua loa
```

Hai cấu hình màn hình:

```text
Chỉ OLED
  -> OLED là màn hình chính
  -> giữ đầy đủ trạng thái, tóm tắt, công thức và link cấu hình

TFT + OLED
  -> TFT 4 inch là màn hình chính nhiều màu
  -> OLED nhỏ hiển thị mắt biểu cảm và đồng hồ/thời tiết
```

## 2. Hiện trạng đã làm

- Raspberry Pi Zero 2 W boot headless qua Wi-Fi/SSH.
- Hostname đặt là `robotpi`.
- Đang dùng thẻ nhớ 64 GB.
- Camera CSI được nhận là sensor `ov5647`, chụp được ảnh.
- TFT 4 inch ST7796S 480×320 qua SPI hoạt động như màn hình chính, có giao diện màu và hoạt ảnh khuôn mặt robot.
- OLED SH1106 I2C 128×64 hoạt động đồng thời như màn hình phụ hiển thị mắt biểu cảm và đồng hồ.
- Web admin cho chọn TFT 4 inch hoặc OLED 1.3 inch làm cấu hình chính; khi chọn OLED, robot hoạt động như phiên bản ban đầu và không phụ thuộc TFT.
- Mắt trên TFT và OLED dùng chung nhịp chớp/liếc khi robot chờ.
- OLED phụ luân phiên mắt 20 giây và đồng hồ 10 giây; đồng hồ có giờ, thứ, ngày tháng, ngày âm lịch, nhiệt độ, độ ẩm, khả năng mưa và biểu tượng thời tiết.
- TFT có các màn hình sẵn sàng, đang nghe, đang chụp, đang suy nghĩ, phát nhạc, câu trả lời, thông tin và lỗi. Footer hiển thị `© THCS Nguyễn Tất Thành` và `Đông Gia Nghĩa - Lâm Đồng`, trừ màn hình ngủ.
- AI text và AI vision hoạt động qua endpoint OpenAI-compatible.
- Model trả JSON để phần mềm tách câu trả lời đầy đủ, dòng ngắn cho màn hình và mô tả ảnh.
- Có bộ nhớ hội thoại, lưu các lượt gần đây vào `logs/conversation.json`.
- Với ảnh sách/vở/tài liệu, prompt yêu cầu AI trích xuất toàn bộ chữ, công thức, bảng, hình vẽ đọc được vào `image_description`.
- Mô tả ảnh được lưu cùng message người dùng và gửi lại trong các request sau; khi message cũ bị cắt theo giới hạn lịch sử thì mô tả ảnh đi cùng cũng bị xóa.
- Trang hỏi đáp web hiển thị cả lịch sử user/assistant và mô tả ảnh nếu lượt đó có ảnh.
- Mic mặc định luôn dùng USB soundcard để ổn định; web không bắt người dùng chọn lại input.
- Loa Bluetooth vừa kết nối được tự chọn làm output. Robot tự ngắt loa Bluetooth cũ, bảo đảm mỗi thời điểm chỉ dùng một loa Bluetooth.
- Khi loa Bluetooth bị ngắt hoặc quên, output tự trở về loa gắn qua USB soundcard. Robot phát câu “Đã thay đổi thiết bị âm thanh” qua output mới.
- Web admin có nút kết nối, ngắt kết nối và quên từng loa Bluetooth.
- Đã loại bỏ hướng Chromium/Web Speech vì quá nặng với Pi Zero 2 W và không có voice tiếng Việt dùng được trên Pi.
- Wake phrase “robot ơi” được phát hiện offline. Chỉ sau khi nhận đúng wake phrase, robot mới lấy phần lời nói tiếp theo, chờ người dùng ngừng nói rồi gửi đoạn ghi âm lên STT.
- Khi wake phrase kích hoạt, màn hình hiện trạng thái “Đang nghe” giống luồng bấm giữ nút.
- Wake listener dừng trong chế độ ngủ để tránh robot tự kích hoạt; nút vật lý dùng để đánh thức.
- Web UI quản lý hỏi đáp, lịch sử, camera, Wi-Fi, Bluetooth, màn hình, AI, TTS, STT, wake phrase, nút bấm và âm trạng thái.
- Web UI đã có tab Nguồn AI để cấu hình AI1-AI4, fallback theo tác vụ, prompt hệ thống, luật camera và audio trạng thái.
- Nếu Pi boot mà không vào được Wi-Fi sau thời gian chờ, daemon tự bật access point `robotpi-setup` và hiển thị link setup.
- Trạng thái kiểm tra gần nhất: robot online tại `192.168.1.130`, daemon `ready`, TFT chính và OLED phụ cùng hoạt động.
- Robot trả lời cục bộ được các câu hỏi về bản thân, khả năng hỗ trợ, địa chỉ IP, port và link cấu hình hiện tại.
- Lệnh “mở nhạc...”, “mở bài hát...”, “mở một bài nhạc...” hoặc “mở một bài hát...” được nhận diện cục bộ, không gọi AI để hiểu lệnh. `yt-dlp` và `mpv` tìm/phát bài qua output hiện tại.
- Chức năng dance và chân GPIO động cơ dance đã được loại bỏ hoàn toàn.
- Các chức năng cần AI/TTS/tìm nhạc chỉ chạy khi `internet_connected=true`; nếu chưa có Internet, robot giữ màn hình cấu hình mạng và không gọi API.
- Sau thời gian không hoạt động, robot dừng wake listener/audio/nhạc nhưng vẫn giữ web admin truy cập được. Ở cấu hình TFT + OLED, TFT chuyển đen hoàn toàn và OLED hiển thị đồng hồ; bấm nút để đánh thức.
- TTS hỗ trợ nhiều provider và fallback; MP3 ưu tiên phát bằng `mpg123`, `mpv` dùng làm fallback hoặc phát nhạc Internet.
- Các câu thông báo trạng thái được tạo sẵn bằng TTS API và cache trong hệ thống để phát nhanh khi boot/kết nối Wi-Fi/sẵn sàng/vào ngủ/lỗi.
- Có chế độ hướng dẫn bài tập từng bước: AI trả danh sách bước, robot đọc/hiện từng bước và chờ người dùng bấm/chạm để sang bước tiếp theo. Trong khi đang phát bước hiện tại, robot tạo sẵn audio các bước sau theo thứ tự để giảm thời gian chờ.
- Trước khi chụp, robot phát câu TTS cache “Tôi đang chụp ảnh...” đồng thời chuẩn bị camera; sau khi chụp có âm shutter, khi bắt đầu suy nghĩ có âm báo local ngắn.
- Ảnh camera mặc định được xóa sau khi AI xử lý xong để không làm đầy thẻ nhớ.
- Text qua `submit_robot.sh` đã dùng chung luật tự chụp ảnh của daemon.

## 3. Phần cứng

| Hạng mục | Thông tin |
| --- | --- |
| Máy tính nhúng | Raspberry Pi Zero 2 W |
| Thẻ nhớ | 64 GB |
| Hệ điều hành | Raspberry Pi OS Lite / Debian GNU/Linux 13 trixie, 64-bit |
| Camera | Camera CSI sensor `ov5647`, tối đa 2592 x 1944 |
| Màn hình chính hiện tại | TFT 4 inch SPI 480×320, driver ST7796S, không dùng cảm ứng |
| Màn hình phụ / cấu hình tiết kiệm | OLED 1.3 inch I2C 128×64, driver SH1106 |
| Loa | Loa gắn qua USB soundcard; hỗ trợ loa Bluetooth A2DP và tự chuyển output |
| Micro / sound card | USB soundcard dùng cố định cho input; thiết bị mặc định `plughw:CARD=Device,DEV=0` |
| Nút bấm điều khiển | 1 nút nhấn thường, GPIO27 physical pin 13, kéo xuống GND khi bấm |
| Nguồn | Cấp nguồn qua cổng micro-USB nguồn của Pi |

Lưu ý Wi-Fi: Raspberry Pi Zero 2 W chỉ hỗ trợ Wi-Fi 2.4 GHz. Pi không vào được mạng Wi-Fi 5 GHz riêng; nếu router phát Wi-Fi 6 ở băng 2.4 GHz và có tương thích ngược thì có thể dùng, nhưng nên cấu hình SSID 2.4 GHz riêng cho Pi.

## 4. Hệ thống màn hình và đấu dây

### 4.1. OLED 1.3 inch SH1106

OLED dùng giao tiếp I2C 128×64. Màn hình có thể làm màn hình chính độc lập hoặc làm mắt/đồng hồ phụ khi TFT được chọn.

Chân trên module OLED:

- `VDD`
- `GND`
- `SCK`
- `SDA`

Đấu với Raspberry Pi Zero 2 W:

| OLED | Raspberry Pi | Chức năng |
| --- | --- | --- |
| `VDD` | 3V3, physical pin 1 hoặc pin 17 | Nguồn 3.3 V |
| `GND` | GND, physical pin 6/9/14/... | Mass |
| `SCK` | GPIO3/SCL, physical pin 5 | Clock I2C |
| `SDA` | GPIO2/SDA, physical pin 3 | Data I2C |

Ghi chú:

- Một số module ghi `SCK`, một số module ghi `SCL`; với I2C hai tên này là chân clock.
- Pi dùng mức logic 3.3 V, nên ưu tiên cấp `VDD` bằng 3.3 V.
- Code dùng bus `/dev/i2c-1`, địa chỉ mặc định `0x3C`.
- Module điều khiển OLED nằm ở `robot/oled.py`.
- Khi là màn hình chính, OLED có các màn hình sẵn sàng, đang nghe, đang chụp, đang suy nghĩ, phát nhạc, lỗi, câu trả lời và setup.
- Khi là màn hình phụ, OLED hiển thị mắt theo trạng thái robot và đồng hồ/thời tiết.

### 4.2. TFT 4 inch ST7796S

TFT dùng SPI0 để hiển thị. Phần cảm ứng điện trở và khe SD trên module không được dùng trong sản phẩm hiện tại.

| TFT | Raspberry Pi Zero 2 W | Chức năng |
| --- | --- | --- |
| `VCC` | 5V, physical pin 2 hoặc 4 | Nguồn module |
| `GND` | GND | Mass |
| `CS` | GPIO8/CE0, physical pin 24 | Chọn màn hình SPI |
| `RESET` | GPIO24, physical pin 18 | Reset TFT |
| `DC/RS` | GPIO23, physical pin 16 | Chọn lệnh/dữ liệu |
| `SDI (MOSI)` | GPIO10/MOSI, physical pin 19 | Dữ liệu từ Pi sang TFT |
| `SCK` | GPIO11/SCLK, physical pin 23 | Clock SPI |
| `LED` | Nguồn backlight phù hợp module | Đèn nền |

Các chân `SDO (MISO)`, `T_IRQ`, `T_DO`, `T_DIN`, `T_CS`, `T_CLK` không cần nối khi chỉ dùng hiển thị và không dùng cảm ứng.

### 4.3. Cách phần mềm chọn màn hình

Web admin tab **Cấu hình** có hai lựa chọn:

- `OLED 1.3" SH1106 128×64`: OLED làm màn hình chính, TFT được đưa về trạng thái trống.
- `TFT 4" ST7796S 480×320`: TFT làm màn hình chính, OLED tự chuyển thành mắt và đồng hồ phụ.

Driver và giao diện được tách trong `robot/display.py`, `robot/oled.py` và `robot/tft.py`, vì vậy việc nâng cấp TFT không làm mất khả năng chạy với một OLED nhỏ.

OLED dùng I2C còn TFT dùng SPI0 nên hai màn hình có thể chạy đồng thời mà không tranh chân dữ liệu. Nguồn 5V cần ổn định vì Pi, TFT, OLED, camera và USB soundcard cùng hoạt động.

## 5. Camera CSI

Camera nối bằng cáp ribbon vào cổng CSI của Raspberry Pi Zero 2 W.

Kết quả kiểm tra camera:

```text
0 : ov5647 [2592x1944 10-bit GBRG]
Modes:
640x480
1296x972
1920x1080
2592x1944
```

Lệnh kiểm tra:

```bash
rpicam-hello --list-cameras
```

Lệnh chụp thử:

```bash
rpicam-still --nopreview -t 1000 -o ~/camera-test.jpg
```

Trong app, `robot/camera.py` gọi `rpicam-still`. Ảnh do daemon tự chụp được lưu trong:

```text
/home/pi/robot/captures/
```

## 6. Loa và micro

Robot dùng USB soundcard làm nền âm thanh ổn định:

- Mic input luôn lấy từ USB soundcard, mặc định `plughw:CARD=Device,DEV=0`.
- Khi không có loa Bluetooth, output dùng loa gắn qua USB soundcard.
- Có thể kết nối loa Bluetooth A2DP từ web admin.
- Loa Bluetooth vừa kết nối được tự chọn làm output, không cần chọn thiết bị rồi bấm lưu lần nữa.
- Trước khi kết nối loa Bluetooth mới, robot tự ngắt loa Bluetooth cũ. Mỗi thời điểm chỉ giữ một loa Bluetooth đang kết nối.
- Khi loa Bluetooth bị ngắt hoặc quên, robot tự trở về output USB soundcard.
- Sau mỗi lần output thay đổi, robot phát câu “Đã thay đổi thiết bị âm thanh” qua thiết bị mới.
- Web admin có nút **Kết nối**, **Ngắt kết nối** và **Quên thiết bị** cho từng loa.
- Volume output được chỉnh bằng slider; danh sách input/output kỹ thuật đã được ẩn để giao diện dễ dùng hơn.

Thiết bị Bluetooth vẫn được phát qua BlueALSA theo dạng:

```text
bluealsa:DEV=<MAC>,PROFILE=a2dp,SRV=org.bluealsa
```

Wake listener, nút bấm và test mic đều dùng cùng mic USB soundcard. Robot không dùng mic Bluetooth SCO/HFP làm input chính.

## 6.1. Nút bấm điều khiển

Robot dùng một nút bấm duy nhất để điều khiển nhanh.

Đấu dây:

| Nút bấm | Raspberry Pi Zero 2 W | Ghi chú |
| --- | --- | --- |
| Một chân nút | GPIO27, physical pin 13 | Input nút bấm |
| Chân còn lại | GND, physical pin 14 hoặc chân GND gần nhất | Kéo xuống mass khi bấm |

Cách kéo điện:

- Code dùng pull-up nội bộ của Raspberry Pi, nghĩa là GPIO27 bình thường ở mức HIGH.
- Khi bấm nút, GPIO27 nối xuống GND và chuyển về LOW.
- Không nối VCC/3V3/5V vào nút bấm.
- Không cần điện trở ngoài cho dây ngắn. Nếu dây nút dài hoặc nhiễu, có thể thêm tụ nhỏ chống dội hoặc điện trở kéo lên 10 kOhm về 3V3, nhưng cấu hình mặc định đã có `bounce_time`.

Hành vi nút bấm:

- Bấm giữ: robot dừng âm đang đọc/nhạc, bắt đầu ghi âm từ mic USB soundcard; thả nút thì dừng ghi âm và gửi STT.
- Nếu `ROBOT_TRANSCRIPTION_ENABLED=1`, file WAV được gửi lên endpoint OpenAI-compatible `/audio/transcriptions`, nhận transcript tiếng Việt rồi đưa vào daemon như câu hỏi text.
- Bấm một cái: nếu đang phát nhạc thì dừng nhạc; nếu đang ở bài hướng dẫn từng bước thì chuyển sang bước tiếp theo.
- Bấm nhanh hai cái: màn hình chính hiện tên Wi-Fi hiện tại và URL quản lý robot trong 5 giây, sau đó tự quay về màn hình trước.
- Bấm nhanh ba cái: robot quên Wi-Fi cũ, bật lại AP `robotpi-setup` và giữ ở màn hình cấu hình Wi-Fi để người dùng thiết lập mạng mới.
- Khi robot đang đọc TTS mà người dùng bấm nút, audio bị ngắt ngay để người dùng chen ngang nói.

Ngoài nút bấm, robot còn có chế độ luôn nghe wake phrase offline. Khi nghe đúng “robot ơi”, màn hình chuyển sang “Đang nghe”, robot giữ phần câu nói phía sau, tự dừng khi im lặng rồi gửi đoạn ghi âm lên STT. Wake listener tắt hoàn toàn khi robot ngủ.

## 7. Hệ điều hành headless

Pi đang chạy headless, không cần mini HDMI.

Thông tin đã ghi nhận:

```text
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
Kernel: 6.12.75+rpt-rpi-v8
Architecture: aarch64
Hostname: robotpi
IP LAN khi kiểm tra gần nhất: 192.168.1.130
```

Các file hỗ trợ cài lại OS nhanh nằm trong:

```text
raspi-headless-bootfs/
```

Các file chính:

- `raspi-headless-bootfs/README.md`
- `raspi-headless-bootfs/apply-to-bootfs.sh`

Quy trình cài lại OS headless:

1. Dùng Raspberry Pi Imager ghi **Raspberry Pi OS Lite 64-bit** ra thẻ nhớ.
2. Không cần dùng phần Advanced Settings trong Imager.
3. Sau khi ghi xong, rút thẻ rồi cắm lại vào máy tính để mount phân vùng `bootfs`.
4. Chạy script cấu hình:

```bash
cd /home/tvt/dev/rasberry/raspi-headless-bootfs
chmod +x apply-to-bootfs.sh
PI_HOSTNAME='robotpi' \
PI_USER='pi' \
PI_PASSWORD='mat-khau-pi' \
WIFI_SSID='ten-wifi-2.4GHz' \
WIFI_PASSWORD='mat-khau-wifi' \
WIFI_COUNTRY='VN' \
./apply-to-bootfs.sh /media/tvt/bootfs
```

Script sẽ tự:

- bật SSH headless,
- tạo user đăng nhập,
- đặt hostname `robotpi`,
- cấu hình Wi-Fi 2.4 GHz và country `VN`,
- bật I2C cho OLED và SPI cho TFT,
- đặt cổng micro-USB `USB` ở chế độ host để dùng sound card USB,
- tạo `firstrun.sh` để chạy một lần ở lần boot đầu, sau đó tự reboot.

Các file sinh ra trong `bootfs` gồm `ssh`, `userconf.txt`, `firstrun.sh`,
`user-data`, `network-config`, `meta-data` và `wpa_supplicant.conf`. Các file
này có thể chứa mật khẩu nên không đưa vào GitHub.

Sau đó eject thẻ, cắm vào Pi, cấp nguồn, chờ khoảng 3-5 phút và SSH:

```bash
ssh pi@robotpi.local
```

Nếu `.local` không hoạt động thì xem IP trong router rồi SSH theo IP:

```bash
ssh pi@192.168.1.130
```

## 8. Gói phần mềm

Các gói quan trọng trên Pi:

| Gói | Mục đích |
| --- | --- |
| `python3-pil` | Render giao diện Unicode lên OLED và TFT |
| `python3-gpiozero` | Đọc nút bấm GPIO giữ/thả/click |
| `fonts-dejavu-core` | Font hỗ trợ tiếng Việt có dấu |
| `bluez` | Bluetooth |
| `bluez-alsa-utils` | Phát audio Bluetooth qua ALSA/BlueALSA |
| `alsa-utils` | Cung cấp `aplay`, `arecord`, liệt kê/test sound card USB |
| `curl` | Kiểm tra web server nội bộ |
| `network-manager` | Quản lý Wi-Fi |
| `mpg123` | Phát MP3 từ API TTS với độ trễ thấp qua ALSA/BlueALSA |
| `mpv` | Fallback phát audio và phát nhạc/stream audio từ Internet qua ALSA/BlueALSA |
| `yt-dlp` | Tìm/lấy stream nhạc từ YouTube hoặc nguồn tương thích |

Đã loại bỏ:

- `espeak-ng`: giọng tiếng Việt không phù hợp.
- Chromium/Chrome chạy nền.
- Xvfb/xauth cho browser headless.
- Web Speech API chạy trong browser.
- Service browser nền cũ.

Lý do loại bỏ: Raspberry Pi Zero 2 W chỉ có ít RAM, Chromium chạy nền nặng, phần TTS trên Pi không có voice tiếng Việt dùng được, còn STT browser không phù hợp với hướng nút bấm ghi âm.

## 9. Cấu trúc repo

```text
/home/tvt/dev/rasberry
├── BAO_CAO_ROBOT_HOC_TAP.md
├── deploy_to_pi.sh
├── install_robot_service.sh
├── submit_robot.sh
├── systemd/
│   ├── robot.service
│   └── bluealsa-robot-hfp.conf
├── raspi-headless-bootfs/
└── robot/
    ├── app.py
    ├── audio.py
    ├── button_manager.py
    ├── camera.py
    ├── config.py
    ├── display.py
    ├── llm_client.py
    ├── music.py
    ├── network_manager.py
    ├── oled.py
    ├── robot_daemon.py
    ├── settings.py
    ├── tft.py
    ├── voice_input.py
    ├── wake_word.py
    ├── web_server.py
    ├── README.md
    ├── .env
    └── .env.example
```

Vai trò file chính:

| File | Vai trò |
| --- | --- |
| `robot/app.py` | CLI: camera-check, capture, models, speak, daemon, status, submit, next-step, regenerate-status-audio |
| `robot/robot_daemon.py` | Daemon chạy nền, quản lý màn hình/camera/AI/hội thoại/voice/audio/sleep và hướng dẫn từng bước |
| `robot/llm_client.py` | Gọi OpenAI-compatible `/chat/completions`, xử lý text/image và fallback provider |
| `robot/camera.py` | Chụp ảnh bằng `rpicam-still` |
| `robot/display.py` | Lớp chọn màn hình và render giao diện màu cho TFT |
| `robot/oled.py` | Driver SH1106 I2C, mắt biểu cảm, giao diện OLED và đồng hồ/thời tiết |
| `robot/tft.py` | Driver SPI ST7796S, cập nhật toàn màn hình hoặc vùng hoạt ảnh |
| `robot/audio.py` | Gọi API TTS, ưu tiên phát MP3 bằng `mpg123`, hỗ trợ OpenAI TTS streaming, cache audio trạng thái |
| `robot/button_manager.py` | Đọc nút GPIO, phân biệt bấm giữ, bấm một cái, hai cái và ba cái |
| `robot/sound_manager.py` | Quét/kết nối Bluetooth, tự chọn output, ngắt loa cũ và fallback về USB soundcard |
| `robot/voice_input.py` | Ghi âm bằng `arecord` từ mic USB soundcard |
| `robot/wake_word.py` | Nhận diện “robot ơi” offline và ghi phần câu nói phía sau tới khi im lặng |
| `robot/music.py` | Tìm và phát nhạc bằng `yt-dlp` + `mpv`, không điều khiển động cơ dance |
| `robot/network_manager.py` | Quản lý Wi-Fi, quét SSID, kết nối Wi-Fi, bật AP fallback |
| `robot/settings.py` | Đọc/ghi `.env` an toàn cho web UI, che API key khi trả về browser |
| `robot/web_server.py` | Web UI quản lý robot, không có STT/TTS browser |
| `robot/config.py` | Đọc cấu hình `.env`, default prompt, luật camera, status audio và provider fallback |
| `systemd/robot.service` | Service tự chạy khi Pi bật nguồn |
| `install_robot_service.sh` | Deploy code, cài gói cần thiết, cài/restart service |
| `deploy_to_pi.sh` | Đồng bộ thư mục `robot/` sang `/home/pi/robot` |
| `submit_robot.sh` | Gửi câu hỏi từ PC sang daemon trên Pi qua SSH |

## 10. Cấu hình `.env`

Dạng cấu hình hiện tại, đã che key thật:

```env
OPENAI_BASE_URL=https://<endpoint-provider>/v1
OPENAI_API_KEY=[redacted]
OPENAI_PROVIDER_ENABLED=1
OPENAI_PROVIDER_NAME="AI chính"
OPENAI_PROVIDER_TYPE=openai_compatible
OPENAI_PROVIDER_ORDER=1,2
OPENAI_MODEL=<model-chat-vision>
OPENAI_TIMEOUT=120
OPENAI_MAX_TOKENS=2500
OPENAI_TEMPERATURE=0.2
OPENAI2_ENABLED=1
OPENAI2_PROVIDER_NAME="OpenAI fallback"
OPENAI2_PROVIDER_TYPE=openai
OPENAI2_BASE_URL=https://api.openai.com/v1
OPENAI2_API_KEY=[redacted nếu có]
OPENAI2_MODEL=gpt-4o-mini
OPENAI3_ENABLED=1
OPENAI3_PROVIDER_NAME="Google STT"
OPENAI3_PROVIDER_TYPE=google
OPENAI3_BASE_URL=https://speech.googleapis.com/v1p1beta1/speech:recognize
OPENAI3_API_KEY=[redacted nếu có]
OPENAI3_MODEL=latest_short
OPENAI4_ENABLED=1
OPENAI4_PROVIDER_NAME="3FTool TTS"
OPENAI4_PROVIDER_TYPE=tts_3ftool
OPENAI4_BASE_URL=https://tts.3ftool.com/v1
OPENAI4_API_KEY=[redacted]
OPENAI4_MODEL=
ROBOT_STT_PROVIDER_ORDER=3,2
ROBOT_TTS_PROVIDER_ORDER=4,2

ROBOT_CONVERSATION_MAX_MESSAGES=24
ROBOT_CONVERSATION_MAX_CHARS=24000
ROBOT_STEP_GUIDE_ENABLED=1
ROBOT_STEP_INTRO_TEXT="Tôi sẽ hướng dẫn bạn từng bước..."
ROBOT_DISPLAY_TYPE=tft_4_st7796s
ROBOT_SLEEP_ENABLED=1
ROBOT_SLEEP_IDLE_SECONDS=300

ROBOT_AUDIO_ENABLED=1
ROBOT_SOUND_ENABLED=1
ROBOT_BLUETOOTH_MAC=
ROBOT_AUDIO_DEVICE=plughw:CARD=Device,DEV=0
ROBOT_AUDIO_INPUT_DEVICE=plughw:CARD=Device,DEV=0
ROBOT_AUDIO_OUTPUT_VOLUME=90
ROBOT_TTS_BASE_URL=https://tts.3ftool.com/v1
ROBOT_TTS_API_KEY=[redacted]
ROBOT_TTS_PROVIDER=auto
ROBOT_TTS_VOICE=vi_female_thuytrang_mb
ROBOT_TTS_RESPONSE_FORMAT=mp3
ROBOT_TTS_SPEED=1.0
ROBOT_TTS_PITCH=1.0
ROBOT_TTS_VOLUME=100
ROBOT_TTS_BITRATE="128 kbps"
ROBOT_TTS_MAX_CHARS=320
ROBOT_TTS_TIMEOUT=120
ROBOT_OPENAI_TTS_MODEL=gpt-4o-mini-tts
ROBOT_OPENAI_TTS_VOICE=alloy
ROBOT_OPENAI_TTS_RESPONSE_FORMAT=mp3
ROBOT_OPENAI_TTS_SPEED=1.0
ROBOT_STATUS_AUDIO_ENABLED=1
ROBOT_STATUS_AUDIO_DIR=/home/pi/robot/status_audio
ROBOT_STATUS_BOOTING_TEXT="Robot đang khởi động..."
ROBOT_STATUS_WIFI_CONNECTED_TEXT="Robot đã có kết nối Wi-Fi."
ROBOT_STATUS_WIFI_SETUP_TEXT="Robot chưa tìm thấy mạng Wi-Fi..."
ROBOT_STATUS_READY_TEXT="Xin chào, tôi là trợ lý học tập..."
ROBOT_STATUS_CAPTURE_PREPARE_TEXT="Tôi đang chụp ảnh..."
ROBOT_STATUS_STT_NOT_HEARD_TEXT="Xin lỗi, tôi chưa nghe rõ câu hỏi của bạn, thử nói lại nhé."

ROBOT_WEB_ENABLED=1
ROBOT_WEB_HOST=0.0.0.0
ROBOT_WEB_PORT=8765

ROBOT_AP_ENABLED=1
ROBOT_AP_IFNAME=wlan0
ROBOT_AP_SSID=robotpi-setup
ROBOT_AP_PASSWORD=[redacted]
ROBOT_WIFI_WAIT_SECONDS=35
ROBOT_CAPTURE_KEEP_FILES=0
ROBOT_CAPTURE_MAX_MB=50
ROBOT_MUSIC_ENABLED=1
ROBOT_MUSIC_PLAYER=mpv
ROBOT_BUTTON_ENABLED=1
ROBOT_BUTTON_GPIO=27
ROBOT_BUTTON_HOLD_SECONDS=0.7
ROBOT_BUTTON_MULTI_CLICK_SECONDS=0.55
ROBOT_WAKE_WORD_ENABLED=1
ROBOT_WAKE_WORD_PHRASES="robot ơi\nrô bốt ơi"
ROBOT_WAKE_WORD_END_SILENCE_SECONDS=1.0
ROBOT_WAKE_WORD_MIN_RMS=1000
ROBOT_WAKE_WORD_RMS_MULTIPLIER=2.0
ROBOT_WAKE_WORD_MAX_SECONDS=20
ROBOT_RECORDINGS_DIR=/home/pi/robot/recordings
ROBOT_RECORDING_SAMPLE_RATE=16000
ROBOT_RECORDING_CHANNELS=1
ROBOT_TRANSCRIPTION_ENABLED=1
ROBOT_TRANSCRIPTION_PROVIDER=auto
ROBOT_TRANSCRIPTION_MODEL=whisper-1
ROBOT_TRANSCRIPTION_LANGUAGE=vi
ROBOT_GOOGLE_STT_ENABLED=1
ROBOT_GOOGLE_STT_LANGUAGE_CODE=vi-VN
```

Giải thích:

- `OPENAI_BASE_URL`: endpoint OpenAI-compatible của provider.
- `OPENAI_MODEL`: model chat/vision đang dùng.
- `OPENAI_PROVIDER_TYPE`, `OPENAI2_PROVIDER_TYPE`, `OPENAI3_PROVIDER_TYPE`, `OPENAI4_PROVIDER_TYPE`: loại provider, gồm `openai_compatible`, `openai`, `google`, `tts_3ftool`.
- `OPENAI_PROVIDER_ORDER`: thứ tự ưu tiên cho hỏi đáp, ảnh và AI thường; mặc định `1,2`, nghĩa là thử AI1 trước rồi fallback AI2.
- `OPENAI2_*`, `OPENAI3_*`, `OPENAI4_*`: cấu hình provider dự phòng hoặc provider chuyên dụng; AI2 là OpenAI fallback, AI3 là Google STT, AI4 là 3FTool TTS.
- `OPENAI_MAX_TOKENS=2500`: đủ dài để trích xuất chữ/công thức từ ảnh tài liệu.
- `ROBOT_CONVERSATION_MAX_MESSAGES`: số message hội thoại giữ lại.
- `ROBOT_CONVERSATION_MAX_CHARS`: giới hạn độ dài hội thoại; quá dài thì cắt phần cũ.
- `ROBOT_PROMPT_PLAIN`, `ROBOT_PROMPT_STRUCTURED`: prompt hệ thống có thể chỉnh trong web admin.
- `ROBOT_STEP_GUIDE_ENABLED`: bật chế độ hướng dẫn bài tập từng bước.
- `ROBOT_DISPLAY_TYPE`: chọn `oled_1_3_sh1106` hoặc `tft_4_st7796s`. Khi chọn TFT, OLED tự chuyển thành màn hình phụ.
- `ROBOT_SLEEP_ENABLED`, `ROBOT_SLEEP_IDLE_SECONDS`: bật và đặt thời gian không tương tác trước khi ngủ.
- `ROBOT_AUDIO_ENABLED=1`: bật đọc câu trả lời ra loa.
- `ROBOT_SOUND_ENABLED=1`: bật âm báo local như shutter/suy nghĩ.
- `ROBOT_BLUETOOTH_MAC`: loa Bluetooth ưu tiên gần nhất; daemon tự cập nhật khi kết nối loa mới.
- `ROBOT_AUDIO_DEVICE`: output hiện tại; daemon tự chuyển giữa BlueALSA và USB soundcard.
- `ROBOT_AUDIO_INPUT_DEVICE`: input mic USB soundcard, được phần mềm giữ cố định để tránh chọn nhầm mic Bluetooth.
- `ROBOT_AUDIO_OUTPUT_VOLUME`: âm lượng mixer output 0-100% cho thiết bị ALSA như USB sound card; daemon tự set khi boot hoặc khi lưu cấu hình. Với output Bluetooth BlueALSA, biến này không chỉnh trực tiếp volume loa.
- `ROBOT_BUTTON_GPIO=27`: chân GPIO đọc nút bấm, mặc định physical pin 13.
- `ROBOT_BUTTON_HOLD_SECONDS`: thời gian giữ tối thiểu để chuyển sang chế độ nói.
- `ROBOT_BUTTON_MULTI_CLICK_SECONDS`: khoảng thời gian nhận click nhanh; hai click hiện URL quản lý trong 5 giây, ba click quên Wi-Fi cũ và bật lại AP setup.
- `ROBOT_WAKE_WORD_ENABLED`: bật nhận diện wake phrase offline.
- `ROBOT_WAKE_WORD_PHRASES`: các cách phát âm chấp nhận cho “robot ơi”.
- `ROBOT_WAKE_WORD_END_SILENCE_SECONDS`: thời gian im lặng để kết thúc câu sau wake phrase.
- `ROBOT_WAKE_WORD_MIN_RMS`, `ROBOT_WAKE_WORD_RMS_MULTIPLIER`: ngưỡng giọng nói so với tiếng nền.
- `ROBOT_WAKE_WORD_MAX_SECONDS`: thời lượng tối đa của câu nói sau wake phrase.
- `ROBOT_RECORDINGS_DIR`: thư mục lưu file WAV ghi âm tạm thời.
- `ROBOT_TRANSCRIPTION_ENABLED`: bật gửi file ghi âm lên endpoint transcription.
- `ROBOT_STT_PROVIDER_ORDER`: thứ tự nguồn STT, mặc định `3,2`, nghĩa là thử AI3/Google Speech-to-Text trước rồi fallback AI2/OpenAI.
- `ROBOT_TTS_PROVIDER_ORDER`: thứ tự nguồn TTS, mặc định `4,2`, nghĩa là thử AI4/3FTool trước rồi fallback AI2/OpenAI TTS nếu có key.
- `ROBOT_TRANSCRIPTION_PROVIDER`: cấu hình cũ vẫn được hỗ trợ; khi `ROBOT_STT_PROVIDER_ORDER` dùng số provider như `3,2` thì thứ tự này là chính.
- `ROBOT_TRANSCRIPTION_MODEL`: model transcription OpenAI-compatible, mặc định `whisper-1`.
- `ROBOT_GOOGLE_STT_*`: tham số Google Speech-to-Text như language code, sample rate và dấu câu; endpoint/key/model nằm trong AI3.
- `OPENAI4_BASE_URL`, `OPENAI4_API_KEY`: endpoint và key API TTS 3FTool.
- `ROBOT_OPENAI_TTS_*`: model/voice/format/speed khi fallback qua OpenAI TTS.
- `ROBOT_TTS_VOICE`: speaker ID tiếng Việt dùng cho TTS.
- `ROBOT_TTS_MAX_CHARS`: giới hạn ký tự mỗi request TTS cho 3FTool/EverAI hoặc OpenAI-compatible; câu trả lời dài sẽ tự chia thành nhiều đoạn. OpenAI chính thức không dùng cấu hình này, robot dùng streaming và giới hạn nội bộ 4096 ký tự/lượt.
- `ROBOT_STATUS_AUDIO_ENABLED`: bật các câu thông báo trạng thái đã cache bằng TTS.
- `ROBOT_STATUS_*_TEXT`: nội dung thông báo boot/Wi-Fi/AP/sẵn sàng/chụp ảnh/không nghe rõ/lỗi.
- `ROBOT_WEB_ENABLED=1`: bật web UI nội bộ.
- `ROBOT_AP_ENABLED=1`: nếu không kết nối được Wi-Fi thì tự bật AP cấu hình.
- `ROBOT_AP_SSID`: tên Wi-Fi setup robot phát ra khi mất Wi-Fi.
- `ROBOT_WIFI_WAIT_SECONDS`: số giây chờ Wi-Fi đã lưu tự kết nối trước khi bật AP setup.
- `ROBOT_CAPTURE_KEEP_FILES=0`: xóa ảnh sau khi xử lý xong; nếu muốn debug có thể đặt số ảnh mới nhất cần giữ.
- `ROBOT_CAPTURE_MAX_MB=50`: giới hạn dung lượng thư mục ảnh khi có giữ ảnh.
- `ROBOT_CAPTURE_*`: các danh sách từ khóa/regex để nhận diện khi nào cần tự chụp ảnh.
- `ROBOT_MUSIC_PLAYER=mpv`: lệnh phát nhạc.
- `ROBOT_MUSIC_OPENING_TEXT`: câu robot nói khi mở nhạc thường, dùng `{query}` để chèn nội dung tìm nhạc.

## 11. AI và provider

Runtime đọc provider từ web admin và `.env`; tên model/endpoint có thể đổi mà không sửa code. Trạng thái robot kiểm tra gần nhất dùng AI chính qua gateway OpenAI-compatible, model `cx/gpt-5.5`, kèm OpenAI fallback, Google STT và 3FTool TTS. API key và endpoint riêng không ghi trong báo cáo công khai.

Luồng chat/vision dùng giao thức OpenAI-compatible:

```text
Base URL: https://<endpoint-provider>/v1
Model: cấu hình trong web admin
Endpoint: /chat/completions
```

Phần mềm hỗ trợ 4 cấu hình provider:

- AI 1: cấu hình chính `OPENAI_*`.
- AI 2: cấu hình dự phòng `OPENAI2_*`.
- AI 3: Google STT hoặc provider chuyên dụng `OPENAI3_*`.
- AI 4: TTS 3FTool hoặc provider chuyên dụng `OPENAI4_*`.

Daemon thử provider theo thứ tự riêng cho hỏi đáp, STT và TTS. Nếu provider trước lỗi HTTP/network/JSON, daemon tự thử provider sau. Khi tất cả provider đều lỗi thì mới báo lỗi lên màn hình/web/CLI.

Chức năng AI đang dùng:

- Hỏi đáp text.
- Hỏi đáp kèm ảnh.
- Vision/OCR cho trang vở, sách, tài liệu, bảng, màn hình.
- Trả JSON để app dễ xử lý.
- Nhận hội thoại cũ để trả lời theo ngữ cảnh.
- Trả danh sách bước hướng dẫn khi bài tập cần giải nhiều bước.

Schema JSON yêu cầu model trả:

```json
{
  "answer": "câu trả lời đầy đủ bằng tiếng Việt",
  "display": "tóm tắt ngắn để hiện trên màn hình robot",
  "image_description": "mô tả/trích xuất nội dung ảnh nếu có ảnh",
  "mode": "answer hoặc guided_steps",
  "steps": [
    {
      "display": "công thức/kết quả ngắn cho màn hình",
      "speech": "nội dung đọc ra loa của bước này"
    }
  ]
}
```

Ý nghĩa:

- `answer`: câu trả lời đầy đủ, phù hợp để đọc ra loa.
- `display`: dòng ngắn ưu tiên vừa OLED 128×64, đồng thời được trình bày lớn hơn trên TFT.
- `image_description`: mô tả và OCR ảnh để lưu vào hội thoại.
- `mode`: `answer` cho câu trả lời thường, `guided_steps` cho hướng dẫn từng bước.
- `steps`: danh sách bước khi cần hướng dẫn học sinh làm bài nhiều bước.

Prompt ảnh đã nhấn mạnh: nếu ảnh là sách/vở/báo/đề bài/tài liệu/màn hình/bảng thì AI phải trích xuất toàn bộ chữ đọc được, công thức, số liệu, đơn vị, bảng, ký hiệu, sơ đồ và hình vẽ quan trọng; phần mờ thì ghi rõ không đọc được.

## 12. Bộ nhớ hội thoại

Trước đây mỗi request độc lập. Hiện daemon đã lưu hội thoại gần đây.

File trên Pi:

```text
/home/pi/robot/logs/conversation.json
/home/pi/robot/logs/history.jsonl
```

Cơ chế:

1. Nhận câu hỏi mới.
2. Lấy lịch sử trong `conversation.json`.
3. Gửi lịch sử + câu hỏi mới lên AI.
4. Nhận JSON trả lời.
5. Lưu lượt user/assistant mới.
6. Nếu quá dài thì cắt message cũ ở đầu.

Khi request có ảnh, daemon lưu thêm:

```text
[Mô tả và nội dung trích xuất từ ảnh trong lượt này]
...
```

Nhờ vậy sau khi chụp trang vở, người dùng có thể hỏi tiếp như “giải thích lại bước 2” mà không cần gửi lại ảnh nếu nội dung đã được trích xuất đủ.

### Hướng dẫn bài tập từng bước

Khi câu hỏi là dạng “hướng dẫn giải bài này”, “bài này làm thế nào”, hoặc bài cần nhiều bước, prompt yêu cầu AI trả `mode=guided_steps` kèm mảng `steps`.

Luồng hoạt động:

1. Robot nói câu giới thiệu: “Tôi sẽ hướng dẫn bạn từng bước...”.
2. Màn hình chính hiện `display` của bước 1, ví dụ công thức, định lý, phép tính hoặc kết quả ngắn.
3. Loa đọc `speech` của bước 1.
4. Robot chuyển sang trạng thái chờ bước tiếp theo.
5. Khi học sinh bấm/chạm nút “bụng robot”, hoặc bấm nút web/lệnh SSH, daemon đọc bước kế tiếp.

Lệnh test khi chưa có nút vật lý:

```bash
ssh pi@192.168.1.130 'cd /home/pi/robot && python3 app.py next-step'
```

Daemon tự bỏ phần lặp kiểu “Bước 3” nếu AI đã lỡ thêm vào `speech`. Trước khi gửi TTS, daemon cũng chuyển một số công thức đơn giản sang lời đọc rõ hơn, ví dụ `2x + 5 = 13` thành “hai ích cộng năm bằng mười ba”, để tránh TTS đọc chữ `x` quá nhỏ hoặc thành tiếng Anh.

Riêng các câu hỏi giải phương trình, bất phương trình, hệ phương trình hoặc tìm nghiệm/tập nghiệm, daemon chủ động thêm yêu cầu điều khiển vào prompt để AI bắt buộc trả `mode="guided_steps"`, kể cả khi học sinh hỏi ngắn như “nghiệm là gì”. Nhờ vậy robot không chỉ đọc đáp án cuối mà hướng dẫn từng bước và chờ học sinh bấm nút để sang bước tiếp theo.

Lệnh reset hội thoại:

```bash
ssh pi@192.168.1.130 'cd /home/pi/robot && python3 app.py reset-conversation'
```

## 13. Luật tự chụp ảnh

Daemon tự quyết định chụp ảnh cho các câu có ý định nhìn/chụp rõ ràng. Luật xử lý nằm trong `robot/robot_daemon.py`, còn danh sách từ khóa/regex được đưa ra web admin tab **Luật camera** để người dùng xem và chỉnh trong `.env`. Luật này dùng chung cho SSH, web UI và luồng voice/nút bấm.

Ví dụ sẽ tự chụp:

```text
chụp ảnh xem cái này là cái gì
dùng camera xem cái này là cái gì
xem câu này giải thế nào
câu 18 giải thế nào
chỗ này điền thế nào
bài này giải sao
mở camera đọc trang này
```

Ví dụ không tự chụp:

```text
không cần chụp ảnh, giải thích lại bước hai
đừng chụp, chỉ trả lời bằng chữ
ảnh hưởng của lực ma sát là gì
camera hoạt động như thế nào
```

Nếu muốn chắc chắn chụp, dùng cờ:

```bash
./submit_robot.sh --capture "Robot ơi, đọc trang này."
```

Nếu muốn chắc chắn không chụp, nói rõ trong câu:

```bash
./submit_robot.sh "Không cần chụp ảnh, giải thích lại bước hai."
```

Các nhóm cấu hình chính:

- Cụm gọi chụp trực tiếp: `chụp`, `chụp ảnh`, `mở camera`, `dùng camera`, `scan`.
- Động từ thị giác: `nhìn`, `xem`, `đọc`, `coi`, `kiểm tra`.
- Đối tượng: `câu`, `bài`, `chỗ`, `trang`, `đề`, `hình`, `vở`, `sách`, `tài liệu`.
- Cụm chỉ định: `câu 18`, `bài này`, `chỗ này`, `trang này`, `đề này`.
- Mẫu phủ định: `không cần chụp`, `đừng chụp`, `chỉ trả lời`.

## 14. Giao diện màn hình

### TFT 4 inch làm màn hình chính

TFT hiển thị giao diện màu 480×320 với chữ lớn và bố cục riêng cho từng trạng thái:

- Sẵn sàng: khuôn mặt robot chớp mắt, liếc mắt và chuyển động nhẹ.
- Đang nghe: micro, sóng âm và thông báo tự gửi khi người dùng nói xong.
- Đang chụp: khung camera và nhắc giữ camera ổn định.
- Đang suy nghĩ: khuôn mặt và bong bóng suy nghĩ được tách rõ.
- Đang nói/trả lời: nội dung tóm tắt dễ đọc.
- Đang phát nhạc: khuôn mặt, nốt nhạc và thanh nhịp.
- Thông tin, setup và lỗi: màu sắc khác nhau để dễ phân biệt.

Giao diện TFT dùng font Unicode, màu sắc sinh động và footer liền mạch `© THCS Nguyễn Tất Thành` bên trái, `Đông Gia Nghĩa - Lâm Đồng` bên phải. Vùng mắt được cập nhật riêng khoảng 8 FPS để hoạt ảnh mượt hơn mà không phải truyền lại toàn bộ khung 480×320 liên tục.

### OLED làm màn hình phụ

Khi TFT được chọn, OLED không bị bỏ đi mà trở thành phần biểu cảm của robot:

- Hiển thị mắt bình thường, nghe, suy nghĩ, nói, vui, phát nhạc, lo lắng và tò mò.
- Nhịp chớp/liếc khi chờ đồng bộ với TFT bằng cùng phase thời gian.
- Hiển thị mắt 20 giây, sau đó chuyển sang đồng hồ 10 giây và lặp lại.
- Đồng hồ hiện giờ lớn, thứ/ngày/tháng, ngày âm lịch, nhiệt độ, độ ẩm, khả năng mưa, khoảng nhiệt độ và biểu tượng thời tiết.
- Nhóm thông tin thời tiết đổi mỗi 2 giây; dữ liệu Internet làm mới khoảng 15 phút một lần.

Khi robot ngủ ở cấu hình hai màn hình, TFT được vẽ đen hoàn toàn còn OLED hiện đồng hồ. Wake listener và audio dừng; web admin vẫn truy cập được và nút GPIO27 dùng để đánh thức.

### Chế độ chỉ dùng OLED

Nếu chọn `oled_1_3_sh1106` trong web admin, OLED trở lại làm màn hình chính và giữ giao diện cũ: sẵn sàng, đang nghe, đang chụp, đang suy nghĩ, phát nhạc, tóm tắt, lỗi, setup và screensaver ngủ. Nhờ vậy sản phẩm vẫn có thể lắp bản giá rẻ chỉ với OLED 1.3 inch.

## 15. Web UI

Web UI chạy khi `ROBOT_WEB_ENABLED=1`:

```text
http://<IP-cua-robot>:8765/
Ví dụ: http://192.168.1.130:8765/
```

Chức năng hiện tại:

- Nhập câu hỏi.
- Hiển thị lịch sử hội thoại user/assistant và mô tả ảnh đã lưu ở từng lượt.
- Tick checkbox chụp ảnh bằng camera nếu muốn ép chụp.
- Bấm “Bước tiếp theo” khi robot đang hướng dẫn bài tập từng bước.
- Xóa hội thoại.
- Xem câu trả lời đầy đủ.
- Xem trạng thái daemon.
- Xem trước camera realtime trong tab Camera qua MJPEG stream `/api/camera/stream`, có các mức 640×480, 1296×972 và 1920×1080; khi cần chụp ảnh để gửi AI, daemon tự tạm dừng preview, chụp bằng `rpicam-still`, xuất JPEG gốc vừa chụp ngay dưới khung preview, rồi preview tự nối lại.
- Cấu hình thời gian vào chế độ ngủ trong tab **Cấu hình**: 1, 3, 5, 10, 15 phút hoặc `Không ngủ`.
- Chọn OLED 1.3 inch làm màn hình chính hoặc TFT 4 inch làm màn hình chính kèm OLED phụ.
- Quét danh sách Wi-Fi xung quanh.
- Chọn SSID, nhập password và yêu cầu Pi kết nối Wi-Fi.
- Quét Bluetooth, pair/trust/connect, ngắt kết nối và quên loa Bluetooth.
- Kết nối loa nào thì robot tự dùng loa đó làm output và tự ngắt loa Bluetooth cũ.
- Chỉnh volume output bằng slider; với USB sound card, web hiển thị mixer `Speaker/Master` hiện tại và daemon set lại theo cấu hình khi lưu/khởi động.
- Mic input được giữ cố định ở USB soundcard; giao diện không hiện danh sách input/output kỹ thuật không cần thiết.
- Test loa bằng TTS và test mic USB bằng bản ghi ngắn.
- Cấu hình AI1-AI4 trong tab Nguồn AI: AI1 OpenAI-compatible chính, AI2 OpenAI fallback, AI3 Google STT, AI4 3FTool TTS.
- Cấu hình thứ tự fallback theo tác vụ: hỏi đáp/ảnh `1,2`, STT `3,2`, TTS `4,2`.
- Cấu hình nút bấm GPIO, thời gian giữ để nói, hai click hiện URL, ba click reset Wi-Fi.
- Cấu hình wake phrase offline, ngưỡng tiếng nói, tiếng nền, thời gian im lặng và thời lượng câu tối đa.
- Sửa cấu hình provider AI, prompt hệ thống, luật camera, tham số TTS/ghi âm, thiết bị âm thanh và AP fallback.
- Bật/tắt âm báo trạng thái.
- Chỉnh câu thông báo trạng thái và regenerate audio cache.
- Cấu hình chính sách giữ/xóa ảnh camera.
- Test TTS ra output hiện tại và làm mới trạng thái âm thanh.
- Khởi động lại `robot.service` ngay trên web admin sau khi đổi cấu hình.

Web UI hiện không dùng Chrome STT/TTS, không dùng Web Speech API và không phụ thuộc browser để nhận/đọc giọng nói.

Khi Pi đã vào LAN, màn hình chính hiển thị link quản lý trong 3 giây khi khởi động, ví dụ:

```text
http://192.168.1.130:8765/
```

Nếu Pi boot nhưng không kết nối được Wi-Fi, daemon bật access point:

```text
SSID: robotpi-setup
Password: [redacted trong báo cáo]
URL: http://10.42.0.1:8765/
```

Người dùng kết nối điện thoại/laptop vào SSID này, mở URL trên, chọn Wi-Fi thật và lưu cấu hình.

Lưu ý quan trọng: khi đang bật AP setup hoặc đã vào Wi-Fi nhưng chưa có Internet, màn hình chính giữ thông tin cấu hình mạng. Các chức năng AI, TTS động, phân tích ảnh và tìm/phát nhạc không chạy cho tới khi robot có Internet.

## 16. Service tự chạy khi cấp nguồn

Service chính:

```text
/etc/systemd/system/robot.service
```

Nội dung chính:

```text
WorkingDirectory=/home/pi/robot
ExecStart=/usr/bin/python3 /home/pi/robot/app.py daemon
Restart=always
RestartSec=3
WantedBy=multi-user.target
```

Khi Pi bật nguồn:

1. `robot.service` tự chạy sau NetworkManager.
2. Daemon phát câu trạng thái “robot đang khởi động” nếu audio cache đã có hoặc có thể tạo được.
3. Daemon kiểm tra `wlan0`.
4. Nếu đã vào Wi-Fi, daemon giữ kết nối LAN hiện tại và phát câu “đã có kết nối Wi-Fi”.
5. Nếu chưa vào được Wi-Fi sau thời gian chờ, daemon bật AP `robotpi-setup` và phát câu hướng dẫn vào Wi-Fi setup.
6. Daemon mở socket `/tmp/robotpi/robot.sock`.
7. Nếu bật web, daemon mở port `8765`.
8. Nếu đã vào LAN, màn hình chính hiện link quản lý trong 3 giây rồi báo sẵn sàng; loa phát câu giới thiệu robot.
9. Nếu đang ở AP setup, màn hình chính giữ SSID/IP/link setup để người dùng cấu hình Wi-Fi.
10. Khi robot idle quá thời gian cấu hình, daemon phát câu thông báo vào ngủ, dừng wake listener/audio/nhạc nhưng vẫn giữ `robot.service`, web admin và button manager.
11. Ở cấu hình TFT + OLED, TFT chuyển đen và OLED hiện đồng hồ. Ở cấu hình chỉ OLED, OLED dùng screensaver ngủ.
12. Khi người dùng bấm nút, màn hình hiện “Đang khởi động”, daemon bật lại audio/wake listener, kiểm tra mạng rồi trở về sẵn sàng hoặc setup.
13. Nếu có lỗi, màn hình hiển thị lỗi; systemd tự restart daemon khi process chết.

Kết quả kiểm tra gần nhất ngày 29/07/2026:

- Sau reboot, Pi quay lại Wi-Fi `Dung Dung Dung Dung Dung Dung`.
- IP LAN: `192.168.1.130`.
- Trang quản lý: `http://192.168.1.130:8765/`.
- Trạng thái daemon: `ready`, TFT chính và OLED phụ hoạt động, wake listener sẵn sàng.

Nguyên nhân lỗi trước đó có khả năng là daemon bật AP setup quá sớm, chiếm `wlan0`, làm NetworkManager không kịp đưa Pi về profile Wi-Fi LAN. Bản hiện tại chờ `ROBOT_WIFI_WAIT_SECONDS`, thử các Wi-Fi đã lưu trước, và chỉ bật AP khi không có LAN.

Installer hiện cũng disable/remove service browser nền cũ để tránh Pi tự chạy Chromium lại.

## 17. Lệnh vận hành

Deploy code từ PC sang Pi:

```bash
cd /home/tvt/dev/rasberry
REMOTE=pi@192.168.1.130 CONTROL_PATH=/tmp/no-robotpi.sock ./deploy_to_pi.sh
```

Script deploy giữ nguyên `.env`, `logs/`, `captures/` và `status_audio/` trên Pi để không làm mất cấu hình web admin, hội thoại, ảnh debug hoặc audio trạng thái đã tạo. Nếu Pi chưa có `.env`, script mới chép `.env` local sang lần đầu.

Cài/restart service:

```bash
cd /home/tvt/dev/rasberry
REMOTE=pi@192.168.1.130 CONTROL_PATH=/tmp/no-robotpi.sock ./install_robot_service.sh
```

Gửi câu hỏi text:

```bash
./submit_robot.sh "Robot ơi, 2 + 3 bằng mấy?"
```

Ép chụp ảnh:

```bash
./submit_robot.sh --capture "Robot ơi, đọc trang này."
```

Kiểm tra trạng thái:

```bash
ssh pi@192.168.1.130 'cd /home/pi/robot && python3 app.py status'
```

Xem log service:

```bash
ssh pi@192.168.1.130 'journalctl -u robot.service -f'
```

## 18. Demo gợi ý khi thuyết trình

### Demo 1: Gọi robot bằng giọng nói

Nói: **“Robot ơi, quang hợp là gì?”**

Điểm cần nói: wake phrase được phát hiện offline; TFT và OLED chuyển sang trạng thái đang nghe, robot tự dừng sau im lặng, gửi STT rồi đọc câu trả lời.

### Demo 2: Chụp ảnh bài vở và hỏi tiếp

Nói: **“Robot ơi, mở camera giải câu 10.”** Sau khi trả lời, hỏi tiếp nội dung khác có trong cùng ảnh.

Điểm cần nói: daemon tự chụp bằng camera CSI; mô tả và chữ trích xuất từ ảnh được lưu vào hội thoại nên robot còn nhớ nội dung ảnh cho các câu hỏi sau.

### Demo 3: Hướng dẫn từng bước

Yêu cầu robot giải một phương trình. Sau mỗi bước, bấm một lần vào nút bụng để chuyển tiếp.

Điểm cần nói: robot không chỉ đọc đáp án cuối mà chia quá trình giải thành các bước ngắn, phù hợp cho học sinh tự học.

### Demo 4: Hai chế độ màn hình

Trong web admin, chuyển giữa `TFT 4"` và `OLED 1.3"`.

Điểm cần nói: TFT lớn giúp trình bày rõ và sinh động; nếu chỉ lắp OLED nhỏ, robot vẫn vận hành đầy đủ. Khi dùng TFT, OLED cũ được tận dụng làm mắt và đồng hồ phụ.

### Demo 5: Âm thanh và mở nhạc

Kết nối một loa Bluetooth rồi nói: **“Robot ơi, mở một bài hát thiếu nhi.”**

Điểm cần nói: robot tự chuyển output sang loa mới, tự ngắt loa Bluetooth cũ và hiểu lệnh nhạc bằng parser cục bộ không tốn token AI.

## 19. Tương tác giọng nói

Robot không dùng browser STT hoặc Chromium. Mic luôn lấy từ USB soundcard và có hai cách bắt đầu câu hỏi.

### Bấm giữ nút để nói

1. Giữ nút GPIO27: robot dừng âm/nhạc đang phát và ghi âm bằng `arecord`.
2. Màn hình chuyển sang “Đang nghe”.
3. Thả nút: robot đóng file WAV, gửi lên provider STT theo thứ tự fallback.
4. Transcript được đưa vào daemon như câu hỏi text.
5. File WAV tạm được xóa sau khi xử lý để không làm đầy thẻ nhớ.

### Gọi “robot ơi”

1. Vosk model tiếng Việt nhỏ chạy offline liên tục khi robot thức.
2. Chỉ khi nhận đúng wake phrase, robot mới chuyển sang trạng thái command.
3. Phần âm thanh phía sau wake phrase được giữ lại, không bắt người dùng lặp câu hỏi.
4. Ngưỡng RMS và hệ số so với tiếng nền giúp ưu tiên giọng nói rõ hơn tiếng xì xào.
5. Khi giọng chính im đủ thời gian cấu hình, robot tự kết thúc đoạn ghi âm và gửi STT.
6. Trong chế độ ngủ, wake listener dừng hoàn toàn; người dùng phải bấm nút để đánh thức.

Các thao tác nút khác:

- Bấm một cái: chuyển bước hướng dẫn; nếu đang phát nhạc thì dừng nhạc.
- Bấm nhanh hai cái: hiện tên Wi-Fi và URL quản lý trên màn hình chính trong 5 giây.
- Bấm nhanh ba cái: quên Wi-Fi cũ, phát lại `robotpi-setup` và hiện màn hình cấu hình Wi-Fi.
- Đang đọc TTS mà bấm nút: robot im ngay để người dùng chen ngang.

Ưu điểm:

- Nhận wake phrase cục bộ, không gửi âm thanh liên tục lên Internet và không tốn token AI.
- Không cần Chromium, dùng ít RAM hơn.
- Vẫn giữ nút bấm làm phương án chắc chắn trong môi trường ồn.
- Người dùng thấy rõ trạng thái đang nghe trên cả TFT và OLED.

## 20. Audio/TTS hiện tại

TTS hỗ trợ nhiều provider theo thứ tự fallback; cấu hình phổ biến dùng 3FTool trước và OpenAI-compatible/OpenAI sau:

```env
ROBOT_AUDIO_ENABLED=1
ROBOT_TTS_BASE_URL=https://tts.3ftool.com/v1
ROBOT_TTS_API_KEY=[redacted]
ROBOT_TTS_PROVIDER=auto
ROBOT_TTS_VOICE=vi_female_thuytrang_mb
ROBOT_TTS_RESPONSE_FORMAT=mp3
ROBOT_TTS_MAX_CHARS=320
```

Luồng phát âm:

```text
answer -> provider TTS -> MP3/stream -> mpg123 hoặc mpv -> output tự động hiện tại
```

Nếu `answer` dài hơn `ROBOT_TTS_MAX_CHARS`, robot tự chia theo câu. Giới hạn này áp dụng cho 3FTool/EverAI hoặc provider OpenAI-compatible; OpenAI chính thức dùng streaming TTS và giới hạn nội bộ 4096 ký tự/lượt. Khi audio đoạn đầu tạo xong thì phát ngay; trong lúc đoạn đầu đang phát, daemon tiếp tục gọi TTS cho đoạn tiếp theo và xếp hàng phát nối tiếp.

API TTS 3FTool dùng:

- `POST https://tts.3ftool.com/v1`
- Header `Authorization: Bearer ...` hoặc `X-API-Key: ...`
- Body JSON gồm `text`, `speaker_id`, `provider`, `audio_type`, `speed`, `pitch`, `volume`

Web UI có nút test loa để kiểm tra TTS mà không cần hỏi AI.

Các câu thông báo trạng thái dùng TTS API và được cache cố định trong:

```text
/home/pi/robot/status_audio/
```

Các câu đang cấu hình:

- robot đang khởi động,
- đã có kết nối Wi-Fi,
- không tìm thấy Wi-Fi và cần vào AP setup,
- robot sẵn sàng/giới thiệu dự án,
- chuẩn bị chụp ảnh: “Tôi đang chụp ảnh, hãy hướng camera về nơi bạn cần chụp”,
- đã lưu cấu hình,
- đã thay đổi thiết bị âm thanh,
- robot gặp lỗi.

Khi đổi nội dung hoặc đổi giọng đọc trong web admin, người dùng bấm **Regenerate audio** để tạo lại file audio. Daemon cũng tự regenerate khi lưu cấu hình liên quan đến câu trạng thái hoặc thông số TTS.

Âm khi chụp/suy nghĩ hiện gồm:

- trước khi chụp: câu TTS cache được phát bằng thread nền, daemon tạm dừng preview camera nếu đang mở rồi gọi `rpicam-still`,
- trước khi vào chế độ ngủ: câu TTS cache được phát xong rồi daemon dừng audio và wake listener; web admin vẫn chạy,
- sau khi ảnh được tạo: âm shutter local,
- trước khi gửi AI/suy nghĩ: âm báo ngắn local. Âm này cũng dùng cho luồng nút bấm/micro khi người dùng thả nút và robot bắt đầu gửi audio lên AI để lấy text.

## 21. Điểm sáng tạo để trình bày

1. Robot học tập giá rẻ dùng Raspberry Pi Zero 2 W.
2. Chạy headless, không cần màn hình HDMI.
3. Kết hợp camera CSI + AI vision để đọc bài thật trong vở/sách/tài liệu.
4. Thiết kế màn hình linh hoạt: bản giá rẻ chỉ cần OLED; bản nâng cấp dùng TFT lớn và tái sử dụng OLED làm mắt/đồng hồ.
5. Hai màn hình đồng bộ chớp/liếc, giúp robot sinh động nhưng vẫn phù hợp giới hạn CPU Pi Zero 2 W.
6. Nội dung công thức trên màn hình dùng ký hiệu toán học, còn nội dung đọc loa được chuyển thành lời để TTS đọc tự nhiên.
7. Wake phrase “robot ơi” chạy offline; không gửi mic liên tục lên API và không tốn token khi chờ.
8. Có bộ nhớ hội thoại giống chat, bao gồm mô tả/OCR ảnh để hỏi tiếp mà không cần chụp lại ngay.
9. Tự nhận biết khi nào cần chụp ảnh thay vì bắt người dùng luôn ra lệnh camera.
10. Có chế độ hướng dẫn bài tập từng bước, phù hợp học sinh tự luyện.
11. Âm thanh tự động: kết nối loa Bluetooth nào dùng ngay loa đó, tự ngắt loa cũ và fallback USB khi mất kết nối.
12. Lệnh mở nhạc được hiểu cục bộ, không gọi AI chỉ để tách tên bài.
13. Có provider/fallback theo từng tác vụ và prompt/luật camera chỉnh được qua web admin.
14. Câu thông báo trạng thái được tạo sẵn bằng TTS cache để robot phản hồi nhanh hơn.

## 22. Hạn chế hiện tại

| Hạn chế | Tình trạng |
| --- | --- |
| AI/STT/TTS động phụ thuộc Internet | Mất Internet vẫn cấu hình được mạng và dùng chức năng cục bộ, nhưng chưa hỏi AI/đọc TTS động |
| Wake phrase trong lớp quá ồn | Có ngưỡng RMS/tiếng nền và nút bấm dự phòng; vẫn cần nói rõ gần micro |
| OCR phụ thuộc chất lượng ảnh | Cần đủ sáng, trang ít nghiêng, camera lấy nét |
| AI có thể sai | Học sinh/giáo viên cần kiểm tra lại lời giải |
| TFT 480×320 cần truyền nhiều dữ liệu | Hoạt ảnh chính giới hạn khoảng 8 FPS và cập nhật riêng vùng mắt để giảm tải CPU |
| Backlight TFT không tắt bằng phần mềm hiện tại | Chế độ ngủ vẽ toàn bộ màn hình màu đen |
| Dự báo thời tiết cần Internet | Khi lỗi mạng, OLED vẫn giữ đồng hồ và dùng dữ liệu gần nhất nếu có |

## 23. Checklist hiện trạng

- [x] Boot headless, không cần màn hình HDMI.
- [x] SSH được vào Pi.
- [x] Wi-Fi 2.4 GHz hoạt động.
- [x] Camera OV5647 nhận đúng.
- [x] Chụp ảnh bằng `rpicam-still`.
- [x] OLED SH1106 I2C hoạt động.
- [x] OLED hiển thị tiếng Việt có dấu.
- [x] TFT 4 inch ST7796S 480×320 SPI hoạt động.
- [x] Web admin chuyển được giữa OLED-only và TFT + OLED.
- [x] OLED phụ hiển thị mắt, đồng hồ, thời tiết và ngày âm lịch.
- [x] Chớp/liếc mắt OLED và TFT đồng bộ khi chờ.
- [x] AI text hoạt động.
- [x] AI vision hoạt động.
- [x] JSON `answer/display/image_description`.
- [x] JSON mở rộng `mode/steps` cho hướng dẫn từng bước.
- [x] Bộ nhớ hội thoại.
- [x] Tự cắt hội thoại dài.
- [x] Tự chụp ảnh theo ý định câu hỏi.
- [x] Loa Bluetooth pair/connect/disconnect/forget qua web.
- [x] Kết nối loa mới tự chọn output và tự ngắt loa Bluetooth cũ.
- [x] Ngắt Bluetooth tự fallback về USB soundcard và phát thông báo đổi output.
- [x] Mic USB soundcard hoạt động cho wake listener và ghi âm nút bấm.
- [x] Web UI không phụ thuộc Chrome STT/TTS.
- [x] Service daemon tự chạy khi cấp nguồn.
- [x] Web UI quản lý Wi-Fi, AI, TTS.
- [x] Web UI quản lý AI1-AI4, fallback theo tác vụ, prompt, luật camera.
- [x] Web UI quản lý audio trạng thái và regenerate cache.
- [x] Web UI quản lý Bluetooth đơn giản, không bắt chọn lại input/output kỹ thuật.
- [x] USB sound card được Pi nhận ở cả playback và capture.
- [x] Nút bấm GPIO đã có logic giữ để ghi âm, một click chuyển bước/dừng nhạc, hai click hiện URL 5 giây, ba click reset Wi-Fi setup.
- [x] Wake phrase “robot ơi” nhận diện offline và tự dừng sau im lặng.
- [x] Màn hình hiện “Đang nghe” khi wake phrase kích hoạt.
- [x] Wake listener dừng trong chế độ ngủ; web admin vẫn truy cập được.
- [x] Trang hỏi đáp web hiển thị lịch sử và mô tả ảnh.
- [x] Mô tả ảnh được đưa lại vào lịch sử request AI và bị xóa cùng message cũ.
- [x] Parser lệnh nhạc cục bộ hỗ trợ nhiều cách nói; không còn dance/GPIO động cơ.
- [x] AP fallback khi không có Wi-Fi.
- [x] TTS API 3FTool đã cấu hình.
- [x] Âm báo chụp ảnh và suy nghĩ.
- [x] CLI/web có nút/lệnh chuyển bước hướng dẫn.
- [ ] Test thực tế TTS dài và chọn voice tối ưu.
- [ ] Đo thời lượng pin/nguồn và nhiệt độ khi trình diễn liên tục nhiều giờ.

## 24. Bảo mật

Không đưa vào báo cáo công khai:

- API key thật trong `.env`.
- Mật khẩu Wi-Fi thật.
- Ảnh bài vở có thông tin cá nhân.
- Log hội thoại riêng tư.

Khi in báo cáo, dùng dạng:

```text
OPENAI_API_KEY=[redacted]
Wi-Fi password=[redacted]
```

## 25. Tài liệu liên quan

- `MO_TA_SAN_PHAM_DU_THI.md`: bản mô tả ngắn dùng cho hồ sơ dự thi.
- `robot/README.md`: hướng dẫn nhanh các lệnh robot.
- `raspi-headless-bootfs/README.md`: hướng dẫn cài OS headless.
- `systemd/robot.service`: service daemon tự chạy.
- `robot/.env.example`: cấu hình mẫu không có key thật.
- `BAO_CAO_ROBOT_HOC_TAP.md`: tài liệu báo cáo tổng hợp.
