优化了平衡,尤其对于向后倾倒的几种极限情况进行优化

This commit is contained in:
myperworld committed 2025-08-16 22:32:06 +08:00
1 parent 10aba2664d
commit fcb8af8163
693 files changed
+2427 -70

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
// -----------------------------------------------------------------------------
// Copyright (c) 2024 Mu Shibo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// -----------------------------------------------------------------------------
// 飞特STS3032舵机同步控制指令
// 调用方式为sms_sts.SyncWritePosEx(ID, 2, Position, Speed, ACC);
#ifndef _SERVO_STS3032_H
#define _SERVO_STS3032_H
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
// 数据结构定义
typedef char s8;
typedef unsigned char u8;
typedef unsigned short u16;
typedef short s16;
typedef unsigned long u32;
typedef long s32;
// 内存表定义
#define SMS_STS_ACC 41
#define INST_SYNC_WRITE 0x83
// 飞特串行舵机通信层协议程序
class SCS
{
public:
SCS();
SCS(u8 End);
SCS(u8 End, u8 Level);
void syncWrite(u8 ID[], u8 IDN, u8 MemAddr, u8 *nDat, u8 nLen); // 同步写指令
public:
u8 Level; // 舵机返回等级
u8 End; // 处理器大小端结构
u8 Error; // 舵机状态
u8 syncReadRxPacketIndex;
u8 syncReadRxPacketLen;
u8 *syncReadRxPacket;
u8 *syncReadRxBuff;
u16 syncReadRxBuffLen;
u16 syncReadRxBuffMax;
u32 syncTimeOut;
protected:
virtual int writeSCS(unsigned char *nDat, int nLen) = 0;
virtual int writeSCS(unsigned char bDat) = 0;
virtual void rFlushSCS() = 0;
virtual void wFlushSCS() = 0;
protected:
void Host2SCS(u8 *DataL, u8 *DataH, u16 Data); // 1个16位数拆分为2个8位数
int Ack(u8 ID); // 返回应答
int checkHead(); // 帧头检测
};
// 飞特串行舵机硬件接口层程序
class SCSerial : public SCS
{
public:
SCSerial();
SCSerial(u8 End);
SCSerial(u8 End, u8 Level);
protected:
int writeSCS(unsigned char *nDat, int nLen); // 输出nLen字节
int writeSCS(unsigned char bDat); // 输出1字节
void rFlushSCS(); //
void wFlushSCS(); //
public:
unsigned long IOTimeOut; // 输入输出超时
HardwareSerial *pSerial; // 串口指针
int Err;
public:
virtual int getErr() { return Err; }
};
// 飞特SMS/STS系列串行舵机应用层程序
class SMS_STS : public SCSerial
{
public:
virtual void SyncWritePosEx(u8 ID[], u8 IDN, s16 Position[], u16 Speed[], u8 ACC[]); // 同步写多个舵机位置指令
};
#endif
+484
View File
@@ -0,0 +1,484 @@
// -----------------------------------------------------------------------------
// Copyright (c) 2024 Mu Shibo
// Modify by Tommy
// -----------------------------------------------------------------------------
#include <WiFi.h>
/***********html&javascript网页构建**********/
const char basic_web[] PROGMEM = R"=====(
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>legged wheel robot web ctrl</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{display:flex;flex-direction:column;align-items:center;min-height:100vh;background-color:#fff;font-family:'Segoe UI',Arial,sans-serif;padding:15px;overflow:hidden;width:100vw}
.page-title{color:#333;margin-bottom:15px;text-align:center;font-weight:600;font-size:1.3rem;width:100%}
.connection-status{margin-bottom:15px;padding:6px 12px;border-radius:4px;font-weight:500;font-size:14px}
.status-connected{background-color:#e8f5e9;color:#2e7d32}
.status-disconnected{background-color:#ffebee;color:#c62828}
.btn{display:inline-block;font-size:15px;width:100px;height:60px;text-align:center;border-radius:12px;background-color:#fff;color:#6495ed;border:3px solid #6495ed;padding:0;transition:all .3s;margin:10px 0}
.row{display:flex;width:100%;justify-content:space-between;align-items:center;gap:10px}
.columnLateral{flex:1;min-width:0;text-align:center}
#joy1Div,#joy2Div{width:100%;max-width:200px;height:200px;margin:10px auto}
@media (max-width:360px){.page-title{font-size:1.1rem}
#joy1Div,#joy2Div{height:160px}
.btn{width:80px;height:50px;font-size:14px}
}
</style>
</head>
<!-- onload 事件在页面载入完成后立即触发 -->
<body onload="javascript:socket_init()">
<h1 class="page-title">WLROBOT WiFi遥控模式</h1>
<div class="connection-status status-disconnected" id="connectionStatus">未连接</div>
<center>
<div class="row">
<div class="columnLateral">
<div id="joy2Div" style="width:200px;height:200px;margin:10px"></div>
<div id="joy2Info">
<span>高度: <span id="joy2YValue">30</span></span>
<br>
<span>倾角: <span id="joy2XValue">0</span></span>
</div>
</div>
<div class="columnLateral">
<div id="joy1Div" style="width:200px;height:200px;margin:10px"></div>
<div id="joy1Info">
<span>前后: <span id="joy1YValue">0</span></span>
<br>
<span>左右: <span id="joy1XValue">0</span></span>
</div>
</div>
</div>
<div class="buttons">
<button class="btn" id="jump">Jump</button>
</div>
</center>
<script>
var socket; // socket通信
var isConnected = false; // 连接状态
var g_roll = 0;
var g_h = 30;
var g_stable = 0;
var joyX = 0;
var joyY = 0;
// 更新连接状态显示
function updateConnectionStatus(connected) {
isConnected = connected;
const statusElement = document.getElementById('connectionStatus');
if (connected) {
statusElement.textContent = '已连接';
statusElement.className = 'connection-status status-connected';
} else {
statusElement.textContent = '未连接';
statusElement.className = 'connection-status status-disconnected';
}
}
function socket_init() {
try {
// 根据环境选择合适的连接方式
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
socket = new WebSocket(wsProtocol + '//' + window.location.hostname + ':81/');
// 连接成功事件
socket.onopen = function() {
// console.log('WebSocket连接已建立');
updateConnectionStatus(true);
};
// 接收消息事件
socket.onmessage = function(event) {
console.log('收到消息:', event.data);
// 可以在这里处理从服务器返回的消息
};
// 连接关闭事件
socket.onclose = function() {
console.log('WebSocket连接已关闭');
updateConnectionStatus(false);
// 尝试重连
setTimeout(socket_init, 3000);
};
// 错误事件
socket.onerror = function(error) {
console.error('WebSocket错误:', error);
updateConnectionStatus(false);
};
} catch (e) {
console.error('WebSocket初始化失败:', e);
updateConnectionStatus(false);
// 重试连接
setTimeout(socket_init, 3000);
}
}
function send_data() {
var data = {
'roll': g_roll,
'height': g_h,
'stable': g_stable,
'mode': 'basic',
'dir': "stop",
'joy_y': joyY,
'joy_x': joyX,
};
// console.log(data);
socket.send(JSON.stringify(data));
}
function is_stable() {
var obj = document.getElementById("stable");
if(obj.checked) {
// alert("is_stable checked");
g_stable = 1;
} else {
// alert("is_stable unchecked");
g_stable = 0;
}
send_data();
}
// 绑定按钮
var buttons = document.getElementsByClassName("btn");
for(i=0;i<buttons.length;i++) {
buttons[i].addEventListener("mousedown",btn_down,true);
buttons[i].addEventListener("mouseup",btn_up,true);
buttons[i].addEventListener("touchstart",btn_down,true);
buttons[i].addEventListener("touchend",btn_up,true);
}
function btn_down() {
this.style = "background-color: cornflowerblue; color: white;";
dir = (this.id == 'jump') ? 'jump' : 'stop';
var data = {
'dir': this.id,
'mode': 'basic',
'roll': g_roll,
'height': g_h,
'stable': g_stable,
'joy_x': joyX,
'joy_y': joyY,
};
// console.log(data);
socket.send(JSON.stringify(data));
}
function btn_up() {
this.style = "background-color: white; color: cornflowerblue;";
dir = (this.id == 'jump') ? 'stop' : 'jump';
var data = {
'dir': "stop",
'mode': 'basic',
'roll': g_roll,
'height': g_h,
'stable': g_stable,
'joy_x': joyX,
'joy_y': joyY,
};
// console.log(data); // 打印测试
socket.send(JSON.stringify(data));
}
/*摇杆内容*/
var JoyStick = (function(container, parameters) {
parameters = parameters || {};
var title = (typeof parameters.title === "undefined" ? "joystick" : parameters.title),
width = (typeof parameters.width === "undefined" ? 0 : parameters.width),
height = (typeof parameters.height === "undefined" ? 0 : parameters.height),
internalFillColor = (typeof parameters.internalFillColor === "undefined" ? "#4a90e2" : parameters.internalFillColor),
internalLineWidth = (typeof parameters.internalLineWidth === "undefined" ? 2 : parameters.internalLineWidth),
internalStrokeColor = (typeof parameters.internalStrokeColor === "undefined" ? "#4a90e2" : parameters.internalStrokeColor),
externalLineWidth = (typeof parameters.externalLineWidth === "undefined" ? 2 : parameters.externalLineWidth),
externalStrokeColor = (typeof parameters.externalStrokeColor === "undefined" ? "#4a90e2" : parameters.externalStrokeColor),
autoReturnToCenter = (typeof parameters.autoReturnToCenter === "undefined" ? true : parameters.autoReturnToCenter);
// Create Canvas element and add it in the Container object
var objContainer = document.getElementById(container);
var canvas = document.createElement("canvas");
canvas.id = title;
if(width === 0) { width = objContainer.clientWidth; }
if(height === 0) { height = objContainer.clientHeight; }
canvas.width = width;
canvas.height = height;
objContainer.appendChild(canvas);
var context = canvas.getContext("2d");
var isPressing = 0;
var isMoving = 0;
var isRelease = 0;
var circumference = 2 * Math.PI;
var internalRadius = (canvas.width - ((canvas.width / 2) + 10)) / 2;
var maxMoveStick = internalRadius + 5;
var externalRadius = internalRadius + 30;
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
var directionHorizontalLimitPos = canvas.width / 10;
var directionHorizontalLimitNeg = directionHorizontalLimitPos * -1;
var directionVerticalLimitPos = canvas.height / 10;
var directionVerticalLimitNeg = directionVerticalLimitPos * -1;
// Used to save current position of stick
var movedX = centerX;
var movedY = centerY;
// Check if the device support the touch or not
if("ontouchstart" in document.documentElement) {
canvas.addEventListener("touchstart", onTouchStart, true);
canvas.addEventListener("touchmove", onTouchMove, true);
document.addEventListener("touchend", onTouchEnd, true);
} else {
canvas.addEventListener("mousedown", onMouseDown, true);
canvas.addEventListener("mousemove", onMouseMove, true);
document.addEventListener("mouseup", onMouseUp, true);
}
// Draw the object
drawExternal();
drawInternal();
/**
* @desc Draw the external circle used as reference position
*/
function drawExternal() {
context.beginPath();
context.arc(centerX, centerY, externalRadius, 0, circumference, false);
context.lineWidth = externalLineWidth;
context.strokeStyle = externalStrokeColor;
context.stroke();
}
/**
* @desc Draw the internal stick in the current position the user have moved it
*/
function drawInternal() {
context.beginPath();
if(movedX < internalRadius) { movedX = maxMoveStick; }
if((movedX + internalRadius) > canvas.width) { movedX = canvas.width - (maxMoveStick); }
if(movedY < internalRadius) { movedY = maxMoveStick; }
if((movedY + internalRadius) > canvas.height) { movedY = canvas.height - (maxMoveStick); }
context.arc(movedX, movedY, internalRadius, 0, circumference, false);
// create radial gradient
var grd = context.createRadialGradient(centerX, centerY, 5, centerX, centerY, 200);
// Light color
grd.addColorStop(0, internalFillColor);
// Dark color
grd.addColorStop(1, internalStrokeColor);
context.fillStyle = grd;
context.fill();
context.lineWidth = internalLineWidth;
context.strokeStyle = internalStrokeColor;
context.stroke();
}
function postCoordinate(containerId) {
var xValue = (100 * ((movedX - centerX) / maxMoveStick)).toFixed();
var yValue = ((100 * ((movedY - centerY) / maxMoveStick)) * -1).toFixed();
if(containerId === 'joy1Div') {
joyX = xValue;
joyY = yValue;
// 更新 joy1 的显示信息
document.getElementById('joy1XValue').textContent = xValue;
document.getElementById('joy1YValue').textContent = yValue;
} else if(containerId === 'joy2Div') {
// 限定 x 轴 roll 取值范围
let rollValue = parseInt((xValue * 30 / 100).toFixed());
if (rollValue < -30) {
rollValue = -30;
} else if (rollValue > 30) {
rollValue = 30;
}
g_roll = rollValue;
// 限定 y 轴 小车高度 取值范围为 15 到 85
let heightValue = parseInt(yValue) ;
if (heightValue < 0) {
heightValue = 0;
} else if (heightValue > 60) {
heightValue = 60;
}
g_h = heightValue;
// 更新 joy2 的显示信息,显示实际限制后的值
document.getElementById('joy2XValue').textContent = g_roll;
document.getElementById('joy2YValue').textContent = g_h;
}
send_data();
}
function releaseControl() {
joyX = 0;
joyY = 0;
g_roll = 0;
g_h = 30;
// 重置显示信息
document.getElementById('joy1XValue').textContent = '0';
document.getElementById('joy1YValue').textContent = '0';
document.getElementById('joy2XValue').textContent = '0';
document.getElementById('joy2YValue').textContent = g_h;
send_data();
}
/**
* @desc Events for manage touch
*/
function noTouch(event) {
isPressing = 0;
isMoving = 0;
isRelease = 0;
}
function onTouchStart(event) {
isPressing = 1;
isMoving = 0;
isRelease = 0;
}
function onTouchMove(event) {
// Prevent the browser from doing its default thing (scroll, zoom)
event.preventDefault();
if(isPressing === 1 && event.targetTouches[0].target === canvas) {
isMoving = 1;
isRelease = 0;
movedX = event.targetTouches[0].pageX;
movedY = event.targetTouches[0].pageY;
// Manage offset
if(canvas.offsetParent.tagName.toUpperCase() === "BODY") {
movedX -= canvas.offsetLeft;
movedY -= canvas.offsetTop;
} else {
movedX -= canvas.offsetParent.offsetLeft;
movedY -= canvas.offsetParent.offsetTop;
}
// Delete canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Redraw object
drawExternal();
drawInternal();
postCoordinate(canvas.parentNode.id);
}
}
function onTouchEnd(event) {
//event.preventDefault();
isPressing = 0;
isMoving = 0;
isRelease = 1;
// If required reset position store variable
if(autoReturnToCenter) {
movedX = centerX;
movedY = centerY;
}
// Delete canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Redraw object
drawExternal();
drawInternal();
releaseControl();
}
/**
* @desc Events for manage mouse
*/
function noMouse(event) {
isPressing = 0;
isMoving = 0;
isRelease = 0;
}
function onMouseDown(event) {
isPressing = 1;
isMoving = 0;
isRelease = 0;
}
function onMouseMove(event) {
if(isPressing === 1) {
isMoving = 1;
isRelease = 0;
movedX = event.pageX;
movedY = event.pageY;
// Manage offset
if(canvas.offsetParent.tagName.toUpperCase() === "BODY") {
movedX -= canvas.offsetLeft;
movedY -= canvas.offsetTop;
} else {
movedX -= canvas.offsetParent.offsetLeft;
movedY -= canvas.offsetParent.offsetTop;
}
// Delete canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Redraw object
drawExternal();
drawInternal();
postCoordinate(canvas.parentNode.id);
}
}
function onMouseUp(event) {
isPressing = 0;
isMoving = 0;
isRelease = 1;
// If required reset position store variable
if(autoReturnToCenter) {
movedX = centerX;
movedY = centerY;
}
// Delete canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Redraw object
drawExternal();
drawInternal();
releaseControl();
}
this.GetX = function () {
return (100 * ((movedX - centerX) / maxMoveStick)).toFixed();
};
/**
* @desc Normalizzed value of Y move of stick
* @return Integer from -100 to +100
*/
this.GetY = function () {
return ((100 * ((movedY - centerY) / maxMoveStick)) * -1).toFixed();
};
});
var joy1Param = { "title": "1" };
var Joy1 = new JoyStick('joy1Div', joy1Param);
var joy2Param = { "title": "2" };
var Joy2 = new JoyStick('joy2Div', joy2Param);
</script>
</body>
</html>
)=====";
+35
View File
@@ -0,0 +1,35 @@
#ifndef _MAIN_H
#define _MAIN_H
// 机器人控制头文件
#include <MPU6050_tockn.h>
#include "Servo_STS3032.h"
#include <SimpleFOC.h>
#include <Arduino.h>
// wifi控制数据传输头文件
#include <WebSocketsServer.h>
#include <ArduinoJson.h>
#include <WebServer.h>
#include <WiFi.h>
#include <FS.h>
#include "basic_web.h"
#include "robot.h"
#include "wifi_config.h"
#include "esp_adc_cal.h"
void basicWebCallback(void);
void webSocketEventCallback(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
void adc_calibration_init();
void bat_check();
void web_loop();
void lqr_balance_loop();
void yaw_loop();
void leg_loop();
void jump_loop();
void yaw_angle_addup();
void blinkLed();
void detectObstacle();
void resetToDefault();
#endif
+77
View File
@@ -0,0 +1,77 @@
// -----------------------------------------------------------------------------
// Copyright (c) 2024 Mu Shibo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// -----------------------------------------------------------------------------
#pragma once
#include <WiFi.h>
#include <ArduinoJson.h>
typedef struct
{
int height = 38;
int roll;
int linear;
int angular;
int dir;
int dir_last;
int joyy;
int joyy_last;
int joyx;
int joyx_last;
bool go;
int blink;
} Wrobot;
extern Wrobot wrobot;
// 机器人运动状态枚举
typedef enum
{
FORWARD = 0,
BACK,
RIGHT,
LEFT,
STOP,
JUMP,
} QR_State_t;
// 机器人模式枚举类型
typedef enum
{
BASIC = 0,
} Robot_Mode_t;
class RobotProtocol
{
public:
RobotProtocol(uint8_t len);
~RobotProtocol();
void spinOnce(void);
void parseBasic(StaticJsonDocument<300> &doc);
private:
uint8_t *_now_buf;
uint8_t *_old_buf;
uint8_t _len;
void UART_WriteBuf(void);
int checkBufRefresh(void);
};
+28
View File
@@ -0,0 +1,28 @@
// -----------------------------------------------------------------------------
// Copyright (c) 2024 Mu Shibo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// -----------------------------------------------------------------------------
#pragma once
#include <WiFi.h>
void WiFi_SetAP(void);
void set_sta_wifi(); // STA模式接入WiFi网络