과제 목적
제목: 손으로 인식하고 작동하는 자동차
이번 과제의 목적으로는 손으로 방향키를 움직여 RC카가 작동하는 걸 목적으로 실습을 했습니다.
최종 프로젝트 영상
프로젝트 코드
codepen.io 코드
<!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>
HTML
복사
마이크로비트 코드
기타 자료
사진 자료
구동 영상
느낀점
학교에서는 마이크로비트만, 그리고 몇 센서들로만 사용을 해봤었습었는데, 이번 생성형 Ai와 메이커 실습으로 레고를 통해 만들어볼수 있어서 좋은 시간이 되었던거 같습니다.
처음에 키트를 받고 다음 1주?가 되자마자 한쪽 모터가 고장이 나, 제대로 키트를 활용하지 못한점이 아쉬웠지만, 마지막에 친구에게 모터를 빌려 만들고 싶은걸 만들어봐서 좋았습니다.
마지막 프로젝트에서 여러 문제점들이 있었습니다. 처음으로는 codepen에서 인식을 해 마이크로비트에 전달을 해야했는데 전달이 안된점이 있었습니다. 그 문제의 원인은 마이크로비트만 받게 해야하는데 블루투스 모두로 코딩을 해서 chatGPT을 써서 해결을 했었지만, 다음 문제는 WASD를 신호를 보내는 과정에서 wasd로 마이크로비트로 만들었는데 codepen 에서는 WASD로 보내게 만들어서 문제가 있었습니다. 다행이 빠르게 알아차려서 해결했고, 잘 작동이 됬지만, 결국에는 해결 못한 2가지가 있었습니다. 1번째는 딜래이가 있었습니다. 바로 신호를 받게 해주고 싶었지만, 딜래이가 있었고, 이건 해결을 못했습니다. 그리고 2번째는 그 딜래이를 해결하려고 많은 시도를 했더니 노트북에서 codepen 프로그램이 작동이 안된점이 아쉬웠습니다. 그래서 선생님의 폰을 빌려 마무리를 하게 되었습니다.
그래도 잘 작동을 한점과 codepen과 마이크로비트가 잘 신호를 주는 점이 인상 깊었습니다.
다음에는 기회가 된다면, 원래는 넣으려고 했던 기능, 충동 방지 기능과 자율 주행 기능을 넣고 싶고 해결 못한 딜래이 문제를 해결하고 싶습니다.








