commit 6ccbc7faf5bbdb30f8e6debeb375a6874036ca06 Author: devdesk Date: Thu Feb 5 15:08:47 2026 +0200 feat: add ESP32 BTS7960 motor controller with web interface - Implement MotorController class with PWM speed control (0-100%) - Add bidirectional control (forward/reverse) via LEDC PWM at 20kHz - Include optional current sensing and stall detection - Create responsive web UI with speed slider and direction buttons - Configure WiFi with static IP (10.81.2.185) - Use built-in WebServer library for ESP32 Arduino 3.x compatibility diff --git a/.agents/rules-architect/AGENTS.md b/.agents/rules-architect/AGENTS.md new file mode 100644 index 0000000..4aa5716 --- /dev/null +++ b/.agents/rules-architect/AGENTS.md @@ -0,0 +1,7 @@ +# Architect Mode Rules + +## Project Structure + +- PlatformIO standard structure: `src/main.cpp`, `include/`, `lib/` +- Consider separate motor control library in `lib/` for reusability +- BTS7960 can drive two motors independently - plan for dual motor control interface diff --git a/.agents/rules-ask/AGENTS.md b/.agents/rules-ask/AGENTS.md new file mode 100644 index 0000000..98f8956 --- /dev/null +++ b/.agents/rules-ask/AGENTS.md @@ -0,0 +1,7 @@ +# Ask Mode Rules + +## Documentation Context + +- Main hardware reference: https://deepbluembedded.com/arduino-bts7960-dc-motor-driver/ +- ESP32 LOLIN32 pinout differs from standard ESP32 DevKit - verify pin assignments +- BTS7960 datasheet needed for current sensing calculations diff --git a/.agents/rules-code/AGENTS.md b/.agents/rules-code/AGENTS.md new file mode 100644 index 0000000..5eb8055 --- /dev/null +++ b/.agents/rules-code/AGENTS.md @@ -0,0 +1,8 @@ +# Code Mode Rules + +## ESP32/PlatformIO Specifics + +- Use `LEDC` peripheral for PWM on ESP32 (not analogWrite) +- BTS7960 PWM frequency: 1-25kHz optimal, 20kHz recommended to reduce motor noise +- Enable pins (R_EN, L_EN) must be HIGH before PWM signals work +- Current sense pins (R_IS, L_IS) output analog voltage proportional to motor current diff --git a/.agents/rules-debug/AGENTS.md b/.agents/rules-debug/AGENTS.md new file mode 100644 index 0000000..2054e7e --- /dev/null +++ b/.agents/rules-debug/AGENTS.md @@ -0,0 +1,7 @@ +# Debug Mode Rules + +## ESP32 Debugging + +- Use `pio device monitor -b 115200` for serial debugging +- BTS7960 current sense pins can diagnose motor stall/overload conditions +- Check R_IS/L_IS for overcurrent (>43A causes fault) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8f44d09 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md + +This file provides guidance to agents when working with code in this repository. + +## Project Overview + +PlatformIO project for ESP32 LOLIN32 Rev1 controlling BTS7960 dual H-bridge motor driver (12V DC). + +## Build Commands + +```bash +pio run # Build +pio run -t upload # Build and upload +pio device monitor # Serial monitor (115200 baud) +``` + +## Hardware-Specific Notes + +- **Board**: ESP32 LOLIN32 Rev1 (use `lolin32` in platformio.ini) +- **Motor Driver**: BTS7960 dual H-bridge - requires 4 GPIO pins (RPWM=25, LPWM=26, R_EN=27, L_EN=14) +- **Power**: 12V DC input to motor driver - logic level is 3.3V compatible with ESP32 +- **WiFi**: Connects to SSID 'tami' with static IP 10.81.2.185 +- **Reference**: https://deepbluembedded.com/arduino-bts7960-dc-motor-driver/ + +## Non-Obvious Notes + +- Use built-in `WebServer` library (NOT ESPAsyncWebServer - has enum compatibility issues with ESP32 Arduino 3.x) +- WebServer requires `handleClient()` call in loop() - unlike async version +- LEDC PWM at 20kHz reduces motor noise +- Enable pins (R_EN, L_EN) must be HIGH before PWM works +- **BTS7960 is 3.3V compatible**: Inputs are TTL/CMOS compatible (VIL<0.8V, VIH>2.0V) - ESP32 GPIOs work directly without level shifters (per BTN7960 datasheet section 5.4.1) diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..820ceef --- /dev/null +++ b/include/config.h @@ -0,0 +1,42 @@ +#ifndef CONFIG_H +#define CONFIG_H + +// WiFi Configuration +#define WIFI_SSID "tami" +#define WIFI_PASSWORD "" + +// Static IP Configuration +#define STATIC_IP IPAddress(10, 81, 2, 185) +#define GATEWAY IPAddress(10, 81, 2, 1) +#define SUBNET IPAddress(255, 255, 255, 0) +#define DNS IPAddress(10, 81, 2, 1) + +// BTS7960 Pin Definitions +#define RPWM_PIN 25 // Right PWM (Forward) +#define LPWM_PIN 26 // Left PWM (Reverse) +#define R_EN_PIN 27 // Right Enable +#define L_EN_PIN 14 // Left Enable +#define R_IS_PIN 34 // Right Current Sense (ADC input only) +#define L_IS_PIN 35 // Left Current Sense (ADC input only) + +// PWM Configuration +#define PWM_FREQ 20000 // 20kHz - reduces motor noise +#define PWM_RESOLUTION 8 // 8-bit resolution (0-255) +#define PWM_CHANNEL_R 0 // LEDC channel for RPWM +#define PWM_CHANNEL_L 1 // LEDC channel for LPWM + +// Current Sense Configuration +// BTS7960 current sense ratio: 8500:1 (kilo-amps) +// With 1kΩ resistor on IS pin: V = I_motor / 8500 +// ESP32 ADC: 12-bit (0-4095), 0-3.3V +#define CURRENT_SENSE_RATIO 8500.0f // Amps to sense current ratio +#define SENSE_RESISTOR 1000.0f // 1kΩ sense resistor (ohms) +#define ADC_MAX 4095.0f // 12-bit ADC max value +#define ADC_VREF 3.3f // ADC reference voltage +#define STALL_CURRENT_THRESHOLD 5.0f // Current (amps) that indicates stall +#define STALL_DETECT_TIME_MS 500 // Time to confirm stall (ms) + +// Web Server +#define HTTP_PORT 80 + +#endif diff --git a/include/motor.h b/include/motor.h new file mode 100644 index 0000000..a77d821 --- /dev/null +++ b/include/motor.h @@ -0,0 +1,41 @@ +#ifndef MOTOR_H +#define MOTOR_H + +#include +#include "config.h" + +class MotorController { +public: + void begin(); + void setSpeed(int speed); // 0-100 percentage + void setDirection(int dir); // -1=reverse, 0=stop, 1=forward + void stop(); + void update(); // Call in loop() for stall detection + + int getSpeed(); + int getDirection(); + float getCurrentRight(); // Current in amps (forward direction) + float getCurrentLeft(); // Current in amps (reverse direction) + float getCurrentActive(); // Current from active direction + bool isStalled(); // True if stall detected + + // Callback for stall events + void setStallCallback(void (*callback)(float current)); + +private: + int _speed = 0; + int _direction = 0; + float _currentRight = 0; + float _currentLeft = 0; + bool _stalled = false; + unsigned long _stallStartTime = 0; + void (*_stallCallback)(float current) = nullptr; + + void applyMotorState(); + float readCurrentSense(int pin); + void checkStall(); +}; + +extern MotorController motor; + +#endif diff --git a/include/webserver.h b/include/webserver.h new file mode 100644 index 0000000..2fb418d --- /dev/null +++ b/include/webserver.h @@ -0,0 +1,7 @@ +#ifndef WEBSERVER_H +#define WEBSERVER_H + +void setupWebServer(); +void handleWebServer(); + +#endif diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..62552d8 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,14 @@ +; PlatformIO Project Configuration File +; ESP32 LOLIN32 Rev1 with BTS7960 Motor Driver + +[env:lolin32] +platform = espressif32 +board = lolin32 +framework = arduino +monitor_speed = 115200 + +; No external libraries needed - using built-in WebServer + +; Build flags +build_flags = + -D CORE_DEBUG_LEVEL=0 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..dd65463 --- /dev/null +++ b/readme.md @@ -0,0 +1,8 @@ +### inital prompt + +start platformio project. +using esp32 lolin32 rev1. +we will connect a dual h-bdrige module based on BTS7960 module. +12v dc input. + +module guide - https://deepbluembedded.com/arduino-bts7960-dc-motor-driver/ diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..cba2dc5 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,62 @@ +#include +#include +#include "config.h" +#include "motor.h" +#include "webserver.h" + +void setupWiFi() { + Serial.println("Connecting to WiFi..."); + + // Configure static IP + if (!WiFi.config(STATIC_IP, GATEWAY, SUBNET, DNS)) { + Serial.println("Static IP configuration failed!"); + } + + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 30) { + delay(500); + Serial.print("."); + attempts++; + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.println("\nWiFi connected!"); + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + } else { + Serial.println("\nWiFi connection failed!"); + Serial.println("Restarting in 5 seconds..."); + delay(5000); + ESP.restart(); + } +} + +void setup() { + Serial.begin(115200); + delay(1000); + + Serial.println("\n============================="); + Serial.println(" BTS7960 Motor Controller"); + Serial.println("=============================\n"); + + // Initialize motor controller + motor.begin(); + + // Connect to WiFi + setupWiFi(); + + // Start web server + setupWebServer(); + + Serial.println("\nReady! Access the control panel at:"); + Serial.print("http://"); + Serial.println(WiFi.localIP()); +} + +void loop() { + handleWebServer(); + motor.update(); // Update current sensing and stall detection + delay(1); +} diff --git a/src/motor.cpp b/src/motor.cpp new file mode 100644 index 0000000..83f4652 --- /dev/null +++ b/src/motor.cpp @@ -0,0 +1,182 @@ +#include "motor.h" + +// Set to true to enable current sensing (requires R_IS and L_IS connected) +#define CURRENT_SENSING_ENABLED false + +MotorController motor; + +void MotorController::begin() { + // Configure enable pins as outputs + pinMode(R_EN_PIN, OUTPUT); + pinMode(L_EN_PIN, OUTPUT); + + // Enable the H-bridge + digitalWrite(R_EN_PIN, HIGH); + digitalWrite(L_EN_PIN, HIGH); + + // Configure LEDC PWM channels for ESP32 + ledcSetup(PWM_CHANNEL_R, PWM_FREQ, PWM_RESOLUTION); + ledcSetup(PWM_CHANNEL_L, PWM_FREQ, PWM_RESOLUTION); + + // Attach PWM channels to GPIO pins + ledcAttachPin(RPWM_PIN, PWM_CHANNEL_R); + ledcAttachPin(LPWM_PIN, PWM_CHANNEL_L); + +#if CURRENT_SENSING_ENABLED + // Configure current sense pins as analog inputs + // GPIO34 and GPIO35 are input-only pins, perfect for ADC + analogSetAttenuation(ADC_11db); // Full range 0-3.3V + pinMode(R_IS_PIN, INPUT); + pinMode(L_IS_PIN, INPUT); + Serial.println("Current sensing enabled"); +#endif + + // Start stopped + stop(); + + Serial.println("Motor controller initialized"); +} + +void MotorController::setSpeed(int speed) { + _speed = constrain(speed, 0, 100); + _stalled = false; // Reset stall state on new command + _stallStartTime = 0; + applyMotorState(); +} + +void MotorController::setDirection(int dir) { + _direction = constrain(dir, -1, 1); + _stalled = false; // Reset stall state on new command + _stallStartTime = 0; + applyMotorState(); +} + +void MotorController::stop() { + _speed = 0; + _direction = 0; + _stalled = false; + _stallStartTime = 0; + ledcWrite(PWM_CHANNEL_R, 0); + ledcWrite(PWM_CHANNEL_L, 0); +} + +void MotorController::update() { +#if CURRENT_SENSING_ENABLED + // Read current sensors + _currentRight = readCurrentSense(R_IS_PIN); + _currentLeft = readCurrentSense(L_IS_PIN); + + // Check for stall condition + checkStall(); +#endif +} + +int MotorController::getSpeed() { + return _speed; +} + +int MotorController::getDirection() { + return _direction; +} + +float MotorController::getCurrentRight() { + return _currentRight; +} + +float MotorController::getCurrentLeft() { + return _currentLeft; +} + +float MotorController::getCurrentActive() { + if (_direction > 0) { + return _currentRight; + } else if (_direction < 0) { + return _currentLeft; + } + return 0.0f; +} + +bool MotorController::isStalled() { + return _stalled; +} + +void MotorController::setStallCallback(void (*callback)(float current)) { + _stallCallback = callback; +} + +void MotorController::applyMotorState() { + // Convert percentage to 8-bit PWM value + int pwmValue = map(_speed, 0, 100, 0, 255); + + if (_direction == 0 || _speed == 0) { + // Stop - both PWM outputs off + ledcWrite(PWM_CHANNEL_R, 0); + ledcWrite(PWM_CHANNEL_L, 0); + } else if (_direction > 0) { + // Forward - RPWM active, LPWM off + ledcWrite(PWM_CHANNEL_R, pwmValue); + ledcWrite(PWM_CHANNEL_L, 0); + } else { + // Reverse - LPWM active, RPWM off + ledcWrite(PWM_CHANNEL_R, 0); + ledcWrite(PWM_CHANNEL_L, pwmValue); + } + + Serial.printf("Motor: dir=%d, speed=%d%%, pwm=%d\n", _direction, _speed, pwmValue); +} + +float MotorController::readCurrentSense(int pin) { +#if CURRENT_SENSING_ENABLED + // Read ADC value (12-bit, 0-4095) + int adcValue = analogRead(pin); + + // Convert to voltage (0-3.3V) + float voltage = (adcValue / ADC_MAX) * ADC_VREF; + + // Calculate current + // IS pin outputs: I_sense = I_load / CURRENT_SENSE_RATIO + // With sense resistor: V = I_sense * R_sense + // Therefore: I_load = V * CURRENT_SENSE_RATIO / R_sense + float current = (voltage * CURRENT_SENSE_RATIO) / SENSE_RESISTOR; + + return current; +#else + return 0.0f; +#endif +} + +void MotorController::checkStall() { +#if CURRENT_SENSING_ENABLED + // Only check stall when motor should be running + if (_direction == 0 || _speed == 0) { + _stalled = false; + _stallStartTime = 0; + return; + } + + float activeCurrent = getCurrentActive(); + + // Check if current exceeds stall threshold + if (activeCurrent > STALL_CURRENT_THRESHOLD) { + if (_stallStartTime == 0) { + // Start timing potential stall + _stallStartTime = millis(); + } else if ((millis() - _stallStartTime) > STALL_DETECT_TIME_MS) { + // Stall confirmed + if (!_stalled) { + _stalled = true; + Serial.printf("STALL DETECTED! Current: %.2fA\n", activeCurrent); + + // Call callback if registered + if (_stallCallback != nullptr) { + _stallCallback(activeCurrent); + } + } + } + } else { + // Current normal, reset stall detection + _stallStartTime = 0; + _stalled = false; + } +#endif +} diff --git a/src/webserver.cpp b/src/webserver.cpp new file mode 100644 index 0000000..cb97731 --- /dev/null +++ b/src/webserver.cpp @@ -0,0 +1,215 @@ +#include +#include +#include "motor.h" +#include "config.h" + +WebServer server(HTTP_PORT); + +// HTML page for motor control +const char index_html[] PROGMEM = R"rawliteral( + + + + + Motor Control + + + +
+

Motor Control

+ +
+ Direction: STOPPED +
+ +
+ +
0%
+ +
+ +
+ + + +
+
+ + + + +)rawliteral"; + +void handleRoot() { + server.send(200, "text/html", index_html); +} + +void handleSpeed() { + if (server.hasArg("value")) { + int speed = server.arg("value").toInt(); + motor.setSpeed(speed); + server.send(200, "text/plain", "OK"); + } else { + server.send(400, "text/plain", "Missing value"); + } +} + +void handleDirection() { + if (server.hasArg("value")) { + int dir = server.arg("value").toInt(); + motor.setDirection(dir); + server.send(200, "text/plain", "OK"); + } else { + server.send(400, "text/plain", "Missing value"); + } +} + +void handleStop() { + motor.stop(); + server.send(200, "text/plain", "OK"); +} + +void handleStatus() { + String json = "{\"speed\":" + String(motor.getSpeed()) + + ",\"direction\":" + String(motor.getDirection()) + + ",\"currentR\":" + String(motor.getCurrentRight(), 2) + + ",\"currentL\":" + String(motor.getCurrentLeft(), 2) + + ",\"stalled\":" + (motor.isStalled() ? "true" : "false") + "}"; + server.send(200, "application/json", json); +} + +void setupWebServer() { + server.on("/", handleRoot); + server.on("/speed", handleSpeed); + server.on("/direction", handleDirection); + server.on("/stop", handleStop); + server.on("/status", handleStatus); + + server.begin(); + Serial.println("Web server started on port 80"); +} + +void handleWebServer() { + server.handleClient(); +}