Arduino 程式碼

testMission_Arduino_Indoor.ino

#include <Stepper.h>
#include <Servo.h> 
#include "HX711.h"

// 指令:
//   PING                  // 回 PONG(測試用)
//   L:<vL> R:<vR>         // 車輛馬達控制 PWM(-255..255)
//   STOP                  // 車輛停止
//   LED <R|G|Y>           // LED亮指定色(會關掉其他色),保持亮直到 LED_STOP
//   LED_STOP              // LED全關
//   STRETCH               // 步進馬達伸5圈→收5圈(測試用)
//   STRETCH <grams>       // 步進馬達伸5圈→等重量達目標→立刻收回3圈→ 回 Orin STRETCH_OK / STRETCH_ERR
//   UP [deg]              // 伺服相對「正轉」deg(預設 15°);回 UP_OK <now_deg>
//   DOWN [deg]            // 伺服相對「反轉」deg(預設 15°);回 DOWN_OK <now_deg>

// ====== 腳位、參數設定 ======
// 左馬達
const int L_EN_FWD = 2;
const int L_EN_REV = 4;
const int L_PWM_FWD = 3;   
const int L_PWM_REV = 6;   
// 右馬達
const int R_EN_FWD = 7;
const int R_EN_REV = 8;
const int R_PWM_FWD = 5;   
const int R_PWM_REV = 9;   
// LED
const int LED_R = A5;
const int LED_G = 12;
const int LED_Y = 13;
// 步進馬達 28BYJ48 + ULN2003
const int STEPS_PER_REV = 2048;
const int STP_IN1 = A0;   // IN1
const int STP_IN2 = A1;   // IN2
const int STP_IN3 = A2;   // IN3
const int STP_IN4 = A3;   // IN4
Stepper stepperMotor(STEPS_PER_REV, STP_IN1, STP_IN3, STP_IN2, STP_IN4);
// 重量感測器 HX711
const int HX_DOUT = A4;   // DOUT
const int HX_SCK  = 11;   // SCK  
HX711 scale;
float SCALE_CAL = 599.0f;   // 校正係數(校正後更新
const float WEIGHT_TOL_G   = 3.0f;        // 允許誤差 ±g
const int   RPM_EXTEND     = 20;          // 伸出速度(RPM)
const int   RPM_RETRACT    = 20;          // 收回速度(RPM)
const unsigned long MAX_WAIT_MS = 20000;  // 等待收料的最長時間(安全逾時)
// 伺服馬達 SG90
const int SERVO_PIN            = 10; // Servo 用 Timer1;attach 期間會關掉 D9/D10 PWM
const int SERVO_INIT_DEG       = 0;  // 程式啟動預設角度(內部狀態)
const int SERVO_SETTLE_MS      = 15;  // 每 1° 的等待(平滑用)
const int SERVO_DEFAULT_DELTA  = 15;  // UP/DOWN 未給角度時的預設位移
const int SERVO_US_MIN         = 500;
const int SERVO_US_MAX         = 2400;
const int SERVO_MIN_DEG        = 0;
const int SERVO_MAX_DEG        = 180;
int servoAngle = SERVO_INIT_DEG;    // 目前記錄的伺服角度(0..180)
Servo feederServo;
// =====================

String buf;

// ------- 小工具 -------
int clampInt(int v, int lo, int hi) { if (v < lo) return lo; if (v > hi) return hi; return v; }

// ------- 馬達控制 -------
void setMotorPWM(int enFwd, int enRev, int pwmFwd, int pwmRev, int speed) {
  int pwm = abs(speed);
  if (pwm > 255) pwm = 255;

  if (speed > 0) {
    // 前進:FWD PWM 有值、REV PWM=0
    digitalWrite(enFwd, HIGH);
    digitalWrite(enRev, HIGH);
    analogWrite(pwmFwd, pwm);
    analogWrite(pwmRev, 0);
  } else if (speed < 0) {
    // 後退:REV PWM 有值、FWD PWM=0
    digitalWrite(enFwd, HIGH);
    digitalWrite(enRev, HIGH);
    analogWrite(pwmFwd, 0);
    analogWrite(pwmRev, pwm);
  } else {
    // 停止:PWM 皆 0
    digitalWrite(enFwd, HIGH);
    digitalWrite(enRev, HIGH);
    analogWrite(pwmFwd, 0);
    analogWrite(pwmRev, 0);
  }
}

void stopMotors() {
  setMotorPWM(L_EN_FWD, L_EN_REV, L_PWM_FWD, L_PWM_REV, 0);
  setMotorPWM(R_EN_FWD, R_EN_REV, R_PWM_FWD, R_PWM_REV, 0);
}

// ------- LED 控制 -------
void ledsAllOff() {
  digitalWrite(LED_R, LOW);
  digitalWrite(LED_G, LOW);
  digitalWrite(LED_Y, LOW);
}
void setLEDColor(char c) {
  ledsAllOff();
  c = toupper(c);
  if (c == 'R') digitalWrite(LED_R, HIGH);
  else if (c == 'G') digitalWrite(LED_G, HIGH);
  else if (c == 'Y') digitalWrite(LED_Y, HIGH);
}

// ------- 解析 L:<vL> R:<vR> -------
bool handleLRCommand(const String& s) {
  int lpos = s.indexOf("L:");
  int rpos = s.indexOf("R:");
  if (lpos < 0 || rpos < 0) return false;

  if (rpos < lpos) {
    int lpos2 = rpos; 
    int rpos2 = lpos;
    lpos = lpos2; 
    rpos = rpos2;
  }

  // 抓數值
  int vL = s.substring(lpos + 2, rpos).toInt();
  int vR = s.substring(rpos + 2).toInt();

  setMotorPWM(L_EN_FWD, L_EN_REV, L_PWM_FWD, L_PWM_REV, vL);
  setMotorPWM(R_EN_FWD, R_EN_REV, R_PWM_FWD, R_PWM_REV, vR);

  return true;
}

// ------- 平均取重(已 set_scale/tare) -------
float readWeightAvg(uint8_t n = 6) {
  if (!scale.is_ready()) return NAN;
  float sum = 0; int cnt = 0;
  for (uint8_t i = 0; i < n; i++) {
    float g = scale.get_units(1);
    if (!isnan(g)) { sum += g; cnt++; }
    delay(5);
  }
  return (cnt > 0) ? (sum / cnt) : NAN;
}

// ------- STRETCH <g> -------
bool stretch_to_weight(float target_g) {
  if (target_g <= 0) { Serial.println("STRETCH_ERR BAD_ARG"); return false; }
  if (!scale.is_ready()) { Serial.println("STRETCH_ERR SCALE_NOT_READY"); return false; }

  // 歸零
  scale.tare(20);
  delay(80);

  // 伸 5 圈
  stepperMotor.setSpeed(RPM_EXTEND);
  stepperMotor.step(-5 * STEPS_PER_REV);  // - 往外伸;若方向反了就把符號換成 +

  // 盯重量直到達標(目標 - 容差),一到就結束等待
  bool reached = false;
  unsigned long t0 = millis();
  while (millis() - t0 < MAX_WAIT_MS) {
    float w = readWeightAvg(4);
    if (!isnan(w) && w >= (target_g - WEIGHT_TOL_G)) {
      reached = true;
      break;               // 達標就立刻收回
    }
    delay(20);
  }

  // 收回 3 圈
  stepperMotor.setSpeed(RPM_RETRACT);
  stepperMotor.step(+3 * STEPS_PER_REV);

  // 回報結果
  if (reached) {
    Serial.println("STRETCH_OK");
    return true;
  } else {
    Serial.println("STRETCH_ERR TIMEOUT");
    return false;
  }
}

// ------- 伺服馬達相對移動(UP/DOWN 共用) -------
void servoMoveRelative(int deltaDeg, bool reportUp) {
  // 安全:停車時才轉伺服
  stopMotors();

  // 目標角度(夾在 0..180)
  long target = (long)servoAngle + (long)deltaDeg;
  target = clampInt((int)target, SERVO_MIN_DEG, SERVO_MAX_DEG);

  if ((int)target == servoAngle) {
    Serial.print(reportUp ? "UP_OK " : "DOWN_OK ");
    Serial.println(servoAngle);
    return;
  }

  int step = (target >= servoAngle) ? 1 : -1;

  // 擴脈寬範圍以利到達接近 180°
  feederServo.attach(SERVO_PIN, SERVO_US_MIN, SERVO_US_MAX);
  feederServo.write(servoAngle);
  delay(SERVO_SETTLE_MS);

  for (int a = servoAngle + step; (step > 0) ? (a <= target) : (a >= target); a += step) {
    feederServo.write(a);
    delay(SERVO_SETTLE_MS);
  }
  servoAngle = (int)target;

  feederServo.detach();                 // 釋放 Timer1

  Serial.print(reportUp ? "UP_OK " : "DOWN_OK ");
  Serial.println(servoAngle);
}

// ------- 指令處理 -------
void handleCommand(String s) {
  s.trim();
  if (!s.length()) return;

  // L:<vL> R:<vR>
  if (handleLRCommand(s)) return;

  String t = s;
  t.toUpperCase();

  if (t == "PING") {
    Serial.println("PONG");
    return;
  }

  if (t == "STOP") {
    stopMotors();
    Serial.println("STOP OK");
    return;
  }

  if (t == "LED_STOP") {
    ledsAllOff();
    Serial.println("LED_STOP OK");
    return;
  }

  // LED <R|G|Y>
  if (t.startsWith("LED ")) {
    int sp1 = s.indexOf(' ');
    if (sp1 > 0 && s.length() >= sp1 + 2) {
      char c = s.substring(sp1 + 1, sp1 + 2)[0];
      setLEDColor(c);     // 亮該色,保持亮直到 LED_STOP
      Serial.println("LED OK");
    }
    return;
  }

  // STRETCH(伸縮機構動作)
  if (t.startsWith("STRETCH")) {
    int sp1 = s.indexOf(' ');
    if (sp1 > 0) {
      // STRETCH <grams>
      float grams = s.substring(sp1 + 1).toFloat();
      stretch_to_weight(grams);  // 完成印 STRETCH_OK;逾時印 STRETCH_ERR TIMEOUT
    } else {
      // STRETCH(測試)
      stepperMotor.setSpeed(20);
      stepperMotor.step(+5 * STEPS_PER_REV); //往外伸
      stepperMotor.step(-5 * STEPS_PER_REV); //往內縮
      Serial.println("STRETCH TEST OK");
    }
    return;
  }

  // SCALE?(重量感測器測試)
  if (t == "SCALE?") {
    if (!scale.is_ready()) {
      Serial.println("WEIGHT NAN");
      return;
    }
    float w = scale.get_units(8);   // 取 8 次平均
    Serial.print("WEIGHT ");
    Serial.println(w, 1);
    return;
  }

  // UP [deg]:相對「正轉」 → 送正角度
  if (t == "UP" || t.startsWith("UP ")) {
    int delta = SERVO_DEFAULT_DELTA;
    int sp1 = s.indexOf(' ');
    if (sp1 > 0) {
      String num = s.substring(sp1 + 1); num.trim();
      if (num.length() > 0) delta = num.toInt();
    }
    delta = clampInt(delta, 0, 180);
    servoMoveRelative(+delta, true);
    return;
  }

  // DOWN [deg]:相對「反轉」 → 送負角度
  if (t == "DOWN" || t.startsWith("DOWN ")) {
    int delta = SERVO_DEFAULT_DELTA;
    int sp1 = s.indexOf(' ');
    if (sp1 > 0) {
      String num = s.substring(sp1 + 1); num.trim();
      if (num.length() > 0) delta = num.toInt();
    }
    delta = clampInt(delta, 0, 180);
    servoMoveRelative(-delta, false);
    return;
  }

  // 其它代補指令
}

void setup() {
  // 馬達腳位
  pinMode(L_EN_FWD, OUTPUT);
  pinMode(L_EN_REV, OUTPUT);
  pinMode(L_PWM_FWD, OUTPUT);
  pinMode(L_PWM_REV, OUTPUT);
  pinMode(R_EN_FWD, OUTPUT);
  pinMode(R_EN_REV, OUTPUT);
  pinMode(R_PWM_FWD, OUTPUT);
  pinMode(R_PWM_REV, OUTPUT);

  // LED 腳位
  pinMode(LED_R, OUTPUT);
  pinMode(LED_G, OUTPUT);
  pinMode(LED_Y, OUTPUT);
  ledsAllOff();

  // 步進馬達初始化 
  stepperMotor.setSpeed(20);  // RPM

  // 重量感測器
  scale.begin(HX_DOUT, HX_SCK);
  scale.set_scale(SCALE_CAL); // 校正因子
  scale.tare(20);

  // 伺服馬達(不 attach,等用到再 attach)
  servoAngle = SERVO_INIT_DEG;  // 起始 0°

  Serial.begin(9600);         
  stopMotors();               // 安全起步
}

void loop() {
  while (Serial.available()) {
    char ch = (char)Serial.read();
    if (ch == '\\n' || ch == '\\r') {
      if (buf.length() > 0) {
        handleCommand(buf);
        buf = "";
      }
    } else {
      buf += ch;
      if (buf.length() > 120) buf = ""; // 防呆
    }
  }
}


Orin(Python)程式碼

testMission_indoor.py

"""
全部移動都「寫死分段」(多段 PWM×秒數);
只有:
  1) 第一關停車用側鏡 YOLO 決定目標動物(第一個穩定)→ 亮 LED、記下重量
  2) 中間到飼料機:到點直接 STRETCH <重量>,等 STRETCH_OK。
  3) 最後一關:分段推進「邊跑邊看」目標動物 → 看到就 STOP → FEED
"""

import cv2
import time
import sys
import serial
from collections import deque
from ultralytics import YOLO

# ========= 可調參數 =========
MODEL_PATH = "best.pt"
SERIAL_PORT = "/dev/ttyACM0"
BAUDRATE    = 9600

# 只開側視相機(偵測用)
CAM_SIDE_INDEX = 4
CAM_W, CAM_H   = 640, 480

# --- 行走分段((秒, 左PWM, 右PWM)) ---
# 起點 → 動物區:跑完後停下來辨識
PRE_TO_ANIMAL_SEGMENTS = [
    (12, 113, 50), # 12秒 113 50
]

# 動物區 → 飼料機:跑完後停下來,直接 STRETCH <重量>
ANIMAL_TO_FEEDER_SEGMENTS = [
    (18.5, 115, 50), # 18.5秒 115 50
]

# 飼料機 → 投餵區(最後一關):邊跑邊看指定動物,看到就停車 FEED
FEEDER_TO_FEEDZONE_SEGMENTS = [
    (3.0, 120, 120),
    (2.0, 100, 100),
]

# 出場(可自行配置)
EXIT_SEGMENTS = [
    # (1.0, -120, -120),
    # (2.0,  140,  140),
]

# 類別/LED/重量
YOLO_CLASS_ROLE = {"chicken":"CHICKEN", "cows":"COW", "pig":"PIG", "machine":"FEEDER"}
ALIASES = {"cow":"cows", "feeder":"machine"}
ANIMAL_ROLES = ("CHICKEN", "PIG", "COW")
ROLE_TO_LED    = {"CHICKEN":"G", "PIG":"Y", "COW":"R"}
ROLE_TO_WEIGHT = {"CHICKEN":70,  "PIG":155, "COW":205}

# 偵測穩定條件
DET_CONF_TH = 0.50
DET_STABLE_FRAMES = 5      # 第一關定案幀數
MAX_DET_WAIT_SEC  = 5.0

# 最後一關「邊跑邊看」觸發條件
FINAL_SEE_CONF_TH   = 0.50
FINAL_SEE_STABLE_FR = 5     # 連續幀數達標就停
FINAL_SEE_MIN_WIDTH = 0     # >0 才檢查 bbox 寬度門檻(像素)
# ===========================

# ===== 工具 =====
def clamp(v, lo, hi): return max(lo, min(hi, v))

def normalize_classname(name: str):
    k = name.strip().lower()
    if k in ALIASES: k = ALIASES[k]
    return k

def bbox_center_w(b):
    x1,y1,x2,y2 = map(int, b)
    cx = (x1+x2)//2
    w  = (x2-x1)
    return cx, w

# ===== 串列 =====
class SerialClient:
    def __init__(self, port, baud):
        self.ser = serial.Serial(port, baud, timeout=0.05)
        time.sleep(2.0)  # 等 Arduino reset
    def send(self, line):
        self.ser.write((line.strip()+"\\n").encode("utf-8"))
    def read_line(self):
        try:
            s = self.ser.readline().decode("utf-8", errors="ignore").strip()
            return s if s else None
        except:
            return None
    def expect(self, prefix, timeout=20.0):
        t0 = time.time()
        while time.time()-t0 < timeout:
            s = self.read_line()
            if s and s.startswith(prefix):
                return s
        return None
    def close(self):
        try: self.ser.close()
        except: pass

def drive(sc, vL, vR): sc.send(f"L:{int(clamp(vL,-255,255))} R:{int(clamp(vR,-255,255))}")
def stop(sc): sc.send("STOP")
def led(sc, color): sc.send(f"LED {color}")
def stretch(sc, grams): sc.send(f"STRETCH {int(grams)}")
def feed_servo(sc): sc.send("FEED")

# ===== 分段執行 =====
def run_segments(sc, segments, tag="segment"):
    for i, (sec, vL, vR) in enumerate(segments, 1):
        print(f"[{tag}] {i}/{len(segments)}: {sec:.2f}s  L={vL} R={vR}")
        drive(sc, vL, vR)
        time.sleep(max(0.0, sec))
    stop(sc); time.sleep(0.2)

def run_segments_until_seen(sc, segments, tag, model, cam, target_role):
    """
    分段推進,但每 30ms 偵測一次 target_role。
    連續 FINAL_SEE_STABLE_FR 幀達標(conf 與寬度門檻)就 STOP 並回 True。
    跑完仍未觸發則回 False。
    """
    stable = deque(maxlen=FINAL_SEE_STABLE_FR)
    for i, (sec, vL, vR) in enumerate(segments, 1):
        print(f"[{tag}] {i}/{len(segments)}: {sec:.2f}s  L={vL} R={vR}  (watch {target_role})")
        t0 = time.time()
        drive(sc, vL, vR)
        while time.time()-t0 < sec:
            ok, frm = cam.read()
            if not ok:
                time.sleep(0.01); continue
            r = model.predict(source=frm, verbose=False)[0]
            seen = False
            for b, c, conf in zip(r.boxes.xyxy.cpu().numpy(),
                                  r.boxes.cls.cpu().numpy(),
                                  r.boxes.conf.cpu().numpy()):
                name = normalize_classname(r.names[int(c)])
                if name not in YOLO_CLASS_ROLE: 
                    continue
                role = YOLO_CLASS_ROLE[name]
                if role != target_role:
                    continue
                if conf < FINAL_SEE_CONF_TH:
                    continue
                _, bw = bbox_center_w(b)
                if FINAL_SEE_MIN_WIDTH > 0 and bw < FINAL_SEE_MIN_WIDTH:
                    continue
                seen = True
                break
            stable.append(1 if seen else 0)
            if len(stable)==FINAL_SEE_STABLE_FR and sum(stable)==FINAL_SEE_STABLE_FR:
                print(f"[{tag}] 偵測到 {target_role}(穩定 {FINAL_SEE_STABLE_FR} 幀)→ 停車")
                stop(sc); time.sleep(0.2)
                return True
            time.sleep(0.03)
    stop(sc); time.sleep(0.2)
    return False

# ===== YOLO 輔助 =====
def yolo_detect(model, frame_bgr):
    out = []
    r = model.predict(source=frame_bgr, verbose=False)[0]
    for b, c, conf in zip(r.boxes.xyxy.cpu().numpy(),
                          r.boxes.cls.cpu().numpy(),
                          r.boxes.conf.cpu().numpy()):
        name = normalize_classname(r.names[int(c)])
        if name not in YOLO_CLASS_ROLE: 
            continue
        if conf < DET_CONF_TH:
            continue
        out.append((YOLO_CLASS_ROLE[name], float(conf), b))
    return out

def decide_first_animal(model, cam_side):
    print("[INFO] 停車偵測第一關動物 ...")
    stable = deque(maxlen=DET_STABLE_FRAMES)
    t0 = time.time()
    while True:
        ok, frm = cam_side.read()
        if not ok:
            if time.time()-t0 > MAX_DET_WAIT_SEC: 
                print("[WARN] 影像讀取超時"); return None
            time.sleep(0.02); continue
        dets = yolo_detect(model, frm)
        animals = [d for d in dets if d[0] in ANIMAL_ROLES]
        if animals:
            animals.sort(key=lambda x:x[1], reverse=True)
            stable.append(animals[0][0])
            print(f"[DBG] animal={animals[0][0]} conf={animals[0][1]:.2f} ({len(stable)}/{DET_STABLE_FRAMES})")
            if len(stable)==DET_STABLE_FRAMES and len(set(stable))==1:
                return stable[-1]
        if time.time()-t0 > MAX_DET_WAIT_SEC:
            return None

# ===== 主流程 =====
def main():
    # 側視相機
    cam_side = cv2.VideoCapture(CAM_SIDE_INDEX)
    cam_side.set(cv2.CAP_PROP_FRAME_WIDTH, CAM_W)
    cam_side.set(cv2.CAP_PROP_FRAME_HEIGHT, CAM_H)
    if not cam_side.isOpened():
        print("[ERR] 開不了側視相機"); sys.exit(2)

    # YOLO
    model = YOLO(MODEL_PATH)

    # 串列
    sc = SerialClient(SERIAL_PORT, BAUDRATE)

    # 1) 起點 → 動物區
    run_segments(sc, PRE_TO_ANIMAL_SEGMENTS, tag="to_animal")

    # 2) 第一關:定案目標動物 → 亮燈 + 設定重量
    target_role = decide_first_animal(model, cam_side) or "CHICKEN"
    target_grams = ROLE_TO_WEIGHT[target_role]
    led(sc, ROLE_TO_LED[target_role])
    print(f"[INFO] 目標={target_role} LED={ROLE_TO_LED[target_role]} 重量={target_grams}g")

    # 3) 動物區 → 飼料機
    run_segments(sc, ANIMAL_TO_FEEDER_SEGMENTS, tag="to_feeder")

    # 4) 到飼料機後:直接 STRETCH <重量>
    print(f"[STEP] STRETCH {target_grams}")
    stretch(sc, target_grams)
    resp = sc.expect("STRETCH_", timeout=30.0)
    print(f"[INFO] Arduino 回覆:{resp or '(逾時)'}")

    # 5) 飼料機 → 投餵區:分段推進「邊跑邊看」指定動物 → 看到就停 & FEED
    seen = run_segments_until_seen(
        sc, FEEDER_TO_FEEDZONE_SEGMENTS, tag="to_feed_zone_watch",
        model=model, cam=cam_side, target_role=target_role
    )
    if seen:
        print("[STEP] FEED")
        feed_servo(sc)
        _ = sc.expect("FEED_OK", timeout=5.0)
    else:
        print("[WARN] 跑完仍未看到目標;不出飼料(可調 FINAL_SEE_* 或段落)")

    # 6) 出場
    if EXIT_SEGMENTS:
        run_segments(sc, EXIT_SEGMENTS, tag="exit")

    # 清理
    sc.close()
    cam_side.release()
    cv2.destroyAllWindows()
    print("[DONE] 全流程完成。")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\\n[EXIT] 手動中止")