初版测试

This commit is contained in:
张梦南 2026-04-12 14:44:05 +08:00
parent 82e8c1d43f
commit 9d8d7444b4
11 changed files with 941 additions and 153 deletions

134
main/CSS/control.css Normal file
View File

@ -0,0 +1,134 @@
/* 移动控制相关样式 */
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80vh;
padding: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 40px;
text-align: center;
}
/* 手柄风格布局 */
.controller {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
width: 100%;
max-width: 350px;
}
/* 移动控制按钮区域 */
.movement-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
width: 100%;
}
.movement-row {
display: flex;
gap: 15px;
justify-content: center;
}
/* 录制回放按钮区域 */
.record-controls {
display: flex;
gap: 15px;
justify-content: center;
width: 100%;
margin-top: 20px;
}
/* 按钮样式 */
.btn {
width: 120px;
height: 50px;
border: none;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0));
z-index: 1;
}
.btn span {
position: relative;
z-index: 2;
}
.btn-forward {
background-color: #4CAF50;
color: white;
}
.btn-backward {
background-color: #f44336;
color: white;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 状态显示 */
.status {
margin-top: 30px;
padding: 15px;
background-color: #f5f5f5;
border-radius: 10px;
min-height: 50px;
width: 100%;
max-width: 350px;
text-align: center;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.status p {
color: #666;
margin: 0;
font-size: 16px;
}
/* 响应式设计 */
@media (max-width: 400px) {
.btn {
width: 100px;
height: 45px;
font-size: 14px;
}
.title {
font-size: 20px;
}
}

10
main/CSS/record.css Normal file
View File

@ -0,0 +1,10 @@
/* 录制回放相关样式 */
.btn-record {
background-color: #ff9800;
color: white;
}
.btn-playback {
background-color: #2196f3;
color: white;
}

79
main/CSS/style.css Normal file
View File

@ -0,0 +1,79 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.container {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
width: 90%;
max-width: 400px;
}
h1 {
color: #333;
margin-bottom: 30px;
}
.controls {
display: flex;
justify-content: space-around;
margin-bottom: 30px;
}
.btn {
width: 120px;
height: 50px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-forward {
background-color: #4CAF50;
color: white;
}
.btn-backward {
background-color: #f44336;
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.status {
margin-top: 20px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 5px;
min-height: 40px;
}
.status p {
color: #666;
}

145
main/Javascript/control.js Normal file
View File

@ -0,0 +1,145 @@
// 获取按钮和状态元素
const forwardBtn = document.getElementById('forwardBtn');
const backwardBtn = document.getElementById('backwardBtn');
const status = document.getElementById('status');
// 实际控制函数
function controlMotor(direction) {
// 根据方向设置状态消息
if (direction === 'stop') {
status.innerHTML = `<p>正在停止...</p>`;
} else {
status.innerHTML = `<p>正在${direction === 'forward' ? '前进' : '后退'}...</p>`;
}
// 记录操作(如果正在录制)
if (window.isRecording) {
const timestamp = Date.now() - window.recordingStartTime;
window.recordedActions.push({ direction, timestamp });
console.log('Recorded action:', { direction, timestamp });
} else {
console.log('Not recording, action:', direction);
}
// 发送AJAX请求到后端服务器
fetch('/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ direction: direction })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
status.innerHTML = `<p>${data.message}</p>`;
} else {
status.innerHTML = `<p>错误: ${data.message}</p>`;
}
})
.catch(error => {
status.innerHTML = `<p>通信错误: ${error.message}</p>`;
});
}
// 按钮按下和松开事件
// 前进按钮 - 支持鼠标和触摸事件
forwardBtn.addEventListener('mousedown', () => {
// 发送前进请求
controlMotor('forward');
});
forwardBtn.addEventListener('mouseup', () => {
// 停止电机
stopMotor();
});
forwardBtn.addEventListener('mouseleave', () => {
// 鼠标离开按钮时也停止
stopMotor();
});
// 前进按钮 - 触摸事件
forwardBtn.addEventListener('touchstart', (e) => {
e.preventDefault(); // 防止默认行为
// 发送前进请求
controlMotor('forward');
});
forwardBtn.addEventListener('touchend', (e) => {
e.preventDefault(); // 防止默认行为
// 停止电机
stopMotor();
});
forwardBtn.addEventListener('touchcancel', (e) => {
e.preventDefault(); // 防止默认行为
// 触摸被取消时停止电机
stopMotor();
});
// 后退按钮 - 支持鼠标和触摸事件
backwardBtn.addEventListener('mousedown', () => {
// 发送后退请求
controlMotor('backward');
});
backwardBtn.addEventListener('mouseup', () => {
// 停止电机
stopMotor();
});
backwardBtn.addEventListener('mouseleave', () => {
// 鼠标离开按钮时也停止
stopMotor();
});
// 后退按钮 - 触摸事件
backwardBtn.addEventListener('touchstart', (e) => {
e.preventDefault(); // 防止默认行为
// 发送后退请求
controlMotor('backward');
});
backwardBtn.addEventListener('touchend', (e) => {
e.preventDefault(); // 防止默认行为
// 停止电机
stopMotor();
});
backwardBtn.addEventListener('touchcancel', (e) => {
e.preventDefault(); // 防止默认行为
// 触摸被取消时停止电机
stopMotor();
});
// 停止电机函数
function stopMotor() {
// 记录操作(如果正在录制)
if (window.isRecording) {
const timestamp = Date.now() - window.recordingStartTime;
window.recordedActions.push({ direction: 'stop', timestamp });
console.log('Recorded action:', { direction: 'stop', timestamp });
}
// 发送停止请求到服务器
fetch('/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ direction: 'stop' })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// 停止成功,不显示消息
} else {
status.innerHTML = `<p>错误: ${data.message}</p>`;
}
})
.catch(error => {
status.innerHTML = `<p>通信错误: ${error.message}</p>`;
});
}

112
main/Javascript/record.js Normal file
View File

@ -0,0 +1,112 @@
// 确保在DOM加载完成后执行
window.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded - Record.js executing');
// 录制相关全局变量
window.isRecording = false;
window.recordedActions = [];
window.recordingStartTime = 0;
// 获取录制和回放按钮
const recordBtn = document.getElementById('recordBtn');
const playbackBtn = document.getElementById('playbackBtn');
const status = document.getElementById('status');
// 调试信息
console.log('Record.js loaded');
console.log('recordBtn:', recordBtn);
console.log('playbackBtn:', playbackBtn);
console.log('status:', status);
// 录制按钮点击事件 - 支持鼠标和触摸事件
function toggleRecording() {
console.log('toggleRecording called, current isRecording:', window.isRecording);
if (window.isRecording) {
// 停止录制
window.isRecording = false;
recordBtn.textContent = '开始录制';
status.innerHTML = `<p>录制完成,共记录 ${window.recordedActions.length} 个操作</p>`;
console.log('Recording stopped, actions:', window.recordedActions);
} else {
// 开始录制
window.isRecording = true;
window.recordedActions = [];
window.recordingStartTime = Date.now();
recordBtn.textContent = '停止录制';
status.innerHTML = `<p>开始录制...</p>`;
console.log('Recording started');
}
}
console.log('Adding click event listener to recordBtn');
recordBtn.addEventListener('click', toggleRecording);
// 录制按钮 - 触摸事件
console.log('Adding touchstart event listener to recordBtn');
recordBtn.addEventListener('touchstart', (e) => {
console.log('Touchstart event on recordBtn');
e.preventDefault(); // 防止默认行为
toggleRecording();
});
// 回放按钮点击事件 - 支持鼠标和触摸事件
function startPlayback() {
console.log('startPlayback called, recordedActions:', window.recordedActions);
if (window.recordedActions.length === 0) {
status.innerHTML = `<p>没有可回放的操作</p>`;
console.log('No actions to playback');
return;
}
// 保存当前录制状态并禁用录制
const originalRecordingState = window.isRecording;
window.isRecording = false;
status.innerHTML = `<p>开始回放...</p>`;
console.log('Starting playback');
// 按时间顺序回放操作
let lastTimestamp = 0;
const totalDuration = window.recordedActions[window.recordedActions.length - 1].timestamp;
// 确保至少有一个操作
if (window.recordedActions.length > 0) {
// 执行第一个操作
console.log('Executing first action immediately:', window.recordedActions[0]);
controlMotor(window.recordedActions[0].direction);
// 执行剩余的操作
for (let i = 1; i < window.recordedActions.length; i++) {
const action = window.recordedActions[i];
const delay = action.timestamp - window.recordedActions[i-1].timestamp;
console.log('Scheduling action:', action, 'at delay:', delay);
setTimeout(() => {
// 调用控制函数执行操作
console.log('Executing action:', action);
controlMotor(action.direction);
}, delay);
}
}
// 回放完成后,确保发送停止请求
setTimeout(() => {
console.log('Playback completed, sending stop command');
controlMotor('stop');
status.innerHTML = `<p>回放完成</p>`;
console.log('Playback completed');
// 恢复原始录制状态
window.isRecording = originalRecordingState;
}, totalDuration + 1000);
}
console.log('Adding click event listener to playbackBtn');
playbackBtn.addEventListener('click', startPlayback);
// 回放按钮 - 触摸事件
console.log('Adding touchstart event listener to playbackBtn');
playbackBtn.addEventListener('touchstart', (e) => {
console.log('Touchstart event on playbackBtn');
e.preventDefault(); // 防止默认行为
startPlayback();
});
});

38
main/Javascript/script.js Normal file
View File

@ -0,0 +1,38 @@
// 获取按钮和状态元素
const forwardBtn = document.getElementById('forwardBtn');
const backwardBtn = document.getElementById('backwardBtn');
const status = document.getElementById('status');
// 实际控制函数
function controlMotor(direction) {
status.innerHTML = `<p>正在${direction === 'forward' ? '前进' : '后退'}...</p>`;
// 发送AJAX请求到后端服务器
fetch('/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ direction: direction })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
status.innerHTML = `<p>${data.message}</p>`;
} else {
status.innerHTML = `<p>错误: ${data.message}</p>`;
}
})
.catch(error => {
status.innerHTML = `<p>通信错误: ${error.message}</p>`;
});
}
// 按钮点击事件
forwardBtn.addEventListener('click', () => {
controlMotor('forward');
});
backwardBtn.addEventListener('click', () => {
controlMotor('backward');
});

Binary file not shown.

View File

@ -1,91 +1,229 @@
import cv2
import numpy as np
import motor
import time
import signal
import sys
# 信号处理函数用于捕获Ctrl+C
def signal_handler(sig, frame):
print("\n接收到中断信号,正在停止电机...")
motor.stop()
print("电机已停止,程序退出")
sys.exit(0)
# 注册信号处理函数
signal.signal(signal.SIGINT, signal_handler)
print("开始运行摄像头程序...")
print(f"OpenCV版本: {cv2.__version__}")
# 尝试打开摄像头
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("无法打开摄像头!")
exit()
print("摄像头打开成功")
# 设置摄像头参数
cap.set(3, 320)
cap.set(4, 240)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
# ===== 核心改动:用背景减法器替代帧差法 =====
# MOG2: 能适应背景变化,适合移动摄像头场景
# history: 保留多少帧来建立背景模型(越大适应越慢,但越稳定)
# varThreshold: 判断前景的灵敏度(越小越敏感,但噪声越多)
fgbg = cv2.createBackgroundSubtractorMOG2(history=100, varThreshold=25, detectShadows=False)
print(f"摄像头分辨率: {cap.get(3)}x{cap.get(4)}")
# 可选用KNN背景减法器效果类似速度稍快
# fgbg = cv2.createBackgroundSubtractorKNN(history=100, dist2Threshold=400, detectShadows=False)
# 初始化稳定阶段
print("初始化摄像头...")
success_count = 0
for i in range(10):
ret, frame = cap.read()
if not ret:
print(f"{i+1}帧读取失败")
continue
success_count += 1
print(f"{i+1}帧读取成功")
time.sleep(0.1)
print(f"摄像头初始化完成,成功读取{success_count}")
# 形态学核(用于去噪)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# 读取初始帧
ret, prev_frame = cap.read()
if not ret:
print("无法读取初始帧!")
cap.release()
exit()
# 用于平滑追踪的卡尔曼滤波器(可选,减少抖动)
kalman = cv2.KalmanFilter(4, 2)
kalman.measurementMatrix = np.array([[1,0,0,0], [0,1,0,0]], np.float32)
kalman.transitionMatrix = np.array([[1,0,1,0], [0,1,0,1], [0,0,1,0], [0,0,0,1]], np.float32)
kalman.processNoiseCov = np.eye(4, dtype=np.float32) * 0.03
kalman.measurementNoiseCov = np.eye(2, dtype=np.float32) * 0.5
kalman.statePost = np.zeros((4,1), np.float32)
print("初始帧读取成功")
prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
prev_gray = cv2.GaussianBlur(prev_gray, (5,5), 0)
print("初始帧处理完成")
# 记录上一帧目标的位置和面积
prev_cx = None
prev_cy = None
prev_area = None
# 目标跟踪状态
tracking_target = None
while True:
ret, frame = cap.read()
if not ret:
break
# 1. 背景减法:得到前景掩码(白色=运动物体)
fgmask = fgbg.apply(frame)
# 2. 形态学处理:去噪 + 填充空洞
fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel) # 先开运算去噪点
fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_CLOSE, kernel) # 再闭运算填充空洞
# 3. 找轮廓
contours, _ = cv2.findContours(fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# ===== 核心:选最大目标 =====
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5,5), 0)
# 帧差
diff = cv2.absdiff(prev_gray, gray)
diff = cv2.convertScaleAbs(diff, alpha=2.5)
# 应用形态学操作,去除噪声
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
diff = cv2.morphologyEx(diff, cv2.MORPH_OPEN, kernel)
diff = cv2.morphologyEx(diff, cv2.MORPH_CLOSE, kernel)
_, thresh = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# ===== 核心:目标检测和跟踪 =====
target = None
max_area = 0
# 找到所有符合条件的轮廓
valid_contours = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < 300: # 过滤噪声
if area < 500: # 过滤噪声,增大阈值
continue
if area > max_area:
max_area = area
target = cnt
if area > 8000: # 过滤大面积假目标(如整个画面),减小阈值
continue
# 计算轮廓的宽高比,过滤不符合垃圾特征的目标
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = float(w) / h if h > 0 else 0
if aspect_ratio > 3 or aspect_ratio < 0.3: # 过滤过于狭长的目标
continue
valid_contours.append((area, cnt, x, y, w, h))
# ===== 处理目标 =====
# 如果有有效的轮廓
if valid_contours:
# 如果正在跟踪目标,优先选择与上一目标位置接近的轮廓
if tracking_target is not None and prev_cx is not None and prev_cy is not None:
best_match = None
min_distance = float('inf')
for area, cnt, x, y, w, h in valid_contours:
cx = x + w // 2
cy = y + h // 2
# 计算与上一目标的距离
distance = ((cx - prev_cx) ** 2 + (cy - prev_cy) ** 2) ** 0.5
# 如果距离小于阈值,认为是同一个目标
if distance < 50: # 距离阈值
if distance < min_distance:
min_distance = distance
best_match = (area, cnt, x, y, w, h)
# 如果找到匹配的目标
if best_match:
area, cnt, x, y, w, h = best_match
target = cnt
max_area = area
else:
# 如果没有找到匹配的目标,选择最大的轮廓
valid_contours.sort(key=lambda x: x[0], reverse=True)
area, cnt, x, y, w, h = valid_contours[0]
target = cnt
max_area = area
# 开始跟踪新目标
tracking_target = True
else:
# 如果没有正在跟踪的目标,选择最大的轮廓
valid_contours.sort(key=lambda x: x[0], reverse=True)
area, cnt, x, y, w, h = valid_contours[0]
target = cnt
max_area = area
# 开始跟踪新目标
tracking_target = True
# ===== 只处理一个目标 =====
if target is not None:
x, y, w, h = cv2.boundingRect(target)
cx = x + w // 2
cy = y + h // 2
# 卡尔曼滤波预测/更新(可选,让追踪更平滑)
measurement = np.array([[np.float32(cx)], [np.float32(cy)]])
kalman.correct(measurement)
prediction = kalman.predict()
pred_x, pred_y = int(prediction[0]), int(prediction[1])
# 画原始检测框
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.circle(frame, (cx, cy), 5, (0, 0, 255), -1)
# 画卡尔曼平滑后的位置(黄色)
cv2.circle(frame, (pred_x, pred_y), 5, (0, 255, 255), -1)
print("唯一目标:", cx, cy, "area:", max_area)
print(f"目标位置: ({cx}, {cy}), 面积: {max_area}")
# 检测目标是否突然跳变
target_jumped = False
if prev_cx is not None and prev_cy is not None and prev_area is not None:
# 计算位置变化
dx = abs(cx - prev_cx)
dy = abs(cy - prev_cy)
# 计算面积变化比例
area_ratio = max_area / prev_area if prev_area > 0 else 0
# 如果位置变化过大或面积变化过大,认为目标跳变
if dx > 100 or dy > 100 or area_ratio < 0.3 or area_ratio > 3:
target_jumped = True
print("目标突然跳变,可能已离开视野范围")
# 重置跟踪状态
tracking_target = None
# 控制逻辑根据y轴坐标控制垃圾桶前后移动
center_y = 120 # 画面中心y坐标
threshold = 20 # 阈值范围
speed = 3000 # 电机速度
if target_jumped:
# 目标跳变,停止移动
print("目标跳变,停止移动")
motor.stop()
elif cy < center_y - threshold:
# 目标在上侧,垃圾桶向后移动
print("目标在上侧,向后移动")
# 使用新的电机控制函数
motor.backward(speed=0.6)
elif cy > center_y + threshold:
# 目标在下侧,垃圾桶向前移动
print("目标在下侧,向前移动")
# 使用新的电机控制函数
motor.forward(speed=0.6)
else:
# 目标在中心附近,停止移动
print("目标在中心,停止移动")
motor.stop()
# 更新上一帧的目标信息
prev_cx = cx
prev_cy = cy
prev_area = max_area
else:
# 没有检测到目标时,只预测不更新
prediction = kalman.predict()
# 可选:根据预测位置保持追踪(即使被短暂遮挡)
# 可选:显示前景掩码(调试用)
cv2.imshow("foreground mask", fgmask)
# 没有检测到目标,停止移动
print("未检测到目标,停止移动")
motor.stop()
# 重置上一帧的目标信息
prev_cx = None
prev_cy = None
prev_area = None
# 重置跟踪状态
tracking_target = None
cv2.imshow("tracking", frame)
prev_gray = gray.copy()
if cv2.waitKey(1) & 0xFF == 27:
break
cap.release()
cv2.destroyAllWindows()
cv2.destroyAllWindows()
motor.stop()

42
main/index.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>垃圾桶控制界面</title>
<link rel="stylesheet" href="CSS/control.css">
<link rel="stylesheet" href="CSS/record.css">
</head>
<body>
<div class="container">
<h1 class="title">垃圾桶控制界面</h1>
<!-- 手柄风格控制器 -->
<div class="controller">
<!-- 移动控制区域 -->
<div class="movement-controls">
<div class="movement-row">
<button class="btn btn-forward" id="forwardBtn"><span>前进</span></button>
</div>
<div class="movement-row">
<button class="btn btn-backward" id="backwardBtn"><span>后退</span></button>
</div>
</div>
<!-- 录制回放控制区域 -->
<div class="record-controls">
<button class="btn btn-record" id="recordBtn"><span>开始录制</span></button>
<button class="btn btn-playback" id="playbackBtn"><span>回放</span></button>
</div>
</div>
<!-- 状态显示区域 -->
<div class="status" id="status">
<p>就绪</p>
</div>
</div>
<script src="Javascript/control.js"></script>
<script src="Javascript/record.js"></script>
</body>
</html>

View File

@ -1,104 +1,143 @@
from smbus2 import SMBus
import time
from smbus2 import SMBus
import time
import signal
import sys
bus = SMBus(1)
addr = 0x60
# ======================
# PCA9685 初始化
# ======================
bus.write_byte_data(addr, 0x00, 0x00)
bus.write_byte_data(addr, 0xFE, 60)
# ======================
# PWM输出0~4095
# ======================
def set_pwm(ch, val):
val = max(0, min(4095, val))
base = 0x06 + 4 * ch
bus.write_byte_data(addr, base, 0)
bus.write_byte_data(addr, base+1, 0)
bus.write_byte_data(addr, base+2, val & 0xFF)
bus.write_byte_data(addr, base+3, val >> 8)
# ======================
# ⭐ 你的真实映射8通道版本
# ======================
MOTORS = {
"M1": {"in1": 0, "in2": 1},
"M2": {"in1": 2, "in2": 3},
"M3": {"in1": 4, "in2": 5},
"M4": {"in1": 6, "in2": 7},
}
# ======================
# 电机控制(重点)
# ======================
def motor(name, speed=0, direction="stop"):
m = MOTORS[name]
# 速度限制
speed = max(0, min(4095, speed))
if direction == "f":
set_pwm(m["in1"], speed)
set_pwm(m["in2"], 0)
elif direction == "b":
set_pwm(m["in1"], 0)
set_pwm(m["in2"], speed)
else:
set_pwm(m["in1"], 0)
set_pwm(m["in2"], 0)
# ======================
# 四轮控制
# ======================
def all_motors(speed, direction):
for m in MOTORS:
motor(m, speed, direction)
def forward(speed=3500):
all_motors(speed, "f")
def backward(speed=3500):
all_motors(speed, "b")
def stop():
all_motors(0, "stop")
# ======================
# 转向(差速)
# ======================
def turn_left(speed=3500):
motor("M1", speed, "b")
motor("M3", speed, "b")
motor("M2", speed, "f")
motor("M4", speed, "f")
def turn_right(speed=3500):
motor("M1", speed, "f")
motor("M3", speed, "f")
motor("M2", speed, "b")
motor("M4", speed, "b")
# ======================
# 测试
# ======================
if __name__ == "__main__":
print("前进")
forward(3500)
time.sleep(3)
print("停止")
# 信号处理函数用于捕获Ctrl+C
def signal_handler(sig, frame):
print("\n接收到中断信号,正在停止电机...")
stop()
time.sleep(1)
print("电机已停止,程序退出")
sys.exit(0)
print("后退")
backward(3500)
time.sleep(3)
# 注册信号处理函数
signal.signal(signal.SIGINT, signal_handler)
print("停止")
stop()
# ======================
# PCA9685 初始化
# ======================
bus = SMBus(1)
addr = 0x60
MODE1 = 0x00
PRESCALE = 0xFE
bus.write_byte_data(addr, MODE1, 0x00)
bus.write_byte_data(addr, PRESCALE, 60)
# ======================
# 电机通道映射(每个电机 IN1 / IN2
# ======================
MOTOR = {
"M1": (0, 1), # 前左
"M2": (2, 3), # 前右
"M3": (4, 5), # 后左
"M4": (6, 7), # 后右
}
# ======================
# 方向修正(麦克纳姆关键)
# 根据你 / \ + \ / 结构校正
# ======================
DIR = {
"M1": 1,
"M2": -1,
"M3": -1,
"M4": 1,
}
# ======================
# PWM输出函数
# ======================
def set_pwm(ch, value):
value = max(0, min(4095, value))
bus.write_byte_data(addr, 0x06 + 4 * ch, 0)
bus.write_byte_data(addr, 0x07 + 4 * ch, 0)
bus.write_byte_data(addr, 0x08 + 4 * ch, value & 0xFF)
bus.write_byte_data(addr, 0x09 + 4 * ch, value >> 8)
# ======================
# 单电机控制
# speed: -1 ~ 1
# ======================
def motor_drive(name, speed):
in1, in2 = MOTOR[name]
speed *= DIR[name]
pwm = int(abs(speed) * 4095)
if speed > 0:
set_pwm(in1, pwm)
set_pwm(in2, 0)
elif speed < 0:
set_pwm(in1, 0)
set_pwm(in2, pwm)
else:
# 刹车关键不是0
set_pwm(in1, 4095)
set_pwm(in2, 4095)
# ======================
# 基础运动控制
# ======================
def forward(speed=0.6):
motor_drive("M1", speed)
motor_drive("M2", speed)
motor_drive("M3", speed)
motor_drive("M4", speed)
def backward(speed=0.6):
motor_drive("M1", -speed)
motor_drive("M2", -speed)
motor_drive("M3", -speed)
motor_drive("M4", -speed)
# ======================
# 麦克纳姆左右移动
# ======================
def move_left(speed=0.6):
motor_drive("M1", -speed)
motor_drive("M2", speed)
motor_drive("M3", speed)
motor_drive("M4", -speed)
def move_right(speed=0.6):
motor_drive("M1", speed)
motor_drive("M2", -speed)
motor_drive("M3", -speed)
motor_drive("M4", speed)
# ======================
# 停止(刹车模式)
# ======================
def stop():
motor_drive("M1", 0)
motor_drive("M2", 0)
motor_drive("M3", 0)
motor_drive("M4", 0)
# ======================
# 测试程序
# ======================
if __name__ == "__main__":
print("前进")
forward(0.5)
time.sleep(1)
print("停止")
stop()
time.sleep(1)
print("后退")
backward(0.5)
time.sleep(1)
print("停止")
stop()
time.sleep(1)
print("停止")
stop()

51
main/server.py Normal file
View File

@ -0,0 +1,51 @@
from flask import Flask, send_file, request, jsonify, send_from_directory
import motor
import time
import os
app = Flask(__name__)
# 静态文件路由
@app.route('/CSS/<path:filename>')
def serve_css(filename):
return send_from_directory('CSS', filename)
@app.route('/Javascript/<path:filename>')
def serve_js(filename):
return send_from_directory('Javascript', filename)
# 主页路由
@app.route('/')
def index():
return send_file('index.html')
# 控制电机路由
@app.route('/control', methods=['POST'])
def control():
data = request.get_json()
direction = data.get('direction')
# forward 与 backward 反向调整
try:
if direction == 'forward':
print("控制垃圾桶前进")
motor.backward(speed=0.6)
# 不使用time.sleep让电机持续运行
return jsonify({'status': 'success', 'message': '前进中'})
elif direction == 'backward':
print("控制垃圾桶后退")
motor.forward(speed=0.6)
# 不使用time.sleep让电机持续运行
return jsonify({'status': 'success', 'message': '后退中'})
elif direction == 'stop':
print("停止垃圾桶")
motor.stop()
return jsonify({'status': 'success', 'message': '已停止'})
else:
return jsonify({'status': 'error', 'message': '无效的方向'})
except Exception as e:
print(f"控制出错: {e}")
return jsonify({'status': 'error', 'message': f'控制出错: {e}'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)