猜拳機器人
實作 小組電子電路 期末小組專題報告 猜拳機器人
背景
搞不懂影像辨識的運作原理、影像辨識在生活中有什麼應用?或是不知道Arduino能做什麼,覺得Arduino很無聊,沒什麼用處?怕跟不上最新AI發展,被時代淘汰嗎?
以上一連串的問題都能輕鬆解決!我們打造出一款好玩、有趣又能學到Arduino和影像辨識的「RPS AI猜拳機器人」!
這是一款能和它玩剪刀石頭布的機器人,透過手機影像辨識技術,RPS AI猜拳機器人能「看見」你出了剪刀、石頭還是布,然後判斷誰輸誰贏。
用到的模組及功能
選用開發板:TTGO T-Display
| 功能 | 模組功能 | 模組 | 通訊/腳位 |
|---|---|---|---|
| 偵測是否有人靠近 | 超音波測距 | HC-SR04 | 25,26 |
| 播放音效 | 發出聲響 | 有源蜂鳴器 | 2 |
| 顯示機器人出拳及狀態 | 顯示資訊 | TFT螢幕 | 附於TTGO T-Display |
| 和手機連線 | 藍牙通訊 | BLE模組 | 附於TTGO T-Display |
系統架構及介面
以下是系統架構圖
使用者介面

系統介紹
手機 App 設計
這個應用程式使用了Ionic移動框架。通過Capacitor js套件,我們可以快速獲得原型並使用網頁技術。利用這個技術堆疊,我們受益匪淺,還能夠使用藍牙連接來連接TTGO T-Display微控制器。
由於我們選擇使用網頁技術來實現這個應用程式,我們可以使用像Mediapipe這樣的套件來進行手勢識別工作。整合該套件後,應用程式能夠辨識你的手勢並在檢測到後執行相應操作。
ESP32 開發板
這次我們使用的MCU是TTGO T-Display。這使我們可以提供一個緊湊的設備,因為它自帶一個小型TFT螢幕。ESP32板的一個好處是我們有藍牙連接,不需要再購買另一個藍牙模塊。
然後,為了讓機器人檢測是否有手靠近以開始遊戲,我們選擇集成HC-SR04模塊。一旦傳感器檢測到距離小於20公分,ESP32將改變BLE連線的值,以便應用程式知道玩家想要再次開始遊戲。
我們還在電路中加入了一個蜂鳴器,以便當機器人贏或輸時發出一些聲音。
使用流程



部分程式碼
開發板程式碼
Setup
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <TFT_eSPI.h>
#include <Ultrasonic.h>
#define SERVICE_UUID ""
#define CHARACTERISTIC_UUID ""
String robotStatus = "boot";
String robotInPleaseStart = "false";
String receivedTemp = "";
int buzzerPin = 2;
Ultrasonic ultrasonic(25, 26);
long cm;
TFT_eSPI tft = TFT_eSPI();
// import images
#include "img1.h"
#include "img2.h"
#include "img3.h"
BLECharacteristic *pCharacteristic;
void setup() {
Serial.begin(115200);
pinMode(buzzerPin, OUTPUT);
tft.begin();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
tft.setFreeFont(&FreeSerifBold24pt7b);
tft.setCursor(30, 60);
tft.setTextColor(TFT_WHITE);
tft.setTextSize(1);
tft.printf("Ready to connect");
Serial.println("Starting BLE work!");
BLEDevice::init("RPS Hand");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharacteristic->setValue("boot");
pService->start();
// BLEAdvertising *pAdvertising = pServer->getAdvertising(); // this still is working for backward compatibility
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("Characteristic defined! Now you can read it in your phone!");
}
Loop
void loop() {
std::string value = pCharacteristic->getValue();
Serial.print("The new characteristic value is: ");
Serial.println(value.c_str());
receivedTemp = String(value.c_str());
if (receivedTemp != robotStatus) {
if (receivedTemp == "show0"){
tft.fillScreen(TFT_BLACK);
tft.pushImage(50, 0, 140, 135, img1);
} else if (receivedTemp == "show1"){
tft.fillScreen(TFT_BLACK);
tft.pushImage(50, 0, 140, 135, img2);
} else if (receivedTemp == "show2"){
tft.fillScreen(TFT_BLACK);
tft.pushImage(50, 0, 140, 135, img3);
} else if (receivedTemp == "test string"){
tft.fillScreen(TFT_BLACK);
digitalWrite(buzzerPin, HIGH);
delay(300);
digitalWrite(buzzerPin, LOW);
delay(500);
pCharacteristic->setValue("idle");
} else if (receivedTemp == "idle") {
// idle
tft.fillScreen(TFT_BLACK);
}
robotStatus = receivedTemp;
}
cm = ultrasonic.read();
Serial.println(cm);
if (cm <= 20){
pCharacteristic->setValue("pleaseStart");
}
delay(200);
}
APP程式碼
藍牙連接
export async function connect(device): Promise<void> {
if (device === null || device === undefined) return
try {
await BleClient.initialize();
// connect to device, the onDisconnect callback is optional
await BleClient.connect(device.deviceId, (deviceId) => onDisconnect(deviceId));
console.log('connected to device', device);
} catch (error) {
console.error(error);
}
}
export async function read(device){
const result = await BleClient.read(device.deviceId, ESP32_SERVICE, ESP32_CHARACTERISTIC);
return result
}
開始遊戲
const startGame = () => {
setShowGameResult(false);
console.log("開始猜拳");
const computerPlayer = Math.floor(Math.random() * 3);
if (!countDown) {
setComputerPlayerResult(computerPlayer);
setCountDown("3");
setTimeout(() => {
setCountDown("2");
}, 1000);
setTimeout(() => {
setCountDown("1");
}, 2000);
setTimeout(() => {
console.log(resultRef.current);
if (resultToNum(resultRef.current) === null) {
setCountDown("出拳失敗");
setTimeout(() => {
setCountDown(null);
}, 1500);
} else {
const playerResultNow = resultToNum(resultRef.current);
setPlayerResult(playerResultNow);
const whoWin = whoWinNumHnadler(playerResultNow, computerPlayer);
setCountDown(numToWhoWinStr(whoWin));
setShowGameResult(true);
setTimeout(() => {
setCountDown(null);
}, 1000);
}
}, 3000);
}
};