메인
home
소프트웨어
home
🚏

2-7. 피코 + 웹 블루투스 + 미디어파이프 연동

웹블루투스 기반 자료

동작 테스트

구성도

2개 서보모터 피코 3번, 5번 연결

피코측 코드

전체 프로젝트 파일
아래 폴더 다운로드 후 압축 해제
pico_bluetooth_servo (2).zip
5.2KB
main.py
from machine import Pin, PWM import bluetooth import time from ble_simple_peripheral import BLESimplePeripheral # 간단한 큐 클래스 정의 class SimpleQueue: def __init__(self, maxsize): self.queue = [] self.maxsize = maxsize def put(self, item): if len(self.queue) < self.maxsize: self.queue.append(item) else: raise OverflowError("Queue is full") def get(self): if len(self.queue) > 0: return self.queue.pop(0) else: raise IndexError("Queue is empty") def empty(self): return len(self.queue) == 0 def full(self): return len(self.queue) >= self.maxsize # BLE 초기화 ble = bluetooth.BLE() sp = BLESimplePeripheral(ble) # 서보모터 핀 초기화 servo1 = PWM(Pin(3)) # 오른손 서보모터 servo2 = PWM(Pin(5)) # 왼손 서보모터 servo1.freq(50) servo2.freq(50) # 데이터 처리 큐 data_queue = SimpleQueue(maxsize=10) # 각도를 서보모터의 듀티 값으로 변환 def angle_to_duty(angle): return int(3277 + (angle * 3277 / 90)) # 0° = 3277 (~0.5ms), 180° = 6553 (~2.5ms) # 서보모터 초기화 def initialize_servos(): initial_angle = 90 duty = angle_to_duty(initial_angle) servo1.duty_u16(duty) servo2.duty_u16(duty) print(f"Servos initialized to {initial_angle} degrees") THRESHOLD = 3 # 최소 각도 변화 임계값 last_angle = {"right": 90, "left": 90} # 마지막 각도 저장 # 서보모터 부드러운 움직임 처리 def move_servo_smooth(servo, current_angle, target_angle, side): global last_angle if abs(target_angle - current_angle) <= THRESHOLD: return # 각도 변화가 작으면 동작 생략 step = 1 if current_angle < target_angle else -1 for angle in range(current_angle, target_angle + step, step): duty = angle_to_duty(angle) servo.duty_u16(duty) time.sleep(0.002) # 2ms 간격으로 이동 last_angle[side] = target_angle print(f"{side.capitalize()} servo moved smoothly to {target_angle} degrees") # 수신 데이터 처리 def process_data(): global last_angle while not data_queue.empty(): try: data = data_queue.get() print("Processing data:", data) if data.startswith(b'R:'): angle = int(data[2:]) if 60 <= angle <= 110: # 60도 ~ 110도 제한 current_angle = last_angle["right"] if last_angle["right"] is not None else angle move_servo_smooth(servo1, current_angle, angle, "right") elif data.startswith(b'L:'): angle = int(data[2:]) if 60 <= angle <= 110: # 60도 ~ 110도 제한 inverted_angle = 180 - angle current_angle = last_angle["left"] if last_angle["left"] is not None else inverted_angle move_servo_smooth(servo2, current_angle, inverted_angle, "left") except Exception as e: print("Error processing data:", e) # BLE 데이터 수신 핸들러 def on_rx(data): try: print("Received:", data) if not data_queue.full(): data_queue.put(data) # 데이터를 큐에 저장 else: print("Data queue is full, dropping data") except Exception as e: print("Error receiving data:", e) # 서보 초기화 initialize_servos() # 메인 루프 while True: if sp.is_connected(): sp.on_write(on_rx) # BLE 데이터 수신 process_data() # 큐에 있는 데이터를 처리 else: print("Waiting for BLE connection...") time.sleep(0.1) # 연결되지 않은 경우 대기
Python
복사

미디어파이프 웹 블루투스 사이트 링크

위 사이트 서버 : neflify 이용
index.html 코드
<!DOCTYPE html> <html> <head> <title>Raspberry Pico Web Bluetooth with MediaPipe Hand Detection</title> <style> body { font-family: Arial, sans-serif; } .container { max-width: 500px; margin: 0 auto; padding: 20px; } .button { display: inline-block; padding: 10px 20px; font-size: 16px; background-color: #4CAF50; color: #fff; border: none; cursor: pointer; } .button:hover { background-color: #45a049; } #outputCanvas { display: block; margin: 20px auto; border: 1px solid black; } pre { background-color: #f4f4f4; padding: 10px; border: 1px solid #ddd; border-radius: 5px; max-height: 200px; overflow-y: auto; } </style> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.min.js"></script> </head> <body> <div class="container"> <h2>Raspberry Pico Web Bluetooth with MediaPipe Hand Detection</h2> <h3>제작자 : 성원경(상암고 교사) & chatGPT 4o / 24.11.23</h3> <button id="connectButton" class="button">Connect</button> <button id="disconnectButton" class="button" disabled>Disconnect</button> </div> <video id="camera" autoplay playsinline style="display:none;"></video> <canvas id="outputCanvas" width="640" height="480"></canvas> <pre id="log"></pre> <script> let device; let writeCharacteristic; let lastSentAngles = { right: null, left: null }; // 각 손의 마지막 송신 각도 저장 let isSending = { right: false, left: false }; // 각 손의 송신 상태 관리 let sendQueue = []; // 전송 큐 const videoElement = document.getElementById('camera'); const canvasElement = document.getElementById('outputCanvas'); const canvasCtx = canvasElement.getContext('2d'); const hands = new Hands({ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}` }); hands.setOptions({ maxNumHands: 2, modelComplexity: 1, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 }); const log = (message) => { const logElement = document.getElementById('log'); logElement.innerHTML += message + '\n'; }; async function connectToDevice() { try { log('Requesting Bluetooth Device...'); device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e'] }); log('Connecting to GATT Server...'); const server = await device.gatt.connect(); log('Getting Serial Service...'); const service = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e'); log('Getting Write Characteristic...'); writeCharacteristic = await service.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e'); log('Connected to ' + device.name); document.getElementById('connectButton').disabled = true; document.getElementById('disconnectButton').disabled = false; device.addEventListener('gattserverdisconnected', () => { log('Device disconnected'); document.getElementById('connectButton').disabled = false; document.getElementById('disconnectButton').disabled = true; writeCharacteristic = null; }); } catch (error) { log('Error: ' + error); } } function disconnectFromDevice() { if (device && device.gatt.connected) { device.gatt.disconnect(); log('Disconnected from device'); document.getElementById('connectButton').disabled = false; document.getElementById('disconnectButton').disabled = true; } } async function sendMessage(hand, message) { if (isSending[hand]) return; isSending[hand] = true; try { const encoder = new TextEncoder(); await writeCharacteristic.writeValue(encoder.encode(message)); log(`Sent (${hand}): ${message}`); } catch (error) { log(`Error sending ${hand} message: ${error}`); } finally { isSending[hand] = false; } } const mapRange = (value, inMin, inMax, outMin, outMax) => { return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; }; hands.onResults((results) => { canvasCtx.save(); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); canvasCtx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height); if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { results.multiHandLandmarks.forEach((landmarks, index) => { drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, { color: '#00FF00', lineWidth: 5 }); drawLandmarks(canvasCtx, landmarks, { color: '#FF0000', lineWidth: 2 }); const thumb = landmarks[4]; const indexFinger = landmarks[8]; // 엄지와 검지를 잇는 선 그리기 canvasCtx.beginPath(); canvasCtx.moveTo(thumb.x * canvasElement.width, thumb.y * canvasElement.height); canvasCtx.lineTo(indexFinger.x * canvasElement.width, indexFinger.y * canvasElement.height); canvasCtx.strokeStyle = 'blue'; canvasCtx.lineWidth = 5; canvasCtx.stroke(); // 거리 계산 -> 각도로 변환 const distance = Math.sqrt( Math.pow(indexFinger.x - thumb.x, 2) + Math.pow(indexFinger.y - thumb.y, 2) ); const clampedDistance = Math.max(0.3, Math.min(distance, 0.6)); const mappedAngle = Math.round(mapRange(clampedDistance, 0.3, 0.6, 60, 120)); const handLabel = results.multiHandedness[index]?.label; // 각도 텍스트 표시 canvasCtx.font = '24px Arial'; canvasCtx.fillStyle = 'black'; const textX = thumb.x * canvasElement.width; const textY = thumb.y * canvasElement.height - 10; canvasCtx.fillText(`${handLabel}: ${mappedAngle}°`, textX, textY); // 송신 조건 확인 const shouldSendMessage = lastSentAngles[handLabel.toLowerCase()] === null || // 초기 상태 Math.abs(mappedAngle - lastSentAngles[handLabel.toLowerCase()]) >= 5 || // 변화량이 충분한 경우 mappedAngle === 60 || mappedAngle === 120; // 주요 각도 강제 송신 if (shouldSendMessage) { const message = `${handLabel === "Right" ? "R" : "L"}:${mappedAngle}`; sendMessage(handLabel.toLowerCase(), message); lastSentAngles[handLabel.toLowerCase()] = mappedAngle; log(`${handLabel}: ${mappedAngle}° (Sent)`); } else { log(`${handLabel}: ${mappedAngle}° (Not Sent)`); } }); } canvasCtx.restore(); }); async function startCamera() { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); videoElement.srcObject = stream; videoElement.play(); } startCamera(); const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({ image: videoElement }); }, width: 640, height: 480 }); camera.start(); document.getElementById('connectButton').addEventListener('click', connectToDevice); document.getElementById('disconnectButton').addEventListener('click', disconnectFromDevice); </script> </body> </html>
HTML
복사

gpt와의 대화 일부분

앞으로 해결문제

아이폰 연결 문제
다양한 AI 모델 웹사이트 제작
최적화 문제

참고자료