1️⃣ 과제 목적

제목: 손으로 인식하고 작동하는 자동차

이번 과제의 목적으로는 손으로 방향키를 움직여 RC카가 작동하는 걸 목적으로 실습을 했습니다.

2️⃣ 최종 프로젝트 영상

https://www.canva.com/design/DAG6D9AK3-U/jA28flTIhAquSncEpsEAQg/edit?utm_content=DAG6D9AK3-U&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton

3️⃣ 프로젝트 코드

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>

마이크로비트 코드

microbit-스크린샷.png

4️⃣ 기타 자료

사진 자료

image.png

image.png

image.png

image.png

image.png