웹블루투스 기반 자료
동작 테스트
구성도
•
2개 서보모터 피코 3번, 5번 연결
피코측 코드
전체 프로젝트 파일
아래 폴더 다운로드 후 압축 해제
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 모델 웹사이트 제작
•
최적화 문제