v1.2版本改为通过xbox系列手柄进行遥控,并增加智能模式和手动模式

This commit is contained in:
myperworld committed 2025-10-10 21:58:44 +08:00
1 parent 76603d3c31
commit 1a97bc32c3
1839 files changed
+478852 -3

No files matched your search

@@ -0,0 +1,158 @@
#include "XboxControllerNotificationParser.h"
#define XBOX_CONTROLLER_INDEX_BUTTONS_DIR 12
#define XBOX_CONTROLLER_INDEX_BUTTONS_MAIN 13
#define XBOX_CONTROLLER_INDEX_BUTTONS_CENTER 14
#define XBOX_CONTROLLER_INDEX_BUTTONS_SHARE 15
XboxControllerNotificationParser::XboxControllerNotificationParser() {
btnA = btnB = btnX = btnY = false;
btnShare = btnStart = btnSelect = btnXbox = false;
btnLB = btnRB = btnLS = btnRS = false;
btnDirUp = btnDirLeft = btnDirRight = btnDirDown = false;
joyLHori = joyLVert = joyRHori = joyRVert = 0xffff / 2;
trigLT = trigRT = 0;
}
uint8_t XboxControllerNotificationParser::update(uint8_t* data, size_t length) {
if (length != expectedDataLen) {
return XBOX_CONTROLLER_ERROR_INVALID_LENGTH;
}
for (uint8_t i = 0; i < expectedDataLen; i++)
{
rx_buf[i]=data[i];
}
uint8_t btnBits;
btnBits = data[XBOX_CONTROLLER_INDEX_BUTTONS_MAIN];
btnA = btnBits & 0b00000001;
btnB = btnBits & 0b00000010;
btnX = btnBits & 0b00001000;
btnY = btnBits & 0b00010000;
btnLB = btnBits & 0b01000000;
btnRB = btnBits & 0b10000000;
btnBits = data[XBOX_CONTROLLER_INDEX_BUTTONS_CENTER];
btnSelect = btnBits & 0b00000100;
btnStart = btnBits & 0b00001000;
btnXbox = btnBits & 0b00010000;
btnLS = btnBits & 0b00100000;
btnRS = btnBits & 0b01000000;
btnBits = data[XBOX_CONTROLLER_INDEX_BUTTONS_SHARE];
btnShare = btnBits & 0b00000001;
btnBits = data[XBOX_CONTROLLER_INDEX_BUTTONS_DIR];
btnDirUp = btnBits == 1 || btnBits == 2 || btnBits == 8;
btnDirRight = 2 <= btnBits && btnBits <= 4;
btnDirDown = 4 <= btnBits && btnBits <= 6;
btnDirLeft = 6 <= btnBits && btnBits <= 8;
joyLHori = (uint16_t)data[0] | ((uint16_t)data[1] << 8);
joyLVert = (uint16_t)data[2] | ((uint16_t)data[3] << 8);
joyRHori = (uint16_t)data[4] | ((uint16_t)data[5] << 8);
joyRVert = (uint16_t)data[6] | ((uint16_t)data[7] << 8);
trigLT = (uint16_t)data[8] | ((uint16_t)data[9] << 8);
trigRT = (uint16_t)data[10] | ((uint16_t)data[11] << 8);
return 0;
}
void XboxControllerNotificationParser::convertU16TU8Arr(uint16_t vU16,
uint8_t* data) {
data[0] = vU16 & 0xff;
data[1] = vU16 >> 8;
}
uint8_t XboxControllerNotificationParser::toArr(uint8_t* data, size_t length) {
if (length < expectedDataLen) {
return XBOX_CONTROLLER_ERROR_INVALID_LENGTH;
}
convertU16TU8Arr(joyLHori, &data[0]);
convertU16TU8Arr(joyLVert, &data[2]);
convertU16TU8Arr(joyRHori, &data[4]);
convertU16TU8Arr(joyRVert, &data[6]);
convertU16TU8Arr(trigLT, &data[8]);
convertU16TU8Arr(trigRT, &data[10]);
{
uint8_t btnBits = 0;
if (btnA) btnBits |= 0b00000001;
if (btnB) btnBits |= 0b00000010;
if (btnX) btnBits |= 0b00001000;
if (btnY) btnBits |= 0b00010000;
if (btnLB) btnBits |= 0b01000000;
if (btnRB) btnBits |= 0b10000000;
data[XBOX_CONTROLLER_INDEX_BUTTONS_MAIN] = btnBits;
}
{
uint8_t btnBits = 0;
if (btnSelect) btnBits |= 0b00000100;
if (btnStart) btnBits |= 0b00001000;
if (btnXbox) btnBits |= 0b00010000;
if (btnLS) btnBits |= 0b00100000;
if (btnRS) btnBits |= 0b01000000;
data[XBOX_CONTROLLER_INDEX_BUTTONS_CENTER] = btnBits;
}
{
uint8_t btnBits = 0;
if (btnShare) btnBits |= 0b00000001;
data[XBOX_CONTROLLER_INDEX_BUTTONS_SHARE] = btnBits;
}
{
uint8_t btnBits = 0;
if (btnDirUp) {
if (btnDirRight) {
btnBits = 2;
} else if (btnDirLeft) {
btnBits = 8;
} else {
btnBits = 1;
}
} else if (btnDirDown) {
if (btnDirRight) {
btnBits = 4;
} else if (btnDirLeft) {
btnBits = 6;
} else {
btnBits = 5;
}
} else {
if (btnDirRight) {
btnBits = 3;
} else if (btnDirLeft) {
btnBits = 7;
} else {
btnBits = 0;
}
}
data[XBOX_CONTROLLER_INDEX_BUTTONS_DIR] = btnBits;
}
return 0;
}
String XboxControllerNotificationParser::toString() {
// clang-format off
String str = String("") +
"btnY: " + String(btnY) + " " +
"btnX: " + String(btnX) + " " +
"btnB: " + String(btnB) + " " +
"btnA: " + String(btnA) + " " +
"btnLB: " + String(btnLB) + " " +
"btnRB: " + String(btnRB) + "\n" +
"btnSelect: " + String(btnSelect) + " " +
"btnStart: " + String(btnStart) + " " +
"btnXbox: " + String(btnXbox) + " " +
"btnShare: " + String(btnShare) + " " +
"btnLS: " + String(btnLS) + " " +
"btnRS: " + String(btnRS) + "\n" +
"btnDirUp: " + String(btnDirUp) + " " +
"btnDirRight: " + String(btnDirRight) + " " +
"btnDirDown: " + String(btnDirDown) + " " +
"btnDirLeft: " + String(btnDirLeft) + "\n"
"joyLHori: " + String(joyLHori) + "\n" +
"joyLVert: " + String(joyLVert) + "\n" +
"joyRHori: " + String(joyRHori) + "\n" +
"joyRVert: " + String(joyRVert) + "\n" +
"trigLT: " + String(trigLT) + "\n" +
"trigRT: " + String(trigRT) + "\n";
// clang-format on
return str;
}
@@ -0,0 +1,36 @@
#ifndef __XBOX_CONTROLLER_NOTIFICATION_PARSER_H__
#define __XBOX_CONTROLLER_NOTIFICATION_PARSER_H__
#include <Arduino.h>
#define XBOX_CONTROLLER_ERROR_INVALID_LENGTH 1
class XboxControllerNotificationParser {
public:
XboxControllerNotificationParser();
bool btnA, btnB, btnX, btnY;
bool btnShare, btnStart, btnSelect, btnXbox;
// side top button
bool btnLB, btnRB;
// button on joy stick
bool btnLS, btnRS;
bool btnDirUp, btnDirLeft, btnDirRight, btnDirDown;
uint16_t joyLHori = maxJoy / 2;
uint16_t joyLVert = maxJoy / 2;
uint16_t joyRHori = maxJoy / 2;
uint16_t joyRVert = maxJoy / 2;
uint16_t trigLT, trigRT;
uint8_t update(uint8_t* data, size_t length);
uint8_t toArr(uint8_t* data, size_t length);
String toString();
static const size_t expectedDataLen = 16;
static const uint16_t maxJoy = 0xffff;
static const uint16_t maxTrig = 0x3ff;
uint8_t rx_buf[expectedDataLen];
private:
static void convertU16TU8Arr(uint16_t vU16, uint8_t* data);
};
#endif
@@ -0,0 +1,553 @@
#pragma once
#include <NimBLEDevice.h>
#include <XboxControllerNotificationParser.h>
#include <XboxSeriesXHIDReportBuilder_asukiaaa.hpp>
// #define XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL Serial
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
const unsigned long printInterval = 100UL;
#endif
namespace XboxSeriesXControllerESP32_asukiaaa {
static NimBLEUUID uuidServiceGeneral("1801");
static NimBLEUUID uuidServiceBattery("180f");
static NimBLEUUID uuidServiceHid("1812");
static NimBLEUUID uuidCharaReport("2a4d");
static NimBLEUUID uuidCharaPnp("2a50");
static NimBLEUUID uuidCharaHidInformation("2a4a");
static NimBLEUUID uuidCharaPeripheralAppearance("2a01");
static NimBLEUUID uuidCharaPeripheralControlParameters("2a04");
static NimBLEAdvertisedDevice* advDevice;
static NimBLEClient* pConnectedClient = nullptr;
static const uint16_t controllerAppearance = 964;
static const String controllerManufacturerDataNormal = "060000";
static const String controllerManufacturerDataSearching = "0600030080";
enum class ConnectionState : uint8_t {
Connected = 0,
WaitingForFirstNotification = 1,
Found = 2,
Scanning = 3,
};
class ClientCallbacks : public NimBLEClientCallbacks {
public:
ConnectionState* pConnectionState;
ClientCallbacks(ConnectionState* pConnectionState) {
this->pConnectionState = pConnectionState;
}
void onConnect(NimBLEClient* pClient) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("Connected");
#endif
*pConnectionState = ConnectionState::WaitingForFirstNotification;
// pClient->updateConnParams(120,120,0,60);
};
void onDisconnect(NimBLEClient* pClient) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.print(
pClient->getPeerAddress().toString().c_str());
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(" Disconnected");
#endif
*pConnectionState = ConnectionState::Scanning;
pConnectedClient = nullptr;
};
/********************* Security handled here **********************
****** Note: these are the same return values as defaults ********/
uint32_t onPassKeyRequest() {
// Serial.println("Client Passkey Request");
/** return the passkey to send to the server */
return 0;
};
bool onConfirmPIN(uint32_t pass_key) {
// Serial.print("The passkey YES/NO number: ");
// Serial.println(pass_key);
/** Return false if passkeys don't match. */
return true;
};
/** Pairing process complete, we can check the results in ble_gap_conn_desc */
void onAuthenticationComplete(ble_gap_conn_desc* desc) {
// Serial.println("onAuthenticationComplete");
if (!desc->sec_state.encrypted) {
// Serial.println("Encrypt connection failed - disconnecting");
/** Find the client with the connection handle provided in desc */
NimBLEDevice::getClientByID(desc->conn_handle)->disconnect();
return;
}
};
};
/** Define a class to handle the callbacks when advertisments are received */
class AdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks {
public:
AdvertisedDeviceCallbacks(String strTargetDeviceAddress,
ConnectionState* pConnectionState) {
if (strTargetDeviceAddress != "") {
this->targetDeviceAddress =
new NimBLEAddress(strTargetDeviceAddress.c_str());
}
this->pConnectionState = pConnectionState;
}
private:
NimBLEAddress* targetDeviceAddress = nullptr;
ConnectionState* pConnectionState;
void onResult(NimBLEAdvertisedDevice* advertisedDevice) {
int rssi = advertisedDevice->getRSSI();
if(rssi > -45)
{
Serial.printf("Strong device found: address=%s, RSSI=%d\n",
advertisedDevice->getAddress().toString().c_str(), rssi);
}
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.print("Advertised Device found: ");
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
advertisedDevice->toString().c_str());
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf(
"name:%s, address:%s\n", advertisedDevice->getName().c_str(),
advertisedDevice->getAddress().toString().c_str());
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf(
"uuidService:%s\n",
advertisedDevice->haveServiceUUID()
? advertisedDevice->getServiceUUID().toString().c_str()
: "none");
#endif
char* pHex = NimBLEUtils::buildHexData(
nullptr, (uint8_t*)advertisedDevice->getManufacturerData().data(),
advertisedDevice->getManufacturerData().length());
if ((targetDeviceAddress != nullptr &&
advertisedDevice->getAddress().equals(*targetDeviceAddress)) ||
(targetDeviceAddress == nullptr &&
advertisedDevice->getAppearance() == controllerAppearance &&
(strcmp(pHex, controllerManufacturerDataNormal.c_str()) == 0 ||
strcmp(pHex, controllerManufacturerDataSearching.c_str()) == 0) &&
advertisedDevice->getServiceUUID().equals(uuidServiceHid)))
// if (advertisedDevice->isAdvertisingService(uuidServiceHid))
{
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("Found target");
#endif
/** stop scan before connecting */
// NimBLEDevice::getScan()->stop();
/** Save the device reference in a global for the client to use*/
*pConnectionState = ConnectionState::Found;
advDevice = advertisedDevice;
}
};
};
class Core {
public:
Core(String targetDeviceAddress = "") {
this->advDeviceCBs =
new AdvertisedDeviceCallbacks(targetDeviceAddress, &connectionState);
this->clientCBs = new ClientCallbacks(&connectionState);
}
AdvertisedDeviceCallbacks* advDeviceCBs;
ClientCallbacks* clientCBs;
uint8_t battery = 0;
static const int deviceAddressLen = 6;
uint8_t deviceAddressArr[deviceAddressLen];
void begin() {
NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DEVICE);
// NimBLEDevice::setScanDuplicateCacheSize(200);
NimBLEDevice::init("");
NimBLEDevice::setOwnAddrType(BLE_OWN_ADDR_PUBLIC);
NimBLEDevice::setSecurityAuth(true, false, false);
NimBLEDevice::setPower(ESP_PWR_LVL_P9); /* +9db */
}
void writeHIDReport(uint8_t* dataArr, size_t dataLen) {
if (pConnectedClient == nullptr) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("no connnected client");
#endif
return;
}
NimBLEClient* pClient = pConnectedClient;
auto pService = pClient->getService(uuidServiceHid);
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(pService->toString().c_str());
#endif
for (auto pChara : *pService->getCharacteristics()) {
if (pChara->canWrite()) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
"canWrite " + String(pChara->canWrite()));
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
pChara->toString().c_str());
writeWithComment(pChara, dataArr, dataLen);
#else
pChara->writeValue(dataArr, dataLen, false);
#endif
}
}
}
void writeHIDReport(
const XboxSeriesXHIDReportBuilder_asukiaaa::ReportBase& repo) {
writeHIDReport((uint8_t*)repo.arr8t, repo.arr8tLen);
}
void writeHIDReport(
const XboxSeriesXHIDReportBuilder_asukiaaa::ReportBeforeUnion&
repoBeforeUnion) {
XboxSeriesXHIDReportBuilder_asukiaaa::ReportBase repo;
repo.v = repoBeforeUnion;
writeHIDReport((uint8_t*)repo.arr8t, repo.arr8tLen);
}
void onLoop() {
if (!isConnected()) {
if (advDevice != nullptr) {
auto connectionResult = connectToServer(advDevice);
if (!connectionResult || !isConnected()) {
NimBLEDevice::deleteBond(advDevice->getAddress());
++countFailedConnection;
// reset();
connectionState = ConnectionState::Scanning;
} else {
countFailedConnection = 0;
}
advDevice = nullptr;
} else if (!isScanning()) {
// reset();
startScan();
}
}
}
String buildDeviceAddressStr() {
char buffer[18];
auto addr = deviceAddressArr;
snprintf(buffer, sizeof(buffer), "%02x:%02x:%02x:%02x:%02x:%02x", addr[5],
addr[4], addr[3], addr[2], addr[1], addr[0]);
return String(buffer);
}
void startScan() {
connectionState = ConnectionState::Scanning;
auto pScan = NimBLEDevice::getScan();
// pScan->clearResults();
// pScan->clearDuplicateCache();
pScan->setDuplicateFilter(false);
pScan->setAdvertisedDeviceCallbacks(advDeviceCBs);
// pScan->setActiveScan(true);
pScan->setInterval(97);
pScan->setWindow(97);
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("Start scan");
#endif
// assign scanCompleteCB to scan on other thread
pScan->start(scanTime, &Core::scanCompleteCB, false);
}
XboxControllerNotificationParser xboxNotif;
bool isWaitingForFirstNotification() {
return connectionState == ConnectionState::WaitingForFirstNotification;
}
bool isConnected() {
return connectionState == ConnectionState::WaitingForFirstNotification ||
connectionState == ConnectionState::Connected;
}
unsigned long getReceiveNotificationAt() { return receivedNotificationAt; }
uint8_t getCountFailedConnection() { return countFailedConnection; }
private:
ConnectionState connectionState = ConnectionState::Scanning;
unsigned long receivedNotificationAt = 0;
uint32_t scanTime = 4; /** 0 = scan forever */
uint8_t countFailedConnection = 0;
uint8_t retryCountInOneConnection = 3;
unsigned long retryIntervalMs = 100;
NimBLEClient* pClient = nullptr;
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
static void writeWithComment(NimBLERemoteCharacteristic* pChara,
uint8_t* data, size_t len) {
Serial.println("send(print from addr 0) ");
for (int i = 0; i < len; ++i) {
Serial.print(data[i]);
Serial.print(" ");
}
if (pChara->writeValue(data, len, true)) {
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("suceeded in writing");
} else {
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("failed writing");
}
}
#endif
static void readAndPrint(NimBLERemoteCharacteristic* pChara) {
auto str = pChara->readValue();
if (str.size() == 0) {
str = pChara->readValue();
}
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
printValue(str);
#endif
}
bool isScanning() { return NimBLEDevice::getScan()->isScanning(); }
// void reset() {
// NimBLEDevice::deinit(true);
// delay(500);
// begin();
// delay(500);
// }
/** Handles the provisioning of clients and connects / interfaces with the
* server */
bool connectToServer(NimBLEAdvertisedDevice* advDevice) {
NimBLEClient* pClient = nullptr;
/** Check if we have a client we should reuse first **/
if (NimBLEDevice::getClientListSize()) {
pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
if (pClient) {
pClient->connect();
}
}
/** No client to reuse? Create a new one. */
if (!pClient) {
if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
"Max clients reached - no more connections available");
#endif
return false;
}
pClient = NimBLEDevice::createClient();
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("New client created");
#endif
// default values
// pClient->setConnectionParams(
// BLE_GAP_INITIAL_CONN_ITVL_MIN, BLE_GAP_INITIAL_CONN_ITVL_MAX,
// BLE_GAP_INITIAL_CONN_LATENCY, BLE_GAP_INITIAL_SUPERVISION_TIMEOUT,
// 16, 16);
// pClient->setConnectionParams(
// BLE_GAP_INITIAL_CONN_ITVL_MIN, BLE_GAP_INITIAL_CONN_ITVL_MAX,
// BLE_GAP_INITIAL_CONN_LATENCY, BLE_GAP_INITIAL_SUPERVISION_TIMEOUT,
// 100, 100);
pClient->setClientCallbacks(clientCBs, true);
pClient->connect(advDevice, true);
}
int retryCount = retryCountInOneConnection;
while (!pClient->isConnected()) {
if (retryCount <= 0) {
return false;
} else {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("Try connection. left: " +
String(retryCount));
#endif
}
// NimBLEDevice::getScan()->stop();
// pClient->disconnect();
delay(retryIntervalMs);
// Serial.println(pClient->toString().c_str());
pClient->connect(true);
--retryCount;
}
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.print("Connected to: ");
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
pClient->getPeerAddress().toString().c_str());
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.print("RSSI: ");
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(pClient->getRssi());
#endif
// pClient->discoverAttributes();
bool result = afterConnect(pClient);
if (!result) {
return result;
}
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("Done with this device!");
#endif
pConnectedClient = pClient;
return true;
}
bool afterConnect(NimBLEClient* pClient) {
memcpy(deviceAddressArr, pClient->getPeerAddress().getNative(),
deviceAddressLen);
for (auto pService : *pClient->getServices(true)) {
auto sUuid = pService->getUUID();
if (!sUuid.equals(uuidServiceHid) && !sUuid.equals(uuidServiceBattery)) {
continue; // skip
}
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
pService->toString().c_str());
#endif
for (auto pChara : *pService->getCharacteristics(true)) {
charaHandle(pChara);
charaSubscribeNotification(pChara);
}
}
return true;
}
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
void charaPrintId(NimBLERemoteCharacteristic* pChara) {
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf(
"s:%s c:%s h:%d",
pChara->getRemoteService()->getUUID().toString().c_str(),
pChara->getUUID().toString().c_str(), pChara->getHandle());
}
static void printValue(std::__cxx11::string str) {
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf("str: %s\n", str.c_str());
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf("hex:");
for (auto v : str) {
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf(" %02x", v);
}
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("");
}
#endif
void charaHandle(NimBLERemoteCharacteristic* pChara) {
if (pChara->canWrite()) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
charaPrintId(pChara);
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(" canWrite");
#endif
}
if (pChara->canRead()) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
charaPrintId(pChara);
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(" canRead");
#endif
// Reading value is required for subscribe
auto str = pChara->readValue();
if (str.size() == 0) {
str = pChara->readValue();
}
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
printValue(str);
#endif
}
}
void charaSubscribeNotification(NimBLERemoteCharacteristic* pChara) {
if (pChara->canNotify()) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
charaPrintId(pChara);
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(" canNotify ");
#endif
if (pChara->subscribe(
true,
std::bind(&Core::notifyCB, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3,
std::placeholders::_4),
true)) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
"succeeded in subscribing");
#endif
} else {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("failed subscribing");
#endif
}
}
}
void notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
auto sUuid = pRemoteCharacteristic->getRemoteService()->getUUID();
if (connectionState != ConnectionState::Connected) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
"Received first notification");
#endif
connectionState = ConnectionState::Connected;
}
if (sUuid.equals(uuidServiceHid)) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
static bool isPrinting = false;
static unsigned long printedAt = 0;
if (isPrinting || millis() - printedAt < printInterval) return;
isPrinting = true;
std::string str = (isNotify == true) ? "Notification" : "Indication";
str += " from ";
/** NimBLEAddress and NimBLEUUID have std::string operators */
str += std::string(pRemoteCharacteristic->getRemoteService()
->getClient()
->getPeerAddress());
str += ": Service = " +
std::string(pRemoteCharacteristic->getRemoteService()->getUUID());
str +=
", Characteristic = " + std::string(pRemoteCharacteristic->getUUID());
// str += ", Value = " + std::string((char*)pData, length);
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(str.c_str());
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.print("value: ");
for (int i = 0; i < length; ++i) {
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf(" %02x", pData[i]);
}
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("");
#endif
xboxNotif.update(pData, length);
receivedNotificationAt = millis();
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
// XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.print(xboxNotif.toString());
printedAt = millis();
isPrinting = false;
#endif
} else {
if (sUuid.equals(uuidServiceBattery)) {
battery = pData[0];
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("battery notification");
#endif
} else {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf("s:%s",
sUuid.toString().c_str());
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println(
"not handled notification");
#endif
}
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
for (int i = 0; i < length; ++i) {
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.printf(" %02x", pData[i]);
}
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("");
#endif
}
}
static void scanCompleteCB(NimBLEScanResults results) {
#ifdef XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL
XBOX_SERIES_X_CONTROLLER_DEBUG_SERIAL.println("Scan Ended");
#endif
}
};
}; // namespace XboxSeriesXControllerESP32_asukiaaa
@@ -0,0 +1,73 @@
#pragma once
namespace XboxSeriesXHIDReportBuilder_asukiaaa {
struct InfoSelect {
uint8_t center : 1;
uint8_t shake : 1;
uint8_t right : 1;
uint8_t left : 1;
uint8_t _unkown_bit_4 : 1;
uint8_t _unkown_bit_5 : 1;
uint8_t _unkown_bit_6 : 1;
uint8_t _unkown_bit_7 : 1;
};
// 0 to 0x64 (100), over 0x64 becomes 0x64
struct InfoPower {
uint8_t left;
uint8_t right;
uint8_t shake;
uint8_t center;
};
struct ReportBeforeUnion {
InfoSelect select;
InfoPower power;
// value * 0.01 seconds, max 2.55 seconds
uint8_t timeActive;
uint8_t timeSilent;
// max 0xff
uint8_t countRepeat;
};
class ReportBase {
public:
static const size_t arr8tLen = 8;
union {
ReportBeforeUnion v;
uint8_t arr8t[arr8tLen];
};
ReportBase() { setFullPowerFor1Sec(); }
void setFullPowerFor1Sec() {
v.select.center = true;
v.select.left = true;
v.select.right = true;
v.select.shake = false;
v.power.center = 30;
// v.power.left = 30;
// v.power.right = 30;
// v.power.shake = 30;
// v.timeActive = 30;
v.timeSilent = 0;
v.countRepeat = 0;
}
void setAllOff() {
v.select.center = false;
v.select.left = false;
v.select.right = false;
v.select.shake = false;
v.power.center = 0;
v.power.left = 0;
v.power.right = 0;
v.power.shake = 0;
v.timeActive = 0;
v.timeSilent = 0;
v.countRepeat = 0;
}
};
} // namespace XboxSeriesXHIDReportBuilder_asukiaaa
+954
View File
@@ -0,0 +1,954 @@
/* ---------------------------------
Copyright (c) 2024 Mu Shibo
Modify by Tommy 2025.10.30
------------------------------------- */
#include <Arduino.h>
#include <FastLED.h>
#include <MPU6050_tockn.h>
#include <SMS_STS.h>
#include <SimpleFOC.h>
#include <FS.h>
#include <esp_adc_cal.h>
#include <XboxSeriesXControllerESP32_asukiaaa.hpp> // xbox手柄
/************ xbox手柄实体 *************/
XboxSeriesXControllerESP32_asukiaaa::Core xboxController("0a:27:12:5c:71:79");
/************函数申明*************/
// 电压检测函数
void adc_calibration_init();
void bat_check();
// 平衡算法函数
void lqr_balance_loop();
void yaw_loop();
void leg_loop();
void jump_loop();
void resetZeroPoint();
void jump_charge(); // 腿部弯曲(蓄能)
// RGB相关函数
void handleLEDBlink();
// Xbox函数
void process_xbox_notif();
void process_xbox_string();
void processControllerData(const XboxControllerNotificationParser& data);
const int deadZone = 10; // 摇杆死区阈值
bool last_btnbtnRS = 0;
/************实例定义*************/
// 电机实例
BLDCMotor motor1 = BLDCMotor(7);
BLDCMotor motor2 = BLDCMotor(7);
BLDCDriver3PWM driver1 = BLDCDriver3PWM(32, 33, 25, 22);
BLDCDriver3PWM driver2 = BLDCDriver3PWM(26, 27, 14, 12);
// 编码器实例
TwoWire I2Cone = TwoWire(0);
TwoWire I2Ctwo = TwoWire(1);
MagneticSensorI2C sensor1 = MagneticSensorI2C(AS5600_I2C);
MagneticSensorI2C sensor2 = MagneticSensorI2C(AS5600_I2C);
// PID控制器实例
PIDController pid_angle(1, 0, 0, 100000, 8); // 比例系数(P), 积分系数(I), 微分系数(D), 积分限幅(当I=0,无效), 输出限幅(8V防止电机过载)
PIDController pid_gyro(0.06, 0, 0, 100000, 8);
PIDController pid_distance(0.5, 0, 0, 100000, 8);
PIDController pid_speed(0.7, 0, 0, 100000, 8);
PIDController pid_yaw_angle(1.0, 0, 0, 100000, 8);
PIDController pid_yaw_gyro(0.04, 0, 0, 100000, 8);
PIDController pid_lqr_u(1, 15, 0, 100000, 8);
PIDController pid_zeropoint(0.002, 0, 0, 100000, 4);
PIDController pid_roll_angle(8, 0, 0, 100000, 450);
// 低通滤波器实例
LowPassFilter lpf_joyy(0.2); // 新输入值占输出值的10%,历史值占90%
LowPassFilter lpf_zeropoint(0.1);
LowPassFilter lpf_roll(0.3);
LowPassFilter lpf_height(0.1);
// STS舵机实例
SMS_STS sms_sts;
// MPU6050实例
MPU6050 mpu6050(I2Ctwo);
// FastLED配置
#define RGB_PIN 21
#define NUM_LEDS 2
CRGB leds[NUM_LEDS];
unsigned long previousBlinkMillis = 0;
int blinkState = 0;
int blinkCount = 0;
bool isBlinking = false;
CRGB blinkColors[6] = {CRGB::Red, CRGB::Red, CRGB::Yellow, CRGB::Yellow, CRGB::Green, CRGB::Green};
/************参数定义*************/
#define pi 3.1415927
// 机身平衡角度调整 roll轴
float roll_adjust = 0.0f;
// 机身高度
float leg_height_base = 20.0f; // 改为全局变量,初始值20 腿部默认高度基准值,值越小,机身越高, 实测范围 -10(最高),52(最低)
// #define LEG_HEIGHT_BASE 20 //20 腿部默认高度基准值,值越小,机身越高, 实测范围 -10(最高),52(最低)
#define SERVO0_MIN 2047 // 舵机1最低位置,大腿与小腿碰到一起, 从小车正面看右手边舵机
#define SERVO1_MIN 2047 // 舵机2最低位置,大腿与小腿碰到一起, 从小车正面看左手边舵机
#define SERVO0_MAX (2047 + 12 + 8.4 * (35 + 10)) // 2438 舵机1最高
#define SERVO1_MAX (2047 - 12 - 8.4 * (35 + 10)) // 1658 舵机2最高
float leg_position_add = 0; // roll轴平衡控制量
int left_height;
int right_height;
float LegHeightDiff = 0; // 左右腿高度差
// 舵机运动参数
byte ID[2] = {1, 2};
s16 Position[2];
u16 Speed[2];
byte ACC[2];
// 腿部高度逻辑
// #define LEG_HEIGHT_BASE 20 # 固定值
// float LegHeightDiff ; # 根据前后转向运动时,智能计算两腿高度差
// left_height,right_height = wrobot.height + LegHeightDiff # 根据前后左右智能计算两腿高度差
// leg_position_add = pid_roll_angle(lpf_roll(roll_angle)); 根据roll自动计算
// Position[0] 2047 + 12 + 8.4 * (left_height - LEG_HEIGHT_BASE) - leg_position_add;
// Position[1] 2047 - 12 - 8.4 * (right_height - LEG_HEIGHT_BASE) - leg_position_add;
// Position 2047 - 12 - 8.4 * (right_height - LEG_HEIGHT_BASE) - leg_position_add;
// Position 2047 - 12 - 8.4 * (wrobot.height + LegHeightDiff - LEG_HEIGHT_BASE) - pid_roll_angle(lpf_roll(roll_angle));
// 遥控器 两腿高度差 固定值 roll轴平衡控制量
// LQR自平衡控制器参数
float LQR_angle = 0;
float LQR_gyro = 0;
float LQR_gyroZ = 0;
float LQR_speed = 0;
float LQR_distance = 0;
float angle_control = 0;
float gyro_control = 0;
float speed_control = 0;
float distance_control = 0;
float LQR_u = 0;
float angle_zeropoint = 8; // 5 设定默认俯仰角度,向前到正,向后倒负
float original_angle_zeropoint = 8; // 保存原始的角度零点
float distance_zeropoint = 0.5f; // 轮部位移零点偏置
// YAW轴控制数据
float YAW_gyro = 0;
float YAW_angle = 0;
float YAW_angle_last = 0;
float YAW_angle_total = 0;
float YAW_angle_zero_point = -10;
float YAW_output = 0;
// 记录轮部转速,用于判断跳跃状态
unsigned long last_speed_record_time = 0; // 上次记录转速的时间
const unsigned long SPEED_RECORD_INTERVAL = 100; // 转速记录间隔(毫秒) ,因为时间相隔太近速度差不明显的
float last_lqr_speed = 0; // 记录上一时刻的轮部转速
float robot_speed_diff = 0; // 速度差
int jump_flag = 0; // 跳跃过程计数
int jump_pre_flag = 0; // 跳跃前蓄能计数
int wheel_ground_flag = 1; // 轮子是否离地标记 默认着地
// 电压检测
#define LED_BAT 13
uint16_t bat_check_num = 0;
int BAT_PIN = 35; // select the input pin for the ADC
static esp_adc_cal_characteristics_t adc_chars;
static const adc1_channel_t channel = ADC1_CHANNEL_7;
static const adc_bits_width_t width = ADC_WIDTH_BIT_12;
static const adc_atten_t atten = ADC_ATTEN_DB_12;
static const adc_unit_t unit = ADC_UNIT_1;
// 倒地起来辅助计数
int loopCounter = 0; // loop函数执行计数
int resetToDefaultCounter = 0; // resetToDefault执行计数
// 小车状态控制
bool robot_enabled = true; // 小车使能状态,true为起立状态,false为坐下状态
bool last_robot_enabled = true; // 记录上一次的小车状态
int sitting_down = 0; // 标记是否正在执行坐下动作
typedef struct
{
int mode = 0; // 0:左摇杆左右转向控制(智能),1:左摇杆上下摇摆控制(手动)
int height = 30;
int roll;
int jump = 0; // 0 =不跳跃 1= 跳跃
int joyy;
int joyy_last;
int joyx;
int joyx_last;
} Wrobot;
Wrobot wrobot;
void setup()
{
Serial.begin(115200); // 通讯串口
Serial2.begin(1000000); // 腿部sts舵机
// 舵机初始化
sms_sts.pSerial = &Serial2;
ID[0] = 1;
ID[1] = 2;
ACC[0] = 30;
ACC[1] = 30;
Speed[0] = 400;
Speed[1] = 400;
Position[0] = SERVO0_MIN;
Position[1] = SERVO1_MIN;
// 舵机(ID1/ID2)以速度V=2400步/秒,加速度A=50(50*100步/秒^2),运行至各自的Position位置
sms_sts.SyncWritePosEx(ID, 2, Position, Speed, ACC);
// 电压检测
adc_calibration_init();
adc1_config_width(width);
adc1_config_channel_atten(channel, atten);
esp_adc_cal_characterize(unit, atten, width, 0, &adc_chars);
// 电量显示LED
pinMode(LED_BAT, OUTPUT);
// 编码器设置
I2Cone.begin(19, 18, 400000UL);
I2Ctwo.begin(23, 5, 400000UL);
sensor1.init(&I2Cone);
sensor2.init(&I2Ctwo);
// mpu6050设置
mpu6050.begin();
mpu6050.calcGyroOffsets(true);
// 连接motor对象与编码器对象
motor1.linkSensor(&sensor1);
motor2.linkSensor(&sensor2);
// 速度环PID参数
motor1.PID_velocity.P = 0.05;
motor1.PID_velocity.I = 1;
motor1.PID_velocity.D = 0;
motor2.PID_velocity.P = 0.05;
motor2.PID_velocity.I = 1;
motor2.PID_velocity.D = 0;
// 驱动器设置
motor1.voltage_sensor_align = 6;
motor2.voltage_sensor_align = 6;
driver1.voltage_power_supply = 8;
driver2.voltage_power_supply = 8;
driver1.init();
driver2.init();
// 连接motor对象与驱动器对象
motor1.linkDriver(&driver1);
motor2.linkDriver(&driver2);
motor1.torque_controller = TorqueControlType::voltage; // 扭矩控制器类型为 "电压模式"
motor2.torque_controller = TorqueControlType::voltage;
motor1.controller = MotionControlType::torque; // 运动控制器类型为 "扭矩模式"
motor2.controller = MotionControlType::torque;
// monitor相关设置
motor1.useMonitoring(Serial);
motor2.useMonitoring(Serial);
// 电机初始化
motor1.init();
motor1.initFOC();
motor2.init();
motor2.initFOC();
// FastLED初始化
FastLED.addLeds<WS2812, RGB_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(50); // 设置亮度
fill_solid(leds, NUM_LEDS, CRGB::Black); // 初始化为熄灭
FastLED.show();
// Xbox手柄初始化
Serial.println("Starting Xbox Bluetooth Client");
xboxController.begin();
delay(500);
}
void loop()
{
// 处理LED闪烁
// handleLEDBlink();
// 处理xbox手柄通知
process_xbox_notif();
bat_check(); // 电压检测
delayMicroseconds(1500);
mpu6050.update(); // IMU数据更新
lqr_balance_loop(); // lqr自平衡控制
yaw_loop(); // yaw轴转向控制
jump_loop(); // 跳跃动作
leg_loop(); // 腿部动作控制
// 记录上一次的遥控数据数据
wrobot.joyx_last = wrobot.joyx;
wrobot.joyy_last = wrobot.joyy;
motor1.target = (-0.5) * (LQR_u + YAW_output); // target正数小车向前,负数向后
motor2.target = (-0.5) * (LQR_u - YAW_output);
loopCounter++;
// loopCounter作用:在期初的800次循环内视为电源开通后第一次自平衡,不能进入resetToDefault()函数重置参数
// resetToDefaultCounter作用:防止小车倒地后某个角度,陷入循环,motor.target一直=0
if ((LQR_angle < -35.0f || LQR_angle > 70.0f ) && loopCounter > 800 && resetToDefaultCounter < 130) {
resetZeroPoint();
resetToDefaultCounter++;
motor1.target = 0;
motor2.target = 0;
}
// 大仰角到底,轮子就不要转了 修改
if (abs(LQR_angle) > 120.0f) {
motor1.target = 0;
motor2.target = 0;
}
// 达到平衡角度后,重置重置计数器
if( abs(LQR_angle) < 25.0f){
resetToDefaultCounter = 0;
}
// 小车坐下
if (sitting_down) {
sitting_down ++;
if (sitting_down < 15) {
// 如果遥感向后运动,给个向后退脉冲,让小车向前倒地
if(wrobot.joyy < 0){
motor1.target = -1.0; // 负数小车向后,正数向前
motor2.target = -1.0;
Position[0] = SERVO0_MIN; // 修改
Position[1] = SERVO1_MIN;
} else { // 否则默认向前倒地 给个向前脉冲
motor1.target = 1.0; // 负数小车向后,正数向前
motor2.target = 1.0;
Position[0] = SERVO0_MIN + 60; // 修改
Position[1] = SERVO1_MIN - 60;
}
}else{
// 通过简单的开环控制前后左右
float speed_coeff = 0.02; // 速度系数:值越大,移动越快(建议0.01~0.02)
motor1.target = (-0.5) * (wrobot.joyy + wrobot.joyx) * speed_coeff;
motor2.target = (-0.5) * (wrobot.joyy - wrobot.joyx) * speed_coeff;
}
// 收缩双腿,舵机到最低位置
if (sitting_down == 5) {
ACC[0] = ACC[1] = 100;
Speed[0] = Speed[1] = 200;
sms_sts.SyncWritePosEx(ID, 2, Position, Speed, ACC);
}
}
// 迭代计算FOC相电压
motor1.loopFOC();
motor2.loopFOC();
// 设置轮部电机输出
motor1.move();
motor2.move();
} // loop()
// lqr自平衡控制
void lqr_balance_loop()
{
/*
motor.target 负数小车向后,正数小车向前
LQR_u 越大,电机输出的转矩越大,电机向后转,默认0-8V
LQR_u = angle_control + gyro_control + distance_control + speed_control
= 俯仰角度控制 + 俯仰角速度控制 + 距离控制 + 速度控制
angle_control = pid_angle( LQR_angle - angle_zeropoint) // 实际pitch角度-目标pitch角度
LQR_angle = mpu6050.getAngleY()
gyro_control = pid_gyro( LQR_gyro - 0 ) // 实际角速度,目标值是零
LQR_gyro = mpu6050.getGyroY()
distance_control = pid_distance( LQR_distance - distance_zeropoint) // 两个电机平均旋转弧度(实际位移量) - 位移基准零点(某一时刻LQR_distance快照)
LQR_distance = (-0.5)*(motor1.shaft_angle + motor2.shaft_angle)
speed_control = pid_speed(LQR_speed - speed_target_coeff * lpf_joyy(wrobot.joyy)) // 两个电机平均速度(实际速度) - 摇杆前后输入值* 系数 ,就速度差
LQR_speed = (-0.5) * (motor1.shaft_velocity + motor2.shaft_velocity)
speed_target_coeff = 前进后退系数
yaw 偏航角控制
float yaw_angle_control = pid_yaw_angle(yaw_target);
yaw_target = wrobot.joyx * 0.1; // 使用摇杆左右转向值
float yaw_gyro_control = pid_yaw_gyro(YAW_gyro); // yaw 轴的角速度控制
YAW_gyro = (float)mpu6050.getGyroZ();
YAW_output = yaw_angle_control + yaw_gyro_control;
motor1.target = (-0.5) * (LQR_u + YAW_output); // target正数小车向前,负数向后
motor2.target = (-0.5) * (LQR_u - YAW_output);
*/
LQR_distance = (-0.5) * (motor1.shaft_angle + motor2.shaft_angle); // 两个电机的旋转角度(shaft_angle),单位:弧度(rad)实际位移量
LQR_speed = (-0.5) * (motor1.shaft_velocity + motor2.shaft_velocity); // 两个电机角速度(shaft_velocity),单位:弧度 / 秒(rad/s)
LQR_angle = (float)mpu6050.getAngleY(); // mpu6050 pitch 角度,单位:度(°)
LQR_gyro = (float)mpu6050.getGyroY(); // pitch Y轴角速度,单位:度 / 秒(°/s)
angle_control = pid_angle(LQR_angle - angle_zeropoint);
gyro_control = pid_gyro(LQR_gyro);
// 前进后退跳跃的系数都不一样
float speed_target_coeff = 0.1;
if( jump_flag ){ // 跳跃
speed_target_coeff = 0.1;
}else if(wrobot.joyy > 0 ){ // 前进
speed_target_coeff = 0.18;
}else if(wrobot.joyy < 0){ // 后退
speed_target_coeff = 0.11;
}else{
speed_target_coeff = 0.1;
}
speed_control = pid_speed(LQR_speed - speed_target_coeff * lpf_joyy(wrobot.joyy)); // 最大8v
// 检测轮子差速,判断轮子是否离地
unsigned long current_time = millis();
if (current_time - last_speed_record_time >= SPEED_RECORD_INTERVAL) {
robot_speed_diff = LQR_speed - last_lqr_speed;
if(robot_speed_diff > 18.0) // 轮子离地
{
wheel_ground_flag = 0; // 轮子离地标记
// Serial.print("TAKE OFF");
// Serial.print(wheel_ground_flag);
// Serial.print(",robot_speed_diff:");
// Serial.print(robot_speed_diff);
// Serial.print(",jump_flag:");
// Serial.println(jump_flag); // 100 左右
}
if( robot_speed_diff < -9.0) // 轮子着地
{
wheel_ground_flag = 1; // 轮子着地标记
// Serial.print("LANDING:");
// Serial.print(",robot_speed_diff:");
// Serial.print(robot_speed_diff);
// Serial.print(",jump_flag:");
// Serial.println(jump_flag); // 120左右
if( jump_flag ){ // 落地点作为新的位移零点
resetZeroPoint();
}
}
last_lqr_speed = LQR_speed; // 每隔100ms更新一次历史转速
last_speed_record_time = current_time; // 更新记录时间
}
// 重置位移零点和积分情形
// 1.判断摇杆是否没有前后左右运动指令
if ((wrobot.joyx_last != 0 && wrobot.joyx == 0) || (wrobot.joyy_last != 0 && wrobot.joyy == 0))
{
resetZeroPoint();
}
// 2. 运动中实时重置位移零点和积分情形
if (abs(robot_speed_diff) > 10 || (abs(LQR_speed) > 15) // 这两种是启动后自平衡,速度较快
|| wrobot.joyy != 0 || sitting_down )
{
resetZeroPoint();
}
distance_control = pid_distance(LQR_distance - distance_zeropoint);
// 计算 LQR_u
// 必须是在跳跃时jump_flag > 0,LQR_u= 角度控制量 + 角速度控制量
if ((jump_flag > 0 and jump_flag < 120)) // jump_flag:100离地 120着地
{
LQR_u = angle_control + gyro_control; // 角度控制量 + 角速度控制,位移和速度控制分量被忽略
}
else // 当轮部未离地时,LQR_u:4个参数
{
// 当轮部未离地时,LQR_u =角度控制量+角速度控制量+位移控制量+速度控制量
LQR_u = angle_control + gyro_control + distance_control + speed_control;
}
// 小车没有控制的时候自稳定状态
// 控制量lqr_u<5V,前进后退控制量很小, 遥控器无信号输入joyy=0,轮部位移控制正常介入distance_control<4,不处于跳跃后的恢复时期jump_flag=0,以及不是坐下状态
if (abs(LQR_u) < 5 && wrobot.joyy == 0 && abs(distance_control) < 4 && (jump_flag == 0)) // && !sitting_down
{
LQR_u = pid_lqr_u(LQR_u); // 小转矩非线性补偿
angle_zeropoint -= pid_zeropoint(lpf_zeropoint(distance_control)); // 重心自适应
}
else
{
pid_lqr_u.error_prev = 0; // 输出积分清零
}
// 平衡控制参数自适应
if (wrobot.height < 50){
pid_speed.P = 0.7;
}else if (wrobot.height < 64){
pid_speed.P = 0.6;
}else{
pid_speed.P = 0.5;
}
}
// 腿部动作控制(舵机)
void leg_loop()
{
// 如果坐下就不控制腿部
if(sitting_down || jump_flag || jump_pre_flag ){
return; // 跳过后续leg高度计算
}
// 机身大角度,譬如倒地时,强制腿部收缩
if (abs(LQR_angle) > 25.0f)
{
ACC[0] = ACC[1] = 100;
Speed[0] = Speed[1] = 400;
Position[0] = SERVO0_MIN;
Position[1] = SERVO1_MIN;
sms_sts.SyncWritePosEx(ID, 2, Position, Speed, ACC);
return; // 跳过后续leg高度计算
}
ACC[0] = ACC[1] = 100; // max 230
Speed[0] = Speed[1] = 400;
float roll_angle = (float)mpu6050.getAngleX() + roll_adjust; // 加上roll角度偏差
leg_position_add = pid_roll_angle(lpf_roll(roll_angle));
// 左后腿默认等于 wrobot.height
left_height = wrobot.height;
right_height = wrobot.height;
LegHeightDiff = 0; // 初始化 roll 角度补偿值
// 智能计算左右脚高度差, 如果同时有转向和前后指令 ,自动计算腿部高度补偿
if(wrobot.mode == 0){
if (abs(wrobot.joyx) > 10 && abs(wrobot.joyy) > 10)
{
LegHeightDiff = map(abs(wrobot.joyy), 0, 100, 0, 10);
leg_position_add = 0; // 不需要两个脚一样高
}
if (wrobot.joyx > 10 && abs(wrobot.joyy) > 10)
{
// 左脚抬高
left_height = wrobot.height + LegHeightDiff ;
// Serial.print("左脚抬高");
}
else if (wrobot.joyx < -10 && abs(wrobot.joyy) > 10)
{
// 右脚抬高
right_height = wrobot.height + LegHeightDiff;
// Serial.print("右脚抬高");
}
}
// 根据新的高度计算舵机位置
// id[0] 正面看右手,id[1] 正面看左手
// 2047是舵机中间位置,8.4是系数,32是默认高度,最高80,最低32
// const int LEG_HEIGHT_BASE 这值越小,机身越高,实测范围 0(最高),52(最低)
Position[0] = 2047 + 8.4 * (left_height - leg_height_base ) - leg_position_add;
Position[1] = 2047 - 8.4 * (right_height - leg_height_base ) - leg_position_add;
// Serial.print("roll_angle:");
// Serial.print(roll_angle);
// Serial.print(",Position[0]:");
// Serial.print(Position[0]);
// Serial.print(",Position[1]:");
// Serial.println(Position[1]);
// 限制舵机位置范围
if (Position[0] < SERVO0_MIN) // 舵机1最低
Position[0] = SERVO0_MIN;
else if (Position[0] > SERVO0_MAX) // 舵机1最高
Position[0] = SERVO0_MAX;
if (Position[1] > SERVO1_MIN) // 舵机2最低(和舵机1方向是反的)
Position[1] = SERVO1_MIN;
else if (Position[1] < SERVO1_MAX) // 舵机2最高
Position[1] = SERVO1_MAX;
sms_sts.SyncWritePosEx(ID, 2, Position, Speed, ACC);
}
// 收缩小腿蓄能
void jump_charge() // 腿部弯曲(蓄能)
{
ACC[0] = 250;
ACC[1] = 250;
Speed[0] = 400;
Speed[1] = 400;
Position[0] = (SERVO0_MIN + 80); // 不能是SERVO1_MIN值,防止腿部碰撞
Position[1] = (SERVO1_MIN - 80);
sms_sts.SyncWritePosEx(ID, 2, Position, Speed, ACC);
}
// 跳跃控制
void jump_loop()
{
if ((wrobot.jump == 1) && (jump_pre_flag == 0))
{
jump_charge();
jump_pre_flag = 1;
}
if(jump_pre_flag > 0 ){
jump_pre_flag ++;
}
if(jump_pre_flag){
if(!sms_sts.ReadMove(ID[0]) && !sms_sts.ReadMove(ID[1])){
jump_flag = 1;
jump_pre_flag = 0;
// 跳跃开始
ACC[0] = 0;
ACC[1] = 0;
Speed[0] = 0;
Speed[1] = 0;
Position[0] = SERVO0_MAX + 20; // 极限距离
Position[1] = SERVO1_MAX - 20;
sms_sts.SyncWritePosEx(ID, 2, Position, Speed, ACC);
}
}
if (jump_flag > 0) // 跳跃计数;
{
jump_flag ++;
}
// 跳跃中,腿部缩起来
if( (jump_flag > 30) && (jump_flag < 35) )
{
ACC[0] = 0;
ACC[1] = 0;
Speed[0] = 0;
Speed[1] = 0;
Position[0] = (SERVO0_MIN + 100); // 100 约等于食指宽度
Position[1] = (SERVO1_MIN - 100);
sms_sts.SyncWritePosEx(ID, 2, Position, Speed, ACC);
}
// 轮子着地
if (jump_flag > 200) // 2000=6s ,1s=2000/6=333.33
{
jump_flag = 0; // 跳跃过程结束
}
}
// yaw轴转向控制 (转向控制)
void yaw_loop()
{
// 跳跃中,YAW_output 设为0,避免干扰左右旋转
if(jump_flag)
{
YAW_output = 0;
return;
}
// 1. 根据abs(wrobot.joyx),动态设置yaw_target系数
float coeff = map( abs(wrobot.joyx), 0, 100, 10, 100) / 100.0f;
coeff = constrain(coeff, 0.1f, 1.0f);
float yaw_target = wrobot.joyx * coeff;
// 2. 根据abs(wrobot.joyx),提高PID响应:临时放大yaw_angle_control(核心突破上限)
float yaw_angle_control_coeff = 1.0f;
if( wrobot.joyy == 0 && abs(wrobot.joyx) > 10){ // 原地打转
yaw_angle_control_coeff = map( abs(wrobot.joyx), 0, 100, 1.0, 2.0);
// 根据转向速度,智能计算wrobot.height
// abs(wrobot.joyx) 范围0到100,默认0,输出范围20-50,默认 30
// 反向映射:绝对值30→60,绝对值100→0(线性过渡)
wrobot.height = map(abs(wrobot.joyx), 0, 100, 50, 30);
// 强制约束输出在0~60范围内(防止异常值)
wrobot.height = constrain(wrobot.height, 30, 50);
// 使用低波过滤器,平滑数据
wrobot.height = lpf_height(wrobot.height);
// 增加rgb效果
if(abs(wrobot.joyx) > 60){
handleLEDBlink();
}
}
float yaw_angle_control = pid_yaw_angle(yaw_target) * yaw_angle_control_coeff;
// 限制yaw_angle_control上限,避免超压(按电机7.4V反推,设为12较合适)
yaw_angle_control = constrain(yaw_angle_control, -12.0f, 12.0f);
YAW_gyro = (float)mpu6050.getGyroZ(); // 左右偏航角速度,用于纠正小车前后走直线时的角度偏差
float yaw_gyro_control = pid_yaw_gyro(YAW_gyro);
YAW_output = yaw_angle_control + yaw_gyro_control;
}
// 电压检测初始化
void adc_calibration_init()
{
if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP) == ESP_OK) {
printf("eFuse Two Point: Supported\n");
} else {
printf("eFuse Two Point: NOT supported\n");
}
// Check Vref is burned into eFuse
if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK) {
printf("eFuse Vref: Supported\n");
} else {
printf("eFuse Vref: NOT supported\n");
}
}
// 电压检测
void bat_check()
{
if (bat_check_num > 1000)
{
// 电压读取
uint32_t sum = 0;
sum = analogRead(BAT_PIN);
uint32_t voltage = esp_adc_cal_raw_to_voltage(sum, &adc_chars);
double battery = (voltage * 3.97) / 1000.0;
// Serial.println(battery);
// 电量显示
if (battery >= 7.8)
digitalWrite(LED_BAT, HIGH);
else
digitalWrite(LED_BAT, LOW);
bat_check_num = 0;
}
else
bat_check_num++;
}
// 重置距离零点
void resetZeroPoint() {
distance_zeropoint = LQR_distance;
pid_lqr_u.error_prev = 0;
}
// 添加LED闪烁处理函数
void handleLEDBlink() {
if (!isBlinking) return;
unsigned long currentMillis = millis();
if (currentMillis - previousBlinkMillis >= 100) { // 每100ms切换一次状态,实现快速闪烁
previousBlinkMillis = currentMillis;
if (blinkState == 0) {
// LED亮
fill_solid(leds, NUM_LEDS, blinkColors[blinkCount]);
FastLED.show();
blinkState = 1;
} else {
// LED灭
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
blinkState = 0;
blinkCount++;
// 检查是否完成6次闪烁
if (blinkCount >= 6) {
isBlinking = false;
blinkCount = 0;
// 闪烁完成后保持熄灭状态
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
}
}
}
}
/*========================================
* 控制器数据处理(共用函数)
* ========================================*/
void processControllerData(const XboxControllerNotificationParser& remoteData) {
/*
String str = String(xboxController.xboxNotif.btnY) + "," + // Y键
String(xboxController.xboxNotif.btnX) + "," + // X键
String(xboxController.xboxNotif.btnB) + "," + // B键
String(xboxController.xboxNotif.btnA) + "," + // A键
String(xboxController.xboxNotif.btnLB) + "," + // 左肩键
String(xboxController.xboxNotif.btnRB) + "," + // 右肩键
String(xboxController.xboxNotif.btnSelect) + "," + // 中间十字 左键 试图键
String(xboxController.xboxNotif.btnStart) + "," + // 中间十字 右键 菜单键
String(xboxController.xboxNotif.btnXbox) + "," + // 中间十字 上键 home键
String(xboxController.xboxNotif.btnShare) + "," + // 中间十字 下键 截图键
String(xboxController.xboxNotif.btnLS) + "," + // 左摇杆键
String(xboxController.xboxNotif.btnRS) + "," + // 右摇杆键
String(xboxController.xboxNotif.btnDirUp) + "," + // 上键
String(xboxController.xboxNotif.btnDirRight) + "," + // 右键
String(xboxController.xboxNotif.btnDirDown) + "," + // 下键
String(xboxController.xboxNotif.btnDirLeft) + "," + // 左键
String(xboxController.xboxNotif.joyLHori) + "," + // 左摇杆 水平轴 (左-右 0 -65535)
String(xboxController.xboxNotif.joyLVert) + "," + // 左摇杆 垂直轴 (上-下 0 -65535)
String(xboxController.xboxNotif.joyRHori) + "," + // 右摇杆 水平轴 (左-右 0 -65535)
String(xboxController.xboxNotif.joyRVert) + "," + // 右摇杆 垂直轴 (上-下 0 -65535)
String(xboxController.xboxNotif.trigLT) + "," + // 左扳机键 (松开- 按下 0 -1023)
String(xboxController.xboxNotif.trigRT) ; // 右扳机键 (松开- 按下 0 -1023)
Serial.println(str);
*/
// rgb led 上键 闪烁6次:红红黄黄绿绿
if(remoteData.btnDirUp && !isBlinking){
isBlinking = true;
blinkCount = 0;
blinkState = 0;
previousBlinkMillis = millis();
}
// 坐下 back键
if (remoteData.btnLB && robot_enabled) {
robot_enabled = false;
sitting_down = 1; // 标记开始坐下动作保存当前的角度零点
}
// 起立 start键
if (remoteData.btnRB && !robot_enabled) {
robot_enabled = true;
sitting_down = 0; // 清除坐下动作标记
}
// 修改后代码(保留摇杆指令,仅禁止起升/跳跃)
if (!robot_enabled || sitting_down) {
wrobot.jump = 0; // 禁止跳跃
wrobot.roll = 0; // 禁止左右摇摆(起升相关)
wrobot.height = 0; // 禁止高度调整(起升相关)
}
// 跳跃状态不接受指令
if( jump_flag > 0){
wrobot.jump = 0; // 强制设为“不跳跃”
wrobot.joyx = 0; // 清空左右转向指令
wrobot.joyy = 0; // 清空前后移动指令
wrobot.roll = 0; // 清空左右摇摆指令
return;
}
// 前后移动 右摇杆Y轴
int convertedJ2PotY = constrain(
map(remoteData.joyRVert, 0, 65535, 100, -100), // 原始0~65535,目标100~-100
-100, 100
);
if(abs(convertedJ2PotY) > deadZone) { // 前后运动 -100 到 100
wrobot.joyy = convertedJ2PotY;
}else{
wrobot.joyy = 0;
}
if(wrobot.mode == 0) // 智能模式
{
// 左右转向 左摇杆X轴:先映射到-100~100,再按比例放大到-100~100
int convertedJ1PotX = constrain(
map(remoteData.joyLHori, 0, 65535, -100, 100),
-100, 100
);
if(abs(convertedJ1PotX) > deadZone) { // 左右运动 -100 到 100
wrobot.joyx = convertedJ1PotX;
} else {
wrobot.joyx = 0;
}
// 智能计算wrobot.height ,根据前后速度,
// convertedJ2PotY 范围-100到100,默认0,输出范围30(最矮)-50(最高), 默认 50
wrobot.height = map( abs(convertedJ2PotY), 0, 100, 50, 30);
wrobot.height = constrain(wrobot.height, 30, 50);
wrobot.height = lpf_height(wrobot.height);
}
else if(wrobot.mode == 1) // 手动模式
{
// 摇摆角度 左摇杆X轴,先映射到-60~60,再按比例放大到-100~100
int convertedJ1PotX = constrain(
map(remoteData.joyLHori, 0, 65535, -30, 30) * (100.0/30), // 先映射到-30~30,再按比例放大到-100~100
-100, 100
);
if(abs(convertedJ1PotX) > (deadZone * 2) ) { // 防止同一个摇杆上下左右移动相互干扰
roll_adjust = convertedJ1PotX;
}else{
roll_adjust = 0;
}
// 上下高度 左摇杆Y轴:原始0-65535 → 目标MAX到MIN
int convertedJ1PotY = constrain(
map(remoteData.joyLVert, 0, 65535, 100, -100), // 原始范围0-65535,目标范围100到-100,默认中间0
-100, 100
);
if(abs(convertedJ1PotY) < (deadZone * 2) ) {
convertedJ1PotY = 0;
}
wrobot.height = map( convertedJ1PotY, -100, 100, 30, 70);
wrobot.height = constrain(wrobot.height, 30, 70);
wrobot.height = lpf_height(wrobot.height);
// 左右转向 右摇杆X轴:原始0-960 → 目标-100到100
int convertedJ2PotX = constrain(
map(remoteData.joyRHori, 0, 65535, -100, 100), // 原始范围0-65535,目标范围-100到100,默认中间0
-100, 100
);
if(abs(convertedJ2PotX) > ( deadZone * 2)) {
wrobot.joyx = convertedJ2PotX;
}else{
wrobot.joyx = 0;
}
}
// 跳跃 右摇杆按键
if (remoteData.btnRS && !last_btnbtnRS) { // 1=true,0=false
wrobot.jump = 1; // 跳跃
}else{
wrobot.jump = 0; // 不跳跃
}
last_btnbtnRS = remoteData.btnRS;
// 切换智能模式和手动模式
if (remoteData.btnY) { // 1:左摇杆上下摇摆控制(手动)
wrobot.mode = 1;
}
if (remoteData.btnA) { // 0:左摇杆左右转向控制(智能)
wrobot.mode = 0;
roll_adjust = 0.0f;
leg_height_base = 20.0f;
}
}
/* Xbox数据处理函数(直接传递原生数据) */
void process_xbox_notif(){
xboxController.onLoop();
if (xboxController.isConnected()){
if (xboxController.isWaitingForFirstNotification()){
Serial.println("Waiting for first Xbox data...");
}else{
// Serial.println("Xbox controller connected !!!"); // 手柄蓝灯常亮
processControllerData(xboxController.xboxNotif);
}
}
else{
Serial.println("Xbox controller not connected");
}
}