제목: 손으로 인식하고 작동하는 자동차
이번 과제의 목적으로는 손으로 방향키를 움직여 RC카가 작동하는 걸 목적으로 실습을 했습니다.
codepen.io 코드
https://codepen.io/eungung0224/pen/KwzXEdV
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>손가락 WASD 컨트롤러 → micro:bit</title>
<script src="<https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.min.js>"></script>
<script src="<https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js>"></script>
<script src="<https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js>"></script>
<style>
body { font-family:'Pretendard','Noto Sans KR',sans-serif; display:flex; flex-direction:column; align-items:center; background:#eef2f7; min-height:100vh; padding:1rem; }
h1{margin:0.5rem;font-size:1.6rem;font-weight:700;}
#status{margin:0.5rem;font-weight:600;}
#sent,#received{
margin:.3rem 0;padding:.4rem .8rem;
border-radius:6px;background:#fff;
font-size:0.95rem;width:260px;text-align:left; box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
#sent{border-left:4px solid #4caf50;}
#received{border-left:4px solid #2196f3;}
button{
margin:.3rem;padding:.6rem 1.2rem;border:none;border-radius:10px;
background:#4caf50;color:white;font-weight:bold;cursor:pointer;
}
button:disabled{background:#ccc;cursor:not-allowed;}
canvas{border-radius:12px;box-shadow:0 0 10px rgba(0,0,0,0.2); margin:1rem;}
video{display:none;}
</style>
</head>
<body>
<h1>🖐 WASD 손가락 컨트롤러</h1>
<div id="status">미연결</div>
<div id="sent">HTML → micro:bit: –</div>
<div id="received">micro:bit → HTML: –</div>
<div>
<button id="connectButton">🔗 Connect</button>
<button id="disconnectButton" disabled>🔒 Disconnect</button>
</div>
<video class="input_video" autoplay playsinline></video>
<canvas class="output_canvas" width="400" height="300"></canvas>
<script>
const UART_SERVICE_UUID='6e400001-b5a3-f393-e0a9-e50e24dcca9e';
let device, txChar, rxChar, isConnected=false;
let lastSignal='', lastSendTime=0;
const statusEl=document.getElementById('status');
const sentEl=document.getElementById('sent');
const receivedEl=document.getElementById('received');
const btnConnect=document.getElementById('connectButton');
const btnDisc=document.getElementById('disconnectButton');
function logStatus(msg){ statusEl.textContent=msg; }
function logSent(cmd){ sentEl.textContent=`HTML → micro:bit: ${cmd}`; }
async function send(cmd){
const now = performance.now();
if(!isConnected || !txChar) return;
if(cmd===lastSignal && now - lastSendTime < 50) return; // 50ms 제한
lastSignal = cmd;
lastSendTime = now;
const data = new TextEncoder().encode(cmd+'\\n');
try{
await txChar.writeValueWithoutResponse(data);
logSent(cmd);
}catch(e){
logStatus('⚠️ 전송 오류: '+e.message);
}
}
async function connectMicrobit(){
try{
logStatus('🔍 micro:bit 검색 중…');
device = await navigator.bluetooth.requestDevice({
filters:[{namePrefix:'BBC micro:bit'}],
optionalServices:[UART_SERVICE_UUID]
});
device.addEventListener('gattserverdisconnected', async ()=>{
logStatus('⚠️ 연결 끊김, 재연결 시도…');
isConnected=false;
btnConnect.disabled=false;
btnDisc.disabled=true;
await reconnectMicrobit();
});
const server = await device.gatt.connect();
const svc = await server.getPrimaryService(UART_SERVICE_UUID);
const chars = await svc.getCharacteristics();
chars.forEach(ch=>{
const p = ch.properties;
if((p.write || p.writeWithoutResponse) && !txChar) txChar = ch;
if((p.notify || p.indicate) && !rxChar) rxChar = ch;
});
if(rxChar){
rxChar.addEventListener('characteristicvaluechanged', e=>{
const v = new TextDecoder().decode(e.target.value).trim();
receivedEl.textContent=`micro:bit → HTML: ${v}`;
});
await rxChar.startNotifications();
}
isConnected = true;
btnConnect.disabled = true;
btnDisc.disabled = false;
logStatus('✅ 연결 완료');
}catch(e){
logStatus('❌ 연결 실패: '+e.message);
}
}
async function reconnectMicrobit(){
try{
if(!device) return;
const server = await device.gatt.connect();
const svc = await server.getPrimaryService(UART_SERVICE_UUID);
const chars = await svc.getCharacteristics();
chars.forEach(ch=>{
const p = ch.properties;
if((p.write || p.writeWithoutResponse) && !txChar) txChar = ch;
if((p.notify || p.indicate) && !rxChar) rxChar = ch;
});
if(rxChar) await rxChar.startNotifications();
isConnected = true;
btnConnect.disabled = true;
btnDisc.disabled = false;
logStatus('🔄 재연결 성공');
}catch(e){
logStatus('❌ 재연결 실패: '+e.message);
}
}
btnConnect.addEventListener('click', connectMicrobit);
btnDisc.addEventListener('click', ()=>{
if(device?.gatt.connected) device.gatt.disconnect();
isConnected=false;
btnConnect.disabled=false;
btnDisc.disabled=true;
logStatus('🔌 연결 해제됨');
});
// ===== MediaPipe Hands =====
const videoElement=document.querySelector('.input_video');
const canvasElement=document.querySelector('.output_canvas');
const ctx=canvasElement.getContext('2d');
const hands = new Hands({locateFile:(file)=>`https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
hands.setOptions({ maxNumHands:1, modelComplexity:1, minDetectionConfidence:0.6, minTrackingConfidence:0.6 });
hands.onResults(onResults);
const camera = new Camera(videoElement,{
onFrame: async ()=>{ await hands.send({image:videoElement}); },
width:400,height:300
});
camera.start();
// ===== WASD 버튼 정의 (소문자 전송) =====
const buttons = [
{x:150,y:20,w:100,h:60,label:'w'},
{x:50,y:100,w:100,h:60,label:'a'},
{x:150,y:100,w:100,h:60,label:'s'},
{x:250,y:100,w:100,h:60,label:'d'}
];
function drawButtons(activeIndex){
buttons.forEach((b,i)=>{
ctx.fillStyle=(i===activeIndex)?'#4caf50':'rgba(255,255,255,0.7)';
ctx.strokeStyle='#333';
ctx.lineWidth=2;
ctx.fillRect(b.x,b.y,b.w,b.h);
ctx.strokeRect(b.x,b.y,b.w,b.h);
ctx.fillStyle=(i===activeIndex)?'#fff':'#000';
ctx.font='bold 28px Pretendard';
ctx.fillText(b.label.toUpperCase(), b.x+b.w/2-10, b.y+b.h/2+10);
});
}
function onResults(results){
ctx.save();
ctx.clearRect(0,0,canvasElement.width,canvasElement.height);
ctx.drawImage(results.image,0,0,canvasElement.width,canvasElement.height);
let active=-1;
if(results.multiHandLandmarks && results.multiHandLandmarks.length>0){
const lm = results.multiHandLandmarks[0];
drawConnectors(ctx,lm,HAND_CONNECTIONS,{color:'#00FF00',lineWidth:2});
drawLandmarks(ctx,lm,{color:'#FF0000',lineWidth:2});
const fx = lm[8].x*canvasElement.width;
const fy = lm[8].y*canvasElement.height;
ctx.beginPath();
ctx.arc(fx,fy,10,0,Math.PI*2);
ctx.fillStyle='rgba(255,0,0,0.6)';
ctx.fill();
buttons.forEach((b,i)=>{
if(fx>b.x && fx<b.x+b.w && fy>b.y && fy<b.y+b.h) active=i;
});
if(active>=0) send(buttons[active].label);
}
drawButtons(active);
ctx.restore();
}
</script>
</body>
</html>
마이크로비트 코드

사진 자료




