From 9d8d7444b47491ad413f87c6f86e0280000a58a4 Mon Sep 17 00:00:00 2001 From: Cx330 <1487537121@qq.com> Date: Sun, 12 Apr 2026 14:44:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E7=89=88=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/CSS/control.css | 134 ++++++++++++++ main/CSS/record.css | 10 + main/CSS/style.css | 79 ++++++++ main/Javascript/control.js | 145 +++++++++++++++ main/Javascript/record.js | 112 ++++++++++++ main/Javascript/script.js | 38 ++++ main/__pycache__/motor.cpython-313.pyc | Bin 0 -> 4033 bytes main/camera.py | 244 +++++++++++++++++++------ main/index.html | 42 +++++ main/motor.py | 239 ++++++++++++++---------- main/server.py | 51 ++++++ 11 files changed, 941 insertions(+), 153 deletions(-) create mode 100644 main/CSS/control.css create mode 100644 main/CSS/record.css create mode 100644 main/CSS/style.css create mode 100644 main/Javascript/control.js create mode 100644 main/Javascript/record.js create mode 100644 main/Javascript/script.js create mode 100644 main/__pycache__/motor.cpython-313.pyc create mode 100644 main/index.html create mode 100644 main/server.py diff --git a/main/CSS/control.css b/main/CSS/control.css new file mode 100644 index 0000000..936b41d --- /dev/null +++ b/main/CSS/control.css @@ -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; + } +} \ No newline at end of file diff --git a/main/CSS/record.css b/main/CSS/record.css new file mode 100644 index 0000000..2b8e710 --- /dev/null +++ b/main/CSS/record.css @@ -0,0 +1,10 @@ +/* 录制回放相关样式 */ +.btn-record { + background-color: #ff9800; + color: white; +} + +.btn-playback { + background-color: #2196f3; + color: white; +} \ No newline at end of file diff --git a/main/CSS/style.css b/main/CSS/style.css new file mode 100644 index 0000000..1e43f28 --- /dev/null +++ b/main/CSS/style.css @@ -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; +} \ No newline at end of file diff --git a/main/Javascript/control.js b/main/Javascript/control.js new file mode 100644 index 0000000..f307d4c --- /dev/null +++ b/main/Javascript/control.js @@ -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 = `

正在停止...

`; + } else { + status.innerHTML = `

正在${direction === 'forward' ? '前进' : '后退'}...

`; + } + + // 记录操作(如果正在录制) + 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 = `

${data.message}

`; + } else { + status.innerHTML = `

错误: ${data.message}

`; + } + }) + .catch(error => { + status.innerHTML = `

通信错误: ${error.message}

`; + }); +} + +// 按钮按下和松开事件 + +// 前进按钮 - 支持鼠标和触摸事件 +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 = `

错误: ${data.message}

`; + } + }) + .catch(error => { + status.innerHTML = `

通信错误: ${error.message}

`; + }); +} \ No newline at end of file diff --git a/main/Javascript/record.js b/main/Javascript/record.js new file mode 100644 index 0000000..171aa7e --- /dev/null +++ b/main/Javascript/record.js @@ -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 = `

录制完成,共记录 ${window.recordedActions.length} 个操作

`; + console.log('Recording stopped, actions:', window.recordedActions); + } else { + // 开始录制 + window.isRecording = true; + window.recordedActions = []; + window.recordingStartTime = Date.now(); + recordBtn.textContent = '停止录制'; + status.innerHTML = `

开始录制...

`; + 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 = `

没有可回放的操作

`; + console.log('No actions to playback'); + return; + } + + // 保存当前录制状态并禁用录制 + const originalRecordingState = window.isRecording; + window.isRecording = false; + + status.innerHTML = `

开始回放...

`; + 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 = `

回放完成

`; + 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(); + }); +}); \ No newline at end of file diff --git a/main/Javascript/script.js b/main/Javascript/script.js new file mode 100644 index 0000000..502ec25 --- /dev/null +++ b/main/Javascript/script.js @@ -0,0 +1,38 @@ +// 获取按钮和状态元素 +const forwardBtn = document.getElementById('forwardBtn'); +const backwardBtn = document.getElementById('backwardBtn'); +const status = document.getElementById('status'); + +// 实际控制函数 +function controlMotor(direction) { + status.innerHTML = `

正在${direction === 'forward' ? '前进' : '后退'}...

`; + + // 发送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 = `

${data.message}

`; + } else { + status.innerHTML = `

错误: ${data.message}

`; + } + }) + .catch(error => { + status.innerHTML = `

通信错误: ${error.message}

`; + }); +} + +// 按钮点击事件 +forwardBtn.addEventListener('click', () => { + controlMotor('forward'); +}); + +backwardBtn.addEventListener('click', () => { + controlMotor('backward'); +}); \ No newline at end of file diff --git a/main/__pycache__/motor.cpython-313.pyc b/main/__pycache__/motor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..564929a45ff85d2932dd9182d35b9dabcf73fb2e GIT binary patch literal 4033 zcmc&$-E&h#6yJOA-sI-1=@+Ffq8$V^NGqQTh%!h~K5UV0tH8 zD$uHhRt;K>&}vvM^sWsoU<)?xB_wo?kU$+EsVg9Xc~wGvp`|W&v{-SNx)nW)K_B&& zF||v`y8Iw$eNYb#77tbb+bqiBh{{pfwI(KKBMdoF!d( zdJrIF5n5NC0>rXJmY%YuCly{n%uQ3K6#GVrkG3Zq{6^PxKEMihfu(Xylz(!Y4x|7B$pv9*ReF?A+8ey%b)p8w0m~ z7{2x0$AQT&&%*2W7eC(q;zA%EiuDEJ{fU0wGH}?afU$j{-e6CtFWej995T-v=|C^DN|#nJFojV+X@=P~g9(_E=SvfLU=Qkl_F%Jm@Jr0z40!*=Ote{k zY|RdyzSRxYK zcNj!Cln5C#6b^GBq`be!P!EQBlLbC<7dSBNdL$7XI27kTsI7%ZKMDd!=JsZ!Gix%5 zA^)|SN3Xe;rnUjWexVDm(uKoxD13g;#p(;MeiXmBCrh9Dr!D@=og;xP-8lC+|K-rg z>!Wl%_rOd_sD|_uRMSkVLDejNf=x-_^T4BisS9~6ifM1ICIrZaT}0`cAsdhqcL@|G zA-xKpq`K}TxwDFE-jE3s5fKvD@-ohqeP5_Z;+&QYz=Btt$}Fb6^~cL*|sK`G5Ueo|ag>p4=!u9UGu&MEqdk`c9? z0~-i$K+%X|5r`2J7; zgSi(R&BnakGTTzrWm*7tnHE59ZHW^8G<=XhgJL}hD@tu&X#ZOR=Cjb!g{`YW6ksMZ z-C4TW1UipCN*B)qw+9?PINV~;EXy!YkRKSTBi?^766}q1C-`$PBP5=V_lvo(&J|w3 z|IcyGiAF9BP$^C;9!twCI?oeQr@+Z$(VhgyU4LLU&?2~UA&t!N7sk#M3R;;xTap}S z_BovVmegtmQ^u}Z?T{R8`8_MHJ*690(0POi+KH6M@!lhZ4ob;p4P|M)$>BM*QCctD z7+Nrhce!9NiM}R|e|Y<+Pokhf8Q*N$#AVY|GpFD>>64;sM7g=yS8v$#I2_}vIj)jH z6EVovHuE~j@zfG`LBqy&ytJdelP^WfW7rdte(SbXhW1LwHnw%k%iH)eY#25yzb>C@jJ&Amh~Wa0muD9t3bk^hP2Bd<*u(gV;P2v`z8;aI!b@Jl_R243oYR z 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() \ No newline at end of file +cv2.destroyAllWindows() +motor.stop() \ No newline at end of file diff --git a/main/index.html b/main/index.html new file mode 100644 index 0000000..05b6029 --- /dev/null +++ b/main/index.html @@ -0,0 +1,42 @@ + + + + + + 垃圾桶控制界面 + + + + +
+

垃圾桶控制界面

+ + +
+ +
+
+ +
+
+ +
+
+ + +
+ + +
+
+ + +
+

就绪

+
+
+ + + + + \ No newline at end of file diff --git a/main/motor.py b/main/motor.py index c64f73d..ee67c7a 100644 --- a/main/motor.py +++ b/main/motor.py @@ -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() \ No newline at end of file +# ====================== +# 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() diff --git a/main/server.py b/main/server.py new file mode 100644 index 0000000..5f486d3 --- /dev/null +++ b/main/server.py @@ -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/') +def serve_css(filename): + return send_from_directory('CSS', filename) + +@app.route('/Javascript/') +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)