diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1d5d348 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,28 @@ +set(PROBOT_SRCS + src/core/runtime.cpp + src/core/scheduler.cpp + src/core/wdt.cpp + src/io/ui_input.cpp + src/platform/esp32s3/drivers/builtin_led_ws2812.cpp + src/probot/devices/motors/Boardoza_BA6208.cpp + src/probot/devices/motors/boardoza_ba6208_driver.cpp + src/probot/devices/motors/boardoza_vnh_motor_driver.cpp + src/probot/logging/logger.cpp + src/probot/logging/telemetry_profiles.cpp + src/probot/logging/wifi_transport_esp32.cpp + src/probot/robot/system.cpp + src/probot/sensors/imu/mpu6050.cpp +) + +idf_component_register( + SRCS ${PROBOT_SRCS} + INCLUDE_DIRS "src" + REQUIRES arduino +) + +target_compile_definitions(${COMPONENT_TARGET} + PUBLIC + ARDUINO=10820 + ESP32 + ESP32S3 +) diff --git a/Makefile b/Makefile index e84f765..9430c0b 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,10 @@ FQBN ?= esp32:esp32:esp32s3 PORT ?= /dev/ttyACM0 BAUD ?= 115200 ARDUINO_CLI ?= arduino-cli +PYTHON ?= python3 + +VERSION_FILE := $(CURDIR)/VERSION +VERSION_SYNC_SCRIPT := $(CURDIR)/tools/sync_version.py # Examples EXAMPLES_DIR := $(CURDIR)/examples @@ -15,6 +19,9 @@ BUILD_DIR_BASE := $(CURDIR)/.build TEST_STUB_DIR := $(CURDIR)/tests/stubs TEST_SOURCES := $(filter %.cpp,$(wildcard $(CURDIR)/tests/*.cpp)) +TEST_EXTRA_SOURCES := \ + $(CURDIR)/src/probot/logging/logger.cpp \ + $(CURDIR)/src/probot/logging/telemetry_profiles.cpp # Common flags EXTRA_FLAGS_COMMON := -DESP32S3 -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 @@ -54,6 +61,7 @@ build-all: _build_single: @echo "==> Building $(EXAMPLE)" + $(PYTHON) $(VERSION_SYNC_SCRIPT) $(ARDUINO_CLI) compile --fqbn $(FQBN) --warnings all \ --library $(CURDIR) \ --build-path $(BUILD_DIR_BASE)/$(EXAMPLE) \ @@ -78,8 +86,11 @@ boards: libs: arduino-cli lib install "Adafruit NeoPixel" +version-sync: + $(PYTHON) $(VERSION_SYNC_SCRIPT) + tests/control_tests: $(TEST_SOURCES) - g++ -std=c++17 -Wall -Wextra -pedantic -I src -I $(TEST_STUB_DIR) -DPROBOT_CLM_NOLOG=1 -DPROBOT_SCHED_NOLOG=1 -o $@ $(TEST_SOURCES) + g++ -std=c++17 -Wall -Wextra -pedantic -I src -I $(TEST_STUB_DIR) -DPROBOT_CLM_NOLOG=1 -DPROBOT_SCHED_NOLOG=1 -DPROBOT_LOGGER_NO_SCHED_ATTACH=1 -o $@ $(TEST_SOURCES) $(TEST_EXTRA_SOURCES) test: build tests/control_tests ./tests/control_tests diff --git a/README.md b/README.md index 58caac8..ba07ee0 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,16 @@ Her örnek doğrudan çalışır durumda ve yorumlarla açıklanmıştır. --- +## Platform Desteği + +- **Arduino IDE / arduino-cli:** `library.properties` ve `Makefile` üzerinden doğrudan desteklenir. `make build EXAMPLE=TankDriveDemo` komutu, `arduino-cli` ile örnekleri derler. +- **PlatformIO (Arduino framework):** Kütüphaneyi `lib_deps = /path/to/probot-lib` ya da Git URL'siyle ekleyin. `library.json` sürüm bilgisi `VERSION` dosyasından otomatik güncellenir. +- **ESP-IDF + Arduino bileşeni:** Depoyu IDF projenizin `components/` klasörüne yerleştirip `idf.py build` çalıştırabilirsiniz. `idf_component.yml` otomatik olarak Arduino bileşenine bağımlıdır; `app_main` içinde `probot::runtime_setup()` çağırarak Arduino dışındaki uygulamalarda da aynı robot yaşam döngüsünü başlatabilirsiniz. + +Tüm platformlarda sürüm numarası `VERSION` dosyasından yönetilir; `make version-sync` çağrısı metadata dosyalarını günceller. + +--- + ## Ne içeriyor? Kütüphane şunları sağlar: @@ -138,6 +148,16 @@ Every example runs out of the box and is documented with inline comments. --- +## Platform Support + +- **Arduino IDE / arduino-cli:** build examples with `make build EXAMPLE=TankDriveDemo`; metadata comes from `library.properties`. +- **PlatformIO (Arduino framework):** add `lib_deps = /path/to/probot-lib` or the Git URL; `library.json` stays in sync with `VERSION`. +- **ESP-IDF with the Arduino component:** drop the repository under your project's `components/` directory (or use `idf_component.yml` via the component manager) and call `idf.py build`. Invoke `probot::runtime_setup()` from `app_main()` to reuse the Arduino lifecycle on pure ESP-IDF projects. + +`make version-sync` keeps all manifests aligned with the single `VERSION` file. + +--- + ## What's inside? The library provides: diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..845639e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.4 diff --git a/examples/AutonomousDemo/AutonomousDemo.ino b/examples/AutonomousDemo/AutonomousDemo.ino new file mode 100644 index 0000000..1da665d --- /dev/null +++ b/examples/AutonomousDemo/AutonomousDemo.ino @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include + +// Otonom tank demo için iki adet VNH sürücüsü. +static constexpr int L_INA = 33; +static constexpr int L_INB = 34; +static constexpr int L_PWM = 35; +static constexpr int L_ENA = -1; +static constexpr int L_ENB = -1; + +static constexpr int R_INA = 36; +static constexpr int R_INB = 37; +static constexpr int R_PWM = 38; +static constexpr int R_ENA = -1; +static constexpr int R_ENB = -1; + +static probot::motor::BoardozaVNHMotorDriver leftDriver(L_INA, L_INB, L_PWM, L_ENA, L_ENB); +static probot::motor::BoardozaVNHMotorDriver rightDriver(R_INA, R_INB, R_PWM, R_ENA, R_ENB); +static probot::sensors::NullEncoder leftEncoder; +static probot::sensors::NullEncoder rightEncoder; + +static const probot::control::PidConfig kPid{.kp = 0.32f, .ki = 0.015f, .kd = 0.0f, .kf = 0.0f, + .out_min = -1.0f, .out_max = 1.0f}; +static probot::control::PID pidLeft(kPid); +static probot::control::PID pidRight(kPid); +static probot::control::ClosedLoopMotor motorLeft(&leftEncoder, &pidLeft, &leftDriver, 1.0f, 1.0f); +static probot::control::ClosedLoopMotor motorRight(&rightEncoder, &pidRight, &rightDriver, 1.0f, 1.0f); +static probot::drive::BasicTankDrive chassis(&motorLeft, &motorRight); + +PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); + +enum class AutoStep { + kDriveForward, + kPause, + kTurn, + kDriveToGoal, + kFinished +}; + +static AutoStep g_step = AutoStep::kDriveForward; +static uint32_t g_stepStart = 0; + +void robotInit() { + Serial.begin(115200); + delay(100); + + leftDriver.begin(); + rightDriver.begin(); + leftDriver.setBrakeMode(true); + rightDriver.setBrakeMode(true); + + motorLeft.setTimeoutMs(0); + motorRight.setTimeoutMs(0); + + chassis.setWheelCircumference(32.0f); + chassis.setTrackWidth(29.0f); + + Serial.println("[AutonomousDemo] robotInit: Otonom örneği hazır"); +} + +void robotEnd() { + chassis.setVelocity(0.0f, 0.0f); + Serial.println("[AutonomousDemo] robotEnd: Motorlar durdu"); +} + +void teleopInit() { + Serial.println("[AutonomousDemo] teleopInit: Bu örnekte teleop, tank sürüşü sağlar"); +} + +void teleopLoop() { + auto js = probot::io::joystick_api::makeDefault(); + float left = js.getLeftY() * 100.0f; + float right = js.getRightY() * 100.0f; + chassis.setVelocity(left, right); + chassis.update(millis(), 20); + delay(20); +} + +void autonomousInit() { + Serial.println("[AutonomousDemo] autonomousInit: İleri -> Bekle -> 90° dön -> Hedefe git"); + g_step = AutoStep::kDriveForward; + g_stepStart = millis(); + chassis.driveDistance(80.0f); // 80 cm ileri +} + +void autonomousLoop() { + uint32_t now = millis(); + + switch (g_step) { + case AutoStep::kDriveForward: + if (now - g_stepStart > 2500) { + g_step = AutoStep::kPause; + g_stepStart = now; + chassis.setVelocity(0.0f, 0.0f); + Serial.println("[AutonomousDemo] Duraklama"); + } + break; + + case AutoStep::kPause: + if (now - g_stepStart > 800) { + g_step = AutoStep::kTurn; + g_stepStart = now; + chassis.turnDegrees(90.0f); + Serial.println("[AutonomousDemo] 90 derece dönüş başlıyor"); + } + break; + + case AutoStep::kTurn: + if (now - g_stepStart > 2200) { + g_step = AutoStep::kDriveToGoal; + g_stepStart = now; + chassis.driveDistance(40.0f); + Serial.println("[AutonomousDemo] Hedefe son itiş"); + } + break; + + case AutoStep::kDriveToGoal: + if (now - g_stepStart > 2000) { + g_step = AutoStep::kFinished; + chassis.setVelocity(0.0f, 0.0f); + Serial.println("[AutonomousDemo] Otonom tamamlandı"); + } + break; + + case AutoStep::kFinished: + default: + chassis.setVelocity(0.0f, 0.0f); + break; + } + + chassis.update(now, 20); + delay(20); +} diff --git a/examples/BasicTankDrive/BasicTankDrive.ino b/examples/BasicTankDrive/BasicTankDrive.ino deleted file mode 100644 index 0fe0990..0000000 --- a/examples/BasicTankDrive/BasicTankDrive.ino +++ /dev/null @@ -1,53 +0,0 @@ -#include -#include -#include -#include -// Not: Donanımı bağlayana kadar NullMotor/TestEncoder kullanabilirsiniz (yer tutucu). -// Gerçek projede bu yer tutucuları gerçek sürücülerle (örn. NFRMotor) değiştirin. -// Desteklenen sürücüler için: https://docs.probotstudio.com/ - -// Bu örnek, tank şasi (BasicTankDrive) ile teleop sürüşünü gösterir. -// Sol çubuk Y sol teker, sağ çubuk Y sağ teker hızını belirler. - -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -static const probot::control::PidConfig kPidCfg{ .kp=0.2f, .ki=0.0f, .kd=0.0f, .kf=0.0f, .out_min=-1.0f, .out_max=1.0f }; -static probot::control::PID pidL(kPidCfg), pidR(kPidCfg); -static probot::sensors::TestEncoder leftEnc, rightEnc; // yer tutucu -static probot::motor::NullMotor leftHW, rightHW; // yer tutucu -static probot::control::ClosedLoopMotor left(&leftEnc, &pidL, &leftHW, 1.0f, 1.0f); -static probot::control::ClosedLoopMotor right(&rightEnc, &pidR, &rightHW, 1.0f, 1.0f); -static probot::drive::BasicTankDrive chassis(&left, &right); - -void robotInit() { - Serial.println("[TankTeleop] robotInit: Tank sürüşü"); - // Örnek bağlama (gerçek sürücüler ile değiştirin): - // chassis.setWheelCircumference(31.4f); - // chassis.setTrackWidth(25.0f); -} - -void robotEnd() { - Serial.println("[TankTeleop] robotEnd: Bitti"); -} - -void teleopInit() { - // Mapping değiştirmek için (varsayılan: "logitech-f310"): - // probot::io::joystick_mapping::setActiveByName("standard"); - // probot::io::joystick_mapping::setActiveByName("logitech-f310"); - // probot::io::joystick_mapping::setActiveByName("axis9-dpad"); - Serial.println("[TankTeleop] teleopInit: Joystick ile tank sürüş"); -} - -void teleopLoop() { - auto js = probot::io::joystick_api::makeDefault(); - float left_axis = js.getLeftY(); // sol Y - float right_axis = js.getRightY(); // sağ Y - - float max_vel = 100.0f; // birim/s örnek - chassis.setVelocity(left_axis*max_vel, right_axis*max_vel); - chassis.update(millis(), 20); - delay(20); -} - -void autonomousInit() {} -void autonomousLoop() { delay(1000); } diff --git a/examples/BoardozaVNHMotorDemo/BoardozaVNHMotorDemo.ino b/examples/BoardozaVNHMotorDemo/BoardozaVNHMotorDemo.ino deleted file mode 100644 index 43c17d3..0000000 --- a/examples/BoardozaVNHMotorDemo/BoardozaVNHMotorDemo.ino +++ /dev/null @@ -1,114 +0,0 @@ -/* - Boardoza VNH Motor Demo (Joystick Kontrollü) - -------------------------------------------- - Bu örnek, VNH5019 tabanlı Boardoza motor sürücüsünü Probot Driver Station - joystick verisiyle nasıl sürebileceğinizi gösterir. - - Adımlar: - 1. Aşağıdaki pin tanımlarını kendi kartınıza göre düzenleyin. - 2. Şifreyi (en az 8 karakter) değiştirin ve kaydedin. - 3. S3 kartınızı çalıştırın, bilgisayardan Probot-XXXX ağına bağlanın. - 4. Tarayıcıdan 192.168.4.1 adresine gidip joystick’i hareket ettirin. - 5. Joystick Y ekseni (varsayılan: axes[1]) motora -1..1 arasında güç gönderir. - - Notlar: - - Sürücü her zaman fren modunda bekler. Joystick bırakıldığında motor sıkı durur. - - Joystick ölçeği -1..1 olduğundan Probot motor gücüyle birebir uyumludur. - - `GamepadSnapshot` eksen sırası driver station’daki girişe göre değişebilir. - Gerekirse `AXIS_INDEX` sabitini uyarlayın (örn. 0 veya 3). -*/ - -#include -#include - -// --- Motor sürücü pinleri (kartınızın bağlantısına göre değiştirin) --- -static constexpr int PIN_INA = 47; -static constexpr int PIN_INB = 46; -static constexpr int PIN_PWM = 48; -static constexpr int PIN_ENA = -1; // ENA pini 3V3'e bağlıysa -1 bırakın -static constexpr int PIN_ENB = -1; // ENB pini 3V3'e bağlıysa -1 bırakın - -// Joystick ekseni: çoğu gamepad'de 1 = sol çubuğun Y ekseni -static constexpr uint8_t AXIS_INDEX = 1; -static constexpr float DEADZONE = 0.05f; // küçük titreşimleri yok say - -// Driver Station erişimi için şifre (minimum 8 karakter) -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -// Motor nesnemiz ve kontrol sapı -static probot::motor::BoardozaVNHMotorDriver g_motor( - PIN_INA, - PIN_INB, - PIN_PWM, - PIN_ENA, - PIN_ENB -); -static probot::motor::MotorHandle g_motorHandle(g_motor); -static float g_lastReportedPower = 0.0f; - -static float applyDeadband(float value){ - if (std::fabs(value) < DEADZONE) return 0.0f; - return value; -} - -static float clampUnit(float v){ - if (v > 1.0f) return 1.0f; - if (v < -1.0f) return -1.0f; - return v; -} - -// --- Probot yaşam döngüsü fonksiyonları --- -void robotInit(){ - Serial.begin(115200); - delay(100); - Serial.println("[BoardozaVNH] Demo starting (20 kHz PWM, brake mode)"); - g_motor.begin(); - g_motor.setBrakeMode(true); // boşta tam fren - g_motor.setBrakeStrength(1.0f); // PWM %100 ile fren uygula - g_motorHandle.setPower(0.0f); - g_lastReportedPower = 0.0f; -} - -void robotEnd(){ - g_motorHandle.setPower(0.0f); - Serial.println("[BoardozaVNH] Demo stopped"); -} - -void teleopInit(){ - g_motorHandle.setPower(0.0f); - g_lastReportedPower = 0.0f; -} - -void teleopLoop(){ - auto snapshot = probot::io::gamepad().read(); - - float power = 0.0f; - if (snapshot.axisCount > AXIS_INDEX){ - power = clampUnit(snapshot.axes[AXIS_INDEX]); - power = -power; // Logitech tabanlı gamepadlerde ileri hareket pozitif olsun - power = applyDeadband(power); - } - - g_motorHandle.setPower(power); - - // İsteğe bağlı: bir düğmeye basıldığında yönü ters çevirmek - /* - if (snapshot.buttonCount > 0 && snapshot.buttons[0]){ - g_motorHandle.setInverted(true); - } else { - g_motorHandle.setInverted(false); - } - */ - - if (std::fabs(power - g_lastReportedPower) > 0.02f){ - Serial.printf("[BoardozaVNH] Power command: %.2f (seq=%lu)\n", - static_cast(power), - static_cast(snapshot.seq)); - g_lastReportedPower = power; - } - - delay(10); // 100 Hz kontrol döngüsü -} - -void autonomousInit(){} -void autonomousLoop(){} diff --git a/examples/ClosedLoopMotorTest/ClosedLoopMotorTest.ino b/examples/ClosedLoopMotorTest/ClosedLoopMotorTest.ino deleted file mode 100644 index 9b3990e..0000000 --- a/examples/ClosedLoopMotorTest/ClosedLoopMotorTest.ino +++ /dev/null @@ -1,47 +0,0 @@ -#include -#include -#include -#include -// Not: Donanımı bağlayana kadar NullMotor/TestEncoder kullanabilirsiniz (yer tutucu). -// Gerçek projede bu yer tutucuları gerçek sürücülerle (örn. NFRMotor) değiştirin. -// Desteklenen sürücüler için: https://docs.probotstudio.com/ - -// Bu örnek, kapalı çevrim (ClosedLoopMotor) bir motoru joystick ile sürer. -// Sol çubuk Y ekseni hız referansı, D-Pad (ör. butonlar) ise mod geçişi gibi kullanılabilir. - -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -static probot::sensors::TestEncoder encoderHW; // yer tutucu -static probot::motor::NullMotor motorHW; // yer tutucu -static const probot::control::PidConfig kPidCfg{ .kp=0.2f, .ki=0.0f, .kd=0.0f, .kf=0.0f, .out_min=-1.0f, .out_max=1.0f }; -static probot::control::PID pid(kPidCfg); -static probot::control::ClosedLoopMotor clm(&encoderHW, &pid, &motorHW, 1.0f, 1.0f); - -void robotInit() { - Serial.println("[CLMTest] robotInit: ClosedLoopMotor testi"); -} - -void robotEnd() { - Serial.println("[CLMTest] robotEnd: Bitti"); -} - -void teleopInit() { - // Mapping değiştirmek için (varsayılan: "logitech-f310"): - // probot::io::joystick_mapping::setActiveByName("standard"); - // probot::io::joystick_mapping::setActiveByName("logitech-f310"); - // probot::io::joystick_mapping::setActiveByName("axis9-dpad"); - Serial.println("[CLMTest] teleopInit: Joystick ile hız/konum kontrolü"); -} - -void teleopLoop() { - auto js = probot::io::joystick_api::makeDefault(); - float axis = js.getLeftY(); // sol çubuk Y - float vel_ref = axis * 100.0f; // örnek: 100 birim/s maksimum hız - clm.setSetpoint(vel_ref, probot::control::ControlType::kVelocity); - clm.update(millis(), 20); - Serial.printf("[CLMTest] vel_ref=%.2f\n", vel_ref); - delay(20); -} - -void autonomousInit() {} -void autonomousLoop() { delay(1000); } diff --git a/examples/EncoderTest/EncoderTest.ino b/examples/EncoderTest/EncoderTest.ino deleted file mode 100644 index 7b794ed..0000000 --- a/examples/EncoderTest/EncoderTest.ino +++ /dev/null @@ -1,32 +0,0 @@ -#include -#include - -// Bu örnek, enkoder değerlerini (tik ve hız) Serial Monitör'e yazdırır. -// Donanımı bağlayana kadar TestEncoder kullanabilirsiniz (yer tutucu). -// Gerçek projede bu yer tutucuyu gerçek enkoder sürücüsüyle değiştirin. - -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -static probot::sensors::TestEncoder encoderHW; // yer tutucu; gerçek sürücü ile değiştirin - -void robotInit() { - Serial.println("[EncoderTest] robotInit: Enkoder testi"); -} - -void robotEnd() { - Serial.println("[EncoderTest] robotEnd: Bitti"); -} - -void teleopInit() { - Serial.println("[EncoderTest] teleopInit: Enkoder değerleri yazdırılacak"); -} - -void teleopLoop() { - int32_t ticks = encoderHW.readTicks(); - int32_t tps = encoderHW.readTicksPerSecond(); - Serial.printf("[EncoderTest] ticks=%ld tps=%ld\n", (long)ticks, (long)tps); - delay(200); -} - -void autonomousInit() {} -void autonomousLoop() { delay(1000); } \ No newline at end of file diff --git a/examples/FullRobotDemo/FullRobotDemo.ino b/examples/FullRobotDemo/FullRobotDemo.ino deleted file mode 100644 index 233753a..0000000 --- a/examples/FullRobotDemo/FullRobotDemo.ino +++ /dev/null @@ -1,138 +0,0 @@ -#include -#include -#include -#include -#include - -// Bu örnek, daha tamamlanmış bir robot iskeleti gösterir: -// - TankDrive şasi (teleop + otonom) -// - Intake (içeri alma) ve Shooter (fırlatma) -// - İki adet Slider ile tırmanma mekanizması (aç/kapa senaryosu) -// Not: NullMotor/TestEncoder yer tutucu (no-op) sürücülerdir. -// Gerçek projede bunları gerçek sürücülerle (örn. NFRMotor) değiştirin. -// Desteklenen motorlar için: https://docs.probotstudio.com/ - -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -// --- Dosya-üstü kurulum (sıralı, güvenli) --- -static const probot::control::PidConfig kPidCfg{ .kp=0.2f, .ki=0.0f, .kd=0.0f, .kf=0.0f, .out_min=-1.0f, .out_max=1.0f }; -static probot::control::PID pidL(kPidCfg), pidR(kPidCfg); -static probot::sensors::TestEncoder leftEnc, rightEnc; // yer tutucu -static probot::motor::NullMotor leftHW, rightHW; // yer tutucu -static probot::control::ClosedLoopMotor left(&leftEnc, &pidL, &leftHW, 1.0f, 1.0f); -static probot::control::ClosedLoopMotor right(&rightEnc, &pidR, &rightHW, 1.0f, 1.0f); -static probot::drive::BasicTankDrive chassis(&left, &right); - -// Tırmanma sliderları (örnek amaçlı aynı motor/encoder ile) -static probot::mechanism::Slider sliderL(&left); -static probot::mechanism::Slider sliderR(&right); - -// Intake/Shooter (no-op); gerçek projede gerçek motorla değiştirin -static probot::motor::NullMotor intakeHW; -static probot::motor::NullMotor shooterHW; -static probot::motor::MotorHandle intake(intakeHW); -static probot::motor::MotorHandle shooter(shooterHW); - -// Tuş atamaları (UI tarafındaki buton indeksleri örnektir) -static const int BTN_INTAKE_IN = 0; // A -static const int BTN_SHOOT = 1; // B -static const int BTN_CLIMB_OPEN = 8; // LB -static const int BTN_CLIMB_CLOSE = 9; // LT - -// Otonom senaryo adımları -static uint32_t g_autoStep = 0; -static uint32_t g_autoMs = 0; - -void robotInit(){ - Serial.println("[FullRobot] robotInit: Başlatılıyor"); - // Örn: chassis.setWheelCircumference(31.4f); chassis.setTrackWidth(25.0f); -} - -void robotEnd(){ - intake.setPower(0.0f); - shooter.setPower(0.0f); - Serial.println("[FullRobot] robotEnd: Bitti"); -} - -static void handleIntakeAndShooter(const probot::io::joystick_api::Joystick& js){ - bool intake_in = js.getRawButton(BTN_INTAKE_IN); - bool shoot_btn = js.getRawButton(BTN_SHOOT); - intake.setPower(intake_in ? 0.8f : 0.0f); - shooter.setPower(shoot_btn ? 1.0f : 0.0f); -} - -static void handleClimb(const probot::io::joystick_api::Joystick& js){ - bool open = js.getRawButton(BTN_CLIMB_OPEN); - bool close = js.getRawButton(BTN_CLIMB_CLOSE); - - if (open){ - sliderL.setTargetLength(40.0f); sliderR.setTargetLength(40.0f); - uint32_t t0 = millis(); while (millis()-t0 < 2000){ sliderL.update(millis(), 20); sliderR.update(millis(), 20); delay(20);} - sliderL.setTargetLength(0.0f); sliderR.setTargetLength(0.0f); - } - if (close){ - sliderL.setTargetLength(0.0f); sliderR.setTargetLength(0.0f); - } - - sliderL.update(millis(), 20); - sliderR.update(millis(), 20); -} - -void teleopInit(){ - // Mapping değiştirmek için (varsayılan: "logitech-f310"): - // probot::io::joystick_mapping::setActiveByName("standard"); - // probot::io::joystick_mapping::setActiveByName("logitech-f310"); - // probot::io::joystick_mapping::setActiveByName("axis9-dpad"); - Serial.println("[FullRobot] teleopInit: Tank sürüş + intake/shooter + climb"); -} - -void teleopLoop(){ - auto js = probot::io::joystick_api::makeDefault(); - float left_axis = js.getLeftY(); - float right_axis = js.getRightY(); - float max_vel = 100.0f; - chassis.setVelocity(left_axis*max_vel, right_axis*max_vel); - chassis.update(millis(), 20); - handleIntakeAndShooter(js); - handleClimb(js); - delay(20); -} - -void autonomousInit(){ - Serial.println("[FullRobot] autonomousInit: Otonom başlayacak"); - g_autoStep = 0; g_autoMs = millis(); -} - -void autonomousLoop(){ - uint32_t now = millis(); - switch (g_autoStep){ - case 0: - Serial.println("[FullRobot/Auto] 1) 50 cm ileri"); - chassis.driveDistance(50.0f); - g_autoStep=1; g_autoMs=now; break; - case 1: - if (now - g_autoMs > 3000){ - Serial.println("[FullRobot/Auto] 2) Shooter çalıştır"); - shooter.setPower(1.0f); - g_autoStep=2; g_autoMs=now; - } - break; - case 2: - if (now - g_autoMs > 2000){ - Serial.println("[FullRobot/Auto] 3) Shooter durdur"); - shooter.setPower(0.0f); - g_autoStep=3; g_autoMs=now; - } - break; - case 3: - if (now - g_autoMs > 500){ - Serial.println("[FullRobot/Auto] 4) 30 cm ileri"); - chassis.driveDistance(30.0f); - g_autoStep=4; g_autoMs=now; - } - break; - default: - break; - } - delay(20); -} diff --git a/examples/JoystickTest/JoystickTest.ino b/examples/JoystickTest/JoystickTest.ino index b411999..a932644 100644 --- a/examples/JoystickTest/JoystickTest.ino +++ b/examples/JoystickTest/JoystickTest.ino @@ -1,51 +1,72 @@ #include #include +#include +#include +#include -// Bu örnek, web arayüzünden gelen joystick verilerini okuyup -// Serial Monitör'e yazdırır. -// Not: Artık joystick verisi her durumda gönderilir. +// Pin atamalarını kendi kartınıza göre güncelleyin. +static constexpr int PIN_INA = 47; +static constexpr int PIN_INB = 46; +static constexpr int PIN_PWM = 48; +static constexpr int PIN_ENA = -1; // EN pinleri 3V3'e bağlıysa -1 bırakmak yeterli. +static constexpr int PIN_ENB = -1; + +// Tüm örneklerde aynı temel motor-stub setini kullanıyoruz. +static probot::motor::BoardozaVNHMotorDriver motor(PIN_INA, PIN_INB, PIN_PWM, PIN_ENA, PIN_ENB); +static probot::sensors::NullEncoder encoder; +static const probot::control::PidConfig kPid{.kp = 0.25f, .ki = 0.0f, .kd = 0.0f, .kf = 0.0f, + .out_min = -1.0f, .out_max = 1.0f}; +static probot::control::PID pid(kPid); +static probot::control::ClosedLoopMotor controller(&encoder, &pid, &motor, 1.0f, 1.0f); PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); void robotInit() { - Serial.println("[JoystickTest] robotInit: Başlatılıyor"); + Serial.begin(115200); + delay(100); + + // Kart ve sürücüyü hazırla. + motor.begin(); + motor.setBrakeMode(true); // boşta tam fren uygula + controller.setSetpoint(0.0f, probot::control::ControlType::kPercent); + + Serial.println("[JoystickTest] robotInit: Joystick ve motor izleme başlatıldı"); } void robotEnd() { - Serial.println("[JoystickTest] robotEnd: Bitti"); + controller.setPowerDirect(0.0f); // çıkışları güvenle sıfırla + Serial.println("[JoystickTest] robotEnd: Motor sıfırlandı"); } void teleopInit() { - // Mapping değiştirmek için (varsayılan: "logitech-f310"): - // probot::io::joystick_mapping::setActiveByName("standard"); - // probot::io::joystick_mapping::setActiveByName("logitech-f310"); - // probot::io::joystick_mapping::setActiveByName("axis9-dpad"); - Serial.println("[JoystickTest] teleopInit: Yeni joystick_api ile test"); + Serial.println("[JoystickTest] teleopInit: Ekseni okuyup motora aktaracağız"); } void teleopLoop() { auto js = probot::io::joystick_api::makeDefault(); - Serial.printf("[JoystickTest] seq=%lu axes=%lu buttons=%lu\n", - (unsigned long)js.getSeq(), - (unsigned long)js.getAxisCount(), - (unsigned long)js.getButtonCount()); - - Serial.printf(" L(%.2f, %.2f) R(%.2f, %.2f) LT=%.0f RT=%.0f POV=%d\n", - js.getLeftX(), js.getLeftY(), - js.getRightX(), js.getRightY(), - js.getLeftTriggerAxis(), js.getRightTriggerAxis(), - js.getPOV()); - - // Ham eksen/tuş örneği - if (js.getAxisCount() > 0) { - Serial.printf(" raw axis[0]=%.2f\n", js.getRawAxis(0)); - } - if (js.getButtonCount() > 0) { - Serial.printf(" raw button[0]=%s\n", js.getRawButton(0) ? "ON" : "OFF"); - } - - delay(150); + + // Sol çubuk Y eksenini oku (varsayılan eşleme). + float axis = js.getLeftY(); + + // Okuduğumuz değeri doğrudan motora gönderiyoruz. + controller.setPowerDirect(axis); + + // Seri log ile joystick verisini ve motor komutunu gözlemleyin. + Serial.printf("[JoystickTest] seq=%lu axisY=%.2f motorCmd=%.2f\n", + static_cast(js.getSeq()), + axis, + controller.lastOutput()); + + delay(50); } -void autonomousInit() {} -void autonomousLoop() { delay(1000); } \ No newline at end of file +void autonomousInit() { + Serial.println("[JoystickTest] autonomousInit: Bu örnek otonomda motoru durdurur"); + controller.setPowerDirect(0.0f); +} + +void autonomousLoop() { + // Joystick okumadığımız için otonomda motoru sıfırda tutuyoruz. + controller.update(millis(), 20); + delay(20); +} diff --git a/examples/LoggingDemo/LoggingDemo.ino b/examples/LoggingDemo/LoggingDemo.ino new file mode 100644 index 0000000..99d0e2e --- /dev/null +++ b/examples/LoggingDemo/LoggingDemo.ino @@ -0,0 +1,111 @@ +/** + * LoggingDemo.ino + * + * Quick check for the serial/Wi-Fi logging stack. Registers a demo source, + * pushes a heartbeat counter every 500 ms, and exposes a static snapshot. Use + * the driver station UI (minimal stack) to see Wi-Fi log streaming, or watch + * the binary frames on the serial monitor. + */ + +#include +#include + +namespace { + +struct DemoSource { + const probot::logging::SourceRegistration* reg{nullptr}; + uint32_t boot_ms{0}; + uint32_t counter{0}; +} demo; + +void demoStaticInfo(const void* object, + probot::logging::TelemetryCollector& collector) { + auto src = static_cast(object); + collector.addInt("boot_ms", src->boot_ms, probot::logging::Priority::kSystemCritical); + collector.addString("note", "Demo static snapshot", probot::logging::Priority::kUserMarked); +} + +void publishCounter(uint32_t now_ms) { + if (!demo.reg) return; + demo.counter++; + auto& mgr = probot::logging::manager(); + mgr.enqueueValue(*demo.reg, + &demo, + "counter", + probot::logging::ValueType::kInt, + probot::logging::Priority::kUserMarked, + static_cast(demo.counter), + 0.0f, + false, + nullptr, + now_ms); + mgr.enqueueValue(*demo.reg, + &demo, + "uptime_ms", + probot::logging::ValueType::kInt, + probot::logging::Priority::kBackground, + static_cast(now_ms), + 0.0f, + false, + nullptr, + now_ms); +} + +} // namespace + +static uint32_t last_publish_ms = 0; + +void robotInit(){ + Serial.begin(115200); + delay(200); + + probot::logging::configureDefaults(); + probot::logging::enableSerialLogging(true); + probot::logging::setSerialBandwidthMode(probot::logging::BandwidthMode::kNormal); + probot::logging::enableWifiLogging(true); + probot::logging::setWifiStreamingEnabled(true); + + demo.boot_ms = millis(); + + probot::logging::SourceRegistration reg{ + "demo", + "logging", + probot::logging::Priority::kUserMarked, + true, + true, + nullptr, + demoStaticInfo + }; + demo.reg = probot::logging::registerSource(&demo, reg); + + Serial.println(); + Serial.println(F("[LoggingDemo] Logging configured. Check the driver UI")); +} + +void robotEnd(){ + if (demo.reg){ + probot::logging::unregisterSource(&demo); + demo.reg = nullptr; + } +} + +void teleopInit(){ + last_publish_ms = millis(); +} + +void teleopLoop(){ + uint32_t now = millis(); + if (now - last_publish_ms >= 500){ + publishCounter(now); + last_publish_ms = now; + } + vTaskDelay(pdMS_TO_TICKS(5)); +} + +void autonomousInit(){ + teleopInit(); +} + +void autonomousLoop(){ + teleopLoop(); +} diff --git a/examples/MecanumDriveDemo/MecanumDriveDemo.ino b/examples/MecanumDriveDemo/MecanumDriveDemo.ino new file mode 100644 index 0000000..d38ce88 --- /dev/null +++ b/examples/MecanumDriveDemo/MecanumDriveDemo.ino @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include + +// Mecanum sürüş için dört motorun pin atamaları. +struct MotorPins { + int ina, inb, pwm, ena, enb; +}; + +static constexpr MotorPins PINS_FL{18, 19, 20, -1, -1}; +static constexpr MotorPins PINS_FR{21, 22, 23, -1, -1}; +static constexpr MotorPins PINS_RL{24, 25, 26, -1, -1}; +static constexpr MotorPins PINS_RR{27, 28, 29, -1, -1}; + +static probot::motor::BoardozaVNHMotorDriver drvFL(PINS_FL.ina, PINS_FL.inb, PINS_FL.pwm, PINS_FL.ena, PINS_FL.enb); +static probot::motor::BoardozaVNHMotorDriver drvFR(PINS_FR.ina, PINS_FR.inb, PINS_FR.pwm, PINS_FR.ena, PINS_FR.enb); +static probot::motor::BoardozaVNHMotorDriver drvRL(PINS_RL.ina, PINS_RL.inb, PINS_RL.pwm, PINS_RL.ena, PINS_RL.enb); +static probot::motor::BoardozaVNHMotorDriver drvRR(PINS_RR.ina, PINS_RR.inb, PINS_RR.pwm, PINS_RR.ena, PINS_RR.enb); + +static probot::sensors::NullEncoder encFL; +static probot::sensors::NullEncoder encFR; +static probot::sensors::NullEncoder encRL; +static probot::sensors::NullEncoder encRR; + +static const probot::control::PidConfig kPid{.kp = 0.30f, .ki = 0.01f, .kd = 0.0f, .kf = 0.0f, + .out_min = -1.0f, .out_max = 1.0f}; + +static probot::control::PID pidFL(kPid); +static probot::control::PID pidFR(kPid); +static probot::control::PID pidRL(kPid); +static probot::control::PID pidRR(kPid); + +static probot::control::ClosedLoopMotor motorFL(&encFL, &pidFL, &drvFL, 1.0f, 1.0f); +static probot::control::ClosedLoopMotor motorFR(&encFR, &pidFR, &drvFR, 1.0f, 1.0f); +static probot::control::ClosedLoopMotor motorRL(&encRL, &pidRL, &drvRL, 1.0f, 1.0f); +static probot::control::ClosedLoopMotor motorRR(&encRR, &pidRR, &drvRR, 1.0f, 1.0f); + +static probot::chassis::SimpleMecanumDrive mecanum(&motorFL, &motorFR, &motorRL, &motorRR); + +PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); + +void robotInit() { + Serial.begin(115200); + delay(100); + + drvFL.begin(); drvFR.begin(); drvRL.begin(); drvRR.begin(); + drvFL.setBrakeMode(true); + drvFR.setBrakeMode(true); + drvRL.setBrakeMode(true); + drvRR.setBrakeMode(true); + + mecanum.setInverted(false, true, false, true); // sağ taraf ters kabloluysa düzelt + + Serial.println("[MecanumDriveDemo] robotInit: Mecanum sürüşe hazır"); +} + +void robotEnd() { + mecanum.stop(); + Serial.println("[MecanumDriveDemo] robotEnd: Motorlar kapatıldı"); +} + +void teleopInit() { + Serial.println("[MecanumDriveDemo] teleopInit:" + " sol çubuk Y ileri-geri, X yan, sağ X dönüş"); +} + +void teleopLoop() { + auto js = probot::io::joystick_api::makeDefault(); + + float vx = js.getLeftY(); // ileri geri + float vy = js.getLeftX(); // yan hareket + float omega = js.getRightX();// dönüş + + mecanum.driveCartesian(vx, vy, omega); + + Serial.printf("[MecanumDriveDemo] vx=%.2f vy=%.2f w=%.2f fl=%.2f fr=%.2f rl=%.2f rr=%.2f\n", + vx, vy, omega, + motorFL.lastOutput(), motorFR.lastOutput(), + motorRL.lastOutput(), motorRR.lastOutput()); + + delay(20); +} + +void autonomousInit() { + Serial.println("[MecanumDriveDemo] autonomousInit: kare rota denemesi"); +} + +void autonomousLoop() { + static uint32_t start = millis(); + uint32_t elapsed = millis() - start; + float vx = 0.0f, vy = 0.0f, omega = 0.0f; + + if (elapsed < 2000) { + vx = 0.6f; // ileri + } else if (elapsed < 4000) { + vy = 0.6f; // sağa kay + } else if (elapsed < 6000) { + vx = -0.6f; // geri + } else if (elapsed < 8000) { + vy = -0.6f; // sola kay + } else { + start = millis(); + } + + mecanum.driveCartesian(vx, vy, omega); + delay(20); +} diff --git a/examples/MotorControllerDemo/MotorControllerDemo.ino b/examples/MotorControllerDemo/MotorControllerDemo.ino new file mode 100644 index 0000000..e975818 --- /dev/null +++ b/examples/MotorControllerDemo/MotorControllerDemo.ino @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include + +// Kullanılan Boardoza VNH sürücü pinleri (kendi kartınıza göre güncelleyin). +static constexpr int PIN_INA = 9; +static constexpr int PIN_INB = 10; +static constexpr int PIN_PWM = 11; +static constexpr int PIN_ENA = -1; +static constexpr int PIN_ENB = -1; + +static probot::motor::BoardozaVNHMotorDriver motor(PIN_INA, PIN_INB, PIN_PWM, PIN_ENA, PIN_ENB); +static probot::sensors::NullEncoder encoder; +static const probot::control::PidConfig kVelocityPid{.kp = 0.35f, .ki = 0.02f, .kd = 0.0f, + .kf = 0.0f, .out_min = -1.0f, .out_max = 1.0f}; +static probot::control::PID pid(kVelocityPid); +static probot::control::ClosedLoopMotor controller(&encoder, &pid, &motor, 1.0f, 1.0f); + +PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); + +static probot::control::ControlType g_mode = probot::control::ControlType::kVelocity; + +void robotInit() { + Serial.begin(115200); + delay(100); + + motor.begin(); + motor.setBrakeMode(true); + + controller.setTimeoutMs(0); // joystick bırakıldığında çıkış sıfırlanmasın + controller.setSetpoint(0.0f, g_mode); + + Serial.println("[MotorControllerDemo] robotInit: Kapalı çevrim denemesi için hazır"); +} + +void robotEnd() { + controller.setPowerDirect(0.0f); + Serial.println("[MotorControllerDemo] robotEnd: Motor kapatıldı"); +} + +void teleopInit() { + Serial.println("[MotorControllerDemo] teleopInit:" + " sol eksen referans, A butonu mod değiştirir (yüzde/velocity)"); +} + +void teleopLoop() { + auto js = probot::io::joystick_api::makeDefault(); + + if (js.getRawButton(0)) { + g_mode = (g_mode == probot::control::ControlType::kVelocity) + ? probot::control::ControlType::kPercent + : probot::control::ControlType::kVelocity; + controller.setSetpoint(0.0f, g_mode); + Serial.printf("[MotorControllerDemo] Mod değişti: %s\n", + g_mode == probot::control::ControlType::kVelocity ? "HIZ" : "YÜZDE"); + delay(200); // buton debouncing + } + + float axis = js.getLeftY(); + float target = axis * (g_mode == probot::control::ControlType::kVelocity ? 100.0f : 1.0f); + controller.setSetpoint(target, g_mode); + controller.update(millis(), 20); + + Serial.printf("[MotorControllerDemo] mode=%s target=%.2f measurement=%.2f out=%.2f\n", + g_mode == probot::control::ControlType::kVelocity ? "VEL" : "PCT", + target, + controller.lastMeasurement(), + controller.lastOutput()); + + delay(20); +} + +void autonomousInit() { + Serial.println("[MotorControllerDemo] autonomousInit: 2 saniyelik hız profili"); + controller.setSetpoint(80.0f, probot::control::ControlType::kVelocity); +} + +void autonomousLoop() { + static uint32_t start = millis(); + controller.update(millis(), 20); + if (millis() - start > 2000) { + controller.setSetpoint(0.0f, probot::control::ControlType::kVelocity); + } + delay(20); +} diff --git a/examples/MotorDriverDemo/MotorDriverDemo.ino b/examples/MotorDriverDemo/MotorDriverDemo.ino new file mode 100644 index 0000000..625166e --- /dev/null +++ b/examples/MotorDriverDemo/MotorDriverDemo.ino @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include + +// Boardoza VNH sürücüsünü kullanırken kendi pinlerinizi mutlaka kontrol edin. +static constexpr int PIN_INA = 39; +static constexpr int PIN_INB = 40; +static constexpr int PIN_PWM = 41; +static constexpr int PIN_ENA = -1; +static constexpr int PIN_ENB = -1; + +static probot::motor::BoardozaVNHMotorDriver motor(PIN_INA, PIN_INB, PIN_PWM, PIN_ENA, PIN_ENB); +static probot::sensors::NullEncoder encoder; +static const probot::control::PidConfig kPid{.kp = 0.30f, .ki = 0.0f, .kd = 0.0f, .kf = 0.0f, + .out_min = -1.0f, .out_max = 1.0f}; +static probot::control::PID pid(kPid); +static probot::control::ClosedLoopMotor controller(&encoder, &pid, &motor, 1.0f, 1.0f); + +PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); + +void robotInit() { + Serial.begin(115200); + delay(100); + + motor.begin(); + motor.setBrakeMode(false); // sürücü serbest bırakıldı, coast modunda + controller.setInverted(false); + + Serial.println("[MotorDriverDemo] robotInit: IMotorDriver arayüzünü test etmek için hazır"); +} + +void robotEnd() { + motor.setPower(0.0f); + Serial.println("[MotorDriverDemo] robotEnd: Motor durduruldu"); +} + +void teleopInit() { + Serial.println("[MotorDriverDemo] teleopInit:" + " sol eksen gücü, sağ tetik ise yön tersleme yapar"); +} + +void teleopLoop() { + auto js = probot::io::joystick_api::makeDefault(); + + // Sol çubuktan ham güç, sağ tetikten tersleme isteği okuyoruz. + float power = js.getLeftY(); + bool invert = js.getRightTriggerAxis() > 0.5f; + + motor.setInverted(invert); + + // IMotorDriver doğrudan PWM uygular; ClosedLoopMotor'u sadece izleme için kullanıyoruz. + motor.setPower(power); + controller.setPowerDirect(power); // lastOutput() bilgisi için eşleştiriyoruz. + + Serial.printf("[MotorDriverDemo] power=%.2f invert=%d motorOut=%.2f\n", + power, invert ? 1 : 0, controller.lastOutput()); + + delay(40); +} + +void autonomousInit() { + Serial.println("[MotorDriverDemo] autonomousInit: Motoru yavaşça durduruyoruz"); +} + +void autonomousLoop() { + motor.setPower(0.0f); + controller.update(millis(), 20); + delay(20); +} diff --git a/examples/MotorTest/MotorTest.ino b/examples/MotorTest/MotorTest.ino deleted file mode 100644 index 32f1d1f..0000000 --- a/examples/MotorTest/MotorTest.ino +++ /dev/null @@ -1,44 +0,0 @@ -#include -#include -#include -#include -// Not: Donanımı bağlayana kadar NullMotor kullanabilirsiniz (yer tutucu). -// Gerçek projede bu yer tutucuyu gerçek sürücülerle (örn. NFRMotor) değiştirin. -// Desteklenen sürücüler için: https://docs.probotstudio.com/ - -// Bu örnek, joystick'ten gelen bir eksen değerini (-1..1) -// ham motor gücüne (normalize -1..1) direkt olarak eşler. -// Amaç: Motor bağlantısını test etmek ve yön/invert kontrolünü doğrulamak. - -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -static probot::motor::NullMotor motorHW; // yer tutucu; gerçek sürücü ile değiştirin -static probot::motor::MotorHandle motor(motorHW); // sahipliği içeride yönetir - -void robotInit() { - Serial.println("[MotorTest] robotInit: Motor testi başlıyor"); -} - -void robotEnd() { - motor.setPower(0.0f); - Serial.println("[MotorTest] robotEnd: Bitti"); -} - -void teleopInit() { - // Mapping değiştirmek için (varsayılan: "logitech-f310"): - // probot::io::joystick_mapping::setActiveByName("standard"); - // probot::io::joystick_mapping::setActiveByName("logitech-f310"); - // probot::io::joystick_mapping::setActiveByName("axis9-dpad"); - Serial.println("[MotorTest] teleopInit: Joystick ekseni motora güç olarak yazılacak"); -} - -void teleopLoop() { - auto js = probot::io::joystick_api::makeDefault(); - float axis = js.getLeftY(); // Örn: sol çubuk Y - motor.setPower(axis); - Serial.printf("[MotorTest] axis=%.2f power=%.2f\n", axis, axis); - delay(50); -} - -void autonomousInit() {} -void autonomousLoop() { delay(1000); } diff --git a/examples/ShooterDemo/ShooterDemo.ino b/examples/ShooterDemo/ShooterDemo.ino new file mode 100644 index 0000000..7a69c92 --- /dev/null +++ b/examples/ShooterDemo/ShooterDemo.ino @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include +#include + +// Shooter tekeri için Boardoza VNH pin konfigürasyonu. +static constexpr int PIN_INA = 15; +static constexpr int PIN_INB = 16; +static constexpr int PIN_PWM = 17; +static constexpr int PIN_ENA = -1; +static constexpr int PIN_ENB = -1; + +static probot::motor::BoardozaVNHMotorDriver motor(PIN_INA, PIN_INB, PIN_PWM, PIN_ENA, PIN_ENB); +static probot::sensors::NullEncoder encoder; +static const probot::control::PidConfig kVelocityPid{.kp = 0.4f, .ki = 0.02f, .kd = 0.0f, + .kf = 0.0f, .out_min = -1.0f, .out_max = 1.0f}; +static probot::control::PID pid(kVelocityPid); +static probot::control::ClosedLoopMotor shooterMotor(&encoder, &pid, &motor, 1.0f, 1.0f); +static probot::mechanism::nfr::NfrShooter shooter(&shooterMotor); + +PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); + +void robotInit() { + Serial.begin(115200); + delay(100); + + motor.begin(); + motor.setBrakeMode(false); // shooter çarkında coast tercihi yapılabilir + + shooterMotor.setTimeoutMs(0); + shooterMotor.selectDefaultSlot(probot::control::ControlType::kVelocity, 0); + shooterMotor.setPidSlotConfig(0, kVelocityPid); + shooter.setTicksPerRevolution(4096.0f); // kullandığınız enkoder değerini girin + shooter.setRpm(0.0f, 0.0f); + + Serial.println("[ShooterDemo] robotInit: Shooter kontrolü başlatıldı"); +} + +void robotEnd() { + shooter.stop(); + Serial.println("[ShooterDemo] robotEnd: Teker kapatıldı"); +} + +void teleopInit() { + Serial.println("[ShooterDemo] teleopInit:" + " sağ tetik hızlandırır, sol tetik fren yapar"); +} + +void teleopLoop() { + auto js = probot::io::joystick_api::makeDefault(); + + float accel = js.getRightTriggerAxis(); // 0..1 + float brake = js.getLeftTriggerAxis(); // 0..1 + + float targetRpm = accel * 3200.0f; // istenen maksimum RPM + if (brake > 0.2f) targetRpm = 0.0f; // fren tetiklendiğinde durdur + + shooter.setPrimaryRpm(targetRpm); + shooterMotor.update(millis(), 20); + + Serial.printf("[ShooterDemo] hedef=%.0f rpm ölçüm=%.1f rpm çıkış=%.2f\n", + targetRpm, + shooterMotor.lastMeasurement(), + shooterMotor.lastOutput()); + + delay(20); +} + +void autonomousInit() { + Serial.println("[ShooterDemo] autonomousInit: 2 saniye spool, sonra durdur"); + shooter.setPrimaryRpm(3000.0f); +} + +void autonomousLoop() { + static uint32_t start = millis(); + shooterMotor.update(millis(), 20); + if (millis() - start > 2000) { + shooter.stop(); + } + delay(20); +} diff --git a/examples/SliderDemo/SliderDemo.ino b/examples/SliderDemo/SliderDemo.ino new file mode 100644 index 0000000..2263d28 --- /dev/null +++ b/examples/SliderDemo/SliderDemo.ino @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include + +// Slider mekanizması için kullanılan VNH sürücü pinleri. +static constexpr int PIN_INA = 12; +static constexpr int PIN_INB = 13; +static constexpr int PIN_PWM = 14; +static constexpr int PIN_ENA = -1; +static constexpr int PIN_ENB = -1; + +static probot::motor::BoardozaVNHMotorDriver motor(PIN_INA, PIN_INB, PIN_PWM, PIN_ENA, PIN_ENB); +static probot::sensors::NullEncoder encoder; +static const probot::control::PidConfig kPositionPid{.kp = 0.5f, .ki = 0.0f, .kd = 0.0f, .kf = 0.0f, + .out_min = -1.0f, .out_max = 1.0f}; +static probot::control::PID pid(kPositionPid); +static probot::control::ClosedLoopMotor controller(&encoder, &pid, &motor, 1.0f, 1.0f); +static probot::mechanism::Slider slider(&controller); + +PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); + +void robotInit() { + Serial.begin(115200); + delay(100); + + motor.begin(); + motor.setBrakeMode(true); + + controller.selectDefaultSlot(probot::control::ControlType::kPosition, 0); + controller.setPidSlotConfig(0, kPositionPid); + controller.setSetpoint(0.0f, probot::control::ControlType::kPosition); + + slider.setLengthToTicks(200.0f); // 1 cm için 200 encoder tıkı varsayalım + slider.setLengthLimits(0.0f, 50.0f); // 0 - 50 cm arası güvenli bölge + + Serial.println("[SliderDemo] robotInit: Slider kontrolü hazır"); +} + +void robotEnd() { + controller.setPowerDirect(0.0f); + Serial.println("[SliderDemo] robotEnd: Motor sıfırlandı"); +} + +void teleopInit() { + Serial.println("[SliderDemo] teleopInit:" + " D-pad ile 0/15/30/45 cm hedefleri seçilir"); +} + +void teleopLoop() { + auto js = probot::io::joystick_api::makeDefault(); + int pov = js.getPOV(); + + if (pov == 0) slider.setTargetLength(45.0f); // yukarı + if (pov == 90) slider.setTargetLength(30.0f); // sağ + if (pov == 180) slider.setTargetLength(0.0f); // aşağı + if (pov == 270) slider.setTargetLength(15.0f); // sol + + slider.update(millis(), 20); + controller.update(millis(), 20); + + Serial.printf("[SliderDemo] hedef=%.1f cm ölçüm=%.1f cm çıkış=%.2f\n", + slider.getTargetLength(), + slider.getCurrentLength(), + controller.lastOutput()); + + delay(20); +} + +void autonomousInit() { + Serial.println("[SliderDemo] autonomousInit: 3 adımda preset dolaşımı"); +} + +void autonomousLoop() { + static uint32_t stateStart = millis(); + static int state = 0; + uint32_t now = millis(); + + if (state == 0) { + slider.setTargetLength(10.0f); + stateStart = now; + state = 1; + } else if (state == 1 && now - stateStart > 1500) { + slider.setTargetLength(40.0f); + stateStart = now; + state = 2; + } else if (state == 2 && now - stateStart > 1500) { + slider.setTargetLength(0.0f); + state = 3; + } + + slider.update(now, 20); + controller.update(now, 20); + delay(20); +} diff --git a/examples/SliderTest/SliderTest.ino b/examples/SliderTest/SliderTest.ino deleted file mode 100644 index 3414614..0000000 --- a/examples/SliderTest/SliderTest.ino +++ /dev/null @@ -1,58 +0,0 @@ -#include -#include -#include -#include -// Not: Donanımı bağlayana kadar NullMotor/TestEncoder kullanabilirsiniz (yer tutucu). -// Gerçek projede bu yer tutucuları gerçek sürücülerle (örn. NFRMotor) değiştirin. -// Desteklenen sürücüler için: https://docs.probotstudio.com/ - -// Bu örnek, Slider nesnesini D-Pad ile 10/20/30/40 cm hedeflerine taşımayı dener. -// Slider, bir ClosedLoopMotor üzerinden konum modunda sürülür. - -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -static const probot::control::PidConfig kPidCfg{ .kp=0.2f, .ki=0.0f, .kd=0.0f, .kf=0.0f, .out_min=-1.0f, .out_max=1.0f }; -static probot::control::PID pid(kPidCfg); -static probot::sensors::TestEncoder encHW; // yer tutucu -static probot::motor::NullMotor motHW; // yer tutucu -static probot::control::ClosedLoopMotor clm(&encHW, &pid, &motHW, 1.0f, 1.0f); -static probot::mechanism::Slider slider(&clm); - -void robotInit() { - Serial.println("[SliderTest] robotInit: Slider testi"); - // slider.setLengthToTicks(...); -} - -void robotEnd() { - Serial.println("[SliderTest] robotEnd: Bitti"); -} - -void teleopInit() { - // Mapping değiştirmek için (varsayılan: "logitech-f310"): - // probot::io::joystick_mapping::setActiveByName("standard"); - // probot::io::joystick_mapping::setActiveByName("logitech-f310"); - // probot::io::joystick_mapping::setActiveByName("axis9-dpad"); - Serial.println("[SliderTest] teleopInit: D-Pad ile 10/20/30/40 cm hedefleri"); -} - -void teleopLoop() { - auto js = probot::io::joystick_api::makeDefault(); - - // Basit D-Pad: Up/Down/Left/Right (POV 0/180/270/90) - int pov = js.getPOV(); - bool up = (pov == 0); - bool down = (pov == 180); - bool left = (pov == 270); - bool right = (pov == 90); - - if (up) { slider.setTargetLength(10.0f); Serial.println("[SliderTest] Hedef: 10 cm"); } - if (down) { slider.setTargetLength(20.0f); Serial.println("[SliderTest] Hedef: 20 cm"); } - if (left) { slider.setTargetLength(30.0f); Serial.println("[SliderTest] Hedef: 30 cm"); } - if (right) { slider.setTargetLength(40.0f); Serial.println("[SliderTest] Hedef: 40 cm"); } - - slider.update(millis(), 20); - delay(20); -} - -void autonomousInit() {} -void autonomousLoop() { delay(1000); } diff --git a/examples/TankDriveAuto/TankDriveAuto.ino b/examples/TankDriveAuto/TankDriveAuto.ino deleted file mode 100644 index 95911f4..0000000 --- a/examples/TankDriveAuto/TankDriveAuto.ino +++ /dev/null @@ -1,44 +0,0 @@ -#include -#include -#include -#include - -// Bu örnek, tank sürüş şasesi için basit bir otonom senaryoyu gösterir. -// Sırasıyla: X cm ileri git, Y derece dön, tekrar X cm ileri git gibi bir akış. -// driveDistance ve turnDegrees komutları konum hedeflerini gönderir. - -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -static const probot::control::PidConfig kPidCfg{ .kp=0.2f, .ki=0.0f, .kd=0.0f, .kf=0.0f, .out_min=-1.0f, .out_max=1.0f }; -static probot::control::PID pidL(kPidCfg), pidR(kPidCfg); -static probot::sensors::TestEncoder leftEnc, rightEnc; -static probot::motor::NullMotor leftHW, rightHW; -static probot::control::ClosedLoopMotor left(&leftEnc, &pidL, &leftHW, 1.0f, 1.0f); -static probot::control::ClosedLoopMotor right(&rightEnc, &pidR, &rightHW, 1.0f, 1.0f); -static probot::drive::BasicTankDrive chassis(&left, &right); - -static uint32_t g_step = 0; -static uint32_t g_last_ms = 0; - -void robotInit() { - Serial.println("[TankAuto] robotInit"); - // chassis.setWheelCircumference(...); chassis.setTrackWidth(...); -} - -void robotEnd() { Serial.println("[TankAuto] robotEnd"); } - -void teleopInit() { Serial.println("[TankAuto] teleopInit"); } -void teleopLoop() { - auto js = probot::io::joystick_api::makeDefault(); - float left_axis = js.getLeftY(); - float right_axis = js.getRightY(); - chassis.setVelocity(left_axis*100.0f, right_axis*100.0f); - chassis.update(millis(), 20); - delay(20); -} - -void autonomousInit() { Serial.println("[TankAuto] autonomousInit"); } -void autonomousLoop() { - chassis.driveDistance(50.0f); - delay(1000); -} diff --git a/examples/TankDriveDemo/TankDriveDemo.ino b/examples/TankDriveDemo/TankDriveDemo.ino new file mode 100644 index 0000000..9225314 --- /dev/null +++ b/examples/TankDriveDemo/TankDriveDemo.ino @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include + +// Tank şasi için iki adet VNH sürücünün pin eşlemesi (örnek değerler). +static constexpr int LEFT_INA = 1; +static constexpr int LEFT_INB = 2; +static constexpr int LEFT_PWM = 3; +static constexpr int LEFT_ENA = -1; +static constexpr int LEFT_ENB = -1; + +static constexpr int RIGHT_INA = 4; +static constexpr int RIGHT_INB = 5; +static constexpr int RIGHT_PWM = 6; +static constexpr int RIGHT_ENA = -1; +static constexpr int RIGHT_ENB = -1; + +static probot::motor::BoardozaVNHMotorDriver leftDriver(LEFT_INA, LEFT_INB, LEFT_PWM, LEFT_ENA, LEFT_ENB); +static probot::motor::BoardozaVNHMotorDriver rightDriver(RIGHT_INA, RIGHT_INB, RIGHT_PWM, RIGHT_ENA, RIGHT_ENB); +static probot::sensors::NullEncoder leftEncoder; +static probot::sensors::NullEncoder rightEncoder; + +static const probot::control::PidConfig kPidCfg{.kp = 0.28f, .ki = 0.01f, .kd = 0.0f, .kf = 0.0f, + .out_min = -1.0f, .out_max = 1.0f}; +static probot::control::PID pidLeft(kPidCfg); +static probot::control::PID pidRight(kPidCfg); +static probot::control::ClosedLoopMotor motorLeft(&leftEncoder, &pidLeft, &leftDriver, 1.0f, 1.0f); +static probot::control::ClosedLoopMotor motorRight(&rightEncoder, &pidRight, &rightDriver, 1.0f, 1.0f); +static probot::drive::BasicTankDrive chassis(&motorLeft, &motorRight); + +PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); + +void robotInit() { + Serial.begin(115200); + delay(100); + + leftDriver.begin(); + rightDriver.begin(); + leftDriver.setBrakeMode(true); + rightDriver.setBrakeMode(true); + + motorLeft.setTimeoutMs(0); + motorRight.setTimeoutMs(0); + + chassis.setWheelCircumference(31.4f); // örnek teker çapı ayarı + chassis.setTrackWidth(28.0f); // örnek şasi genişliği + + Serial.println("[TankDriveDemo] robotInit: Tank şasi hazır"); +} + +void robotEnd() { + chassis.setVelocity(0.0f, 0.0f); + motorLeft.setPowerDirect(0.0f); + motorRight.setPowerDirect(0.0f); + Serial.println("[TankDriveDemo] robotEnd: Motorlar kapandı"); +} + +void teleopInit() { + Serial.println("[TankDriveDemo] teleopInit: Sol/Sağ joystick ile tank sürüşü"); +} + +void teleopLoop() { + auto js = probot::io::joystick_api::makeDefault(); + + float leftCmd = js.getLeftY() * 120.0f; // cm/s cinsinden hedef hız + float rightCmd = js.getRightY() * 120.0f; + + chassis.setVelocity(leftCmd, rightCmd); + + uint32_t now = millis(); + chassis.update(now, 20); + + Serial.printf("[TankDriveDemo] left=%.1f right=%.1f outL=%.2f outR=%.2f\n", + leftCmd, rightCmd, + motorLeft.lastOutput(), motorRight.lastOutput()); + + delay(20); +} + +void autonomousInit() { + Serial.println("[TankDriveDemo] autonomousInit: 3 adımda ilerleme testi"); +} + +void autonomousLoop() { + static uint32_t stateStart = millis(); + static int state = 0; + + uint32_t now = millis(); + switch (state) { + case 0: // 1 metre ileri + chassis.driveDistance(100.0f); + stateStart = now; + state = 1; + break; + case 1: + if (now - stateStart > 3000) { + chassis.turnDegrees(90.0f); + stateStart = now; + state = 2; + } + break; + case 2: + if (now - stateStart > 2500) { + chassis.driveDistance(50.0f); + state = 3; + } + break; + default: + chassis.setVelocity(0.0f, 0.0f); + break; + } + + chassis.update(now, 20); + delay(20); +} diff --git a/examples/TurretDemo/TurretDemo.ino b/examples/TurretDemo/TurretDemo.ino new file mode 100644 index 0000000..25e248f --- /dev/null +++ b/examples/TurretDemo/TurretDemo.ino @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include +#include + +// Taret döndürme motoru için Boardoza VNH pinleri. +static constexpr int PIN_INA = 30; +static constexpr int PIN_INB = 31; +static constexpr int PIN_PWM = 32; +static constexpr int PIN_ENA = -1; +static constexpr int PIN_ENB = -1; + +static probot::motor::BoardozaVNHMotorDriver motor(PIN_INA, PIN_INB, PIN_PWM, PIN_ENA, PIN_ENB); +static probot::sensors::NullEncoder encoder; +static const probot::control::PidConfig kPositionPid{.kp = 0.6f, .ki = 0.0f, .kd = 0.0f, + .kf = 0.0f, .out_min = -1.0f, .out_max = 1.0f}; +static probot::control::PID pid(kPositionPid); +static probot::control::ClosedLoopMotor turretMotor(&encoder, &pid, &motor, 1.0f, 1.0f); +static probot::mechanism::Turret turret(&turretMotor); + +PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); + +void robotInit() { + Serial.begin(115200); + delay(100); + + motor.begin(); + motor.setBrakeMode(true); + + turretMotor.selectDefaultSlot(probot::control::ControlType::kPosition, 0); + turretMotor.setPidSlotConfig(0, kPositionPid); + turretMotor.setTimeoutMs(0); + + turret.setDegreesToTicks(100.0f); // encoder dönüşümünü kendi mekanizmanıza göre ayarlayın + turret.setAngleLimits(-120.0f, 120.0f); // güvenli dönüş aralığı + turret.setTargetAngleDeg(0.0f); + turret.setSlewRateLimit(150.0f); // hızlı bile olsa yumuşatma olsun + + Serial.println("[TurretDemo] robotInit: Taret kontrolü hazır"); +} + +void robotEnd() { + turretMotor.setPowerDirect(0.0f); + Serial.println("[TurretDemo] robotEnd: Taret durduruldu"); +} + +void teleopInit() { + Serial.println("[TurretDemo] teleopInit:" + " sağ çubuk X hedef açıyı belirler, B butonu sıfırlar"); +} + +void teleopLoop() { + auto js = probot::io::joystick_api::makeDefault(); + + if (js.getRawButton(1)) { // B butonu + turret.setTargetAngleDeg(0.0f); + } else { + float stick = js.getRightX(); // -1..1 + turret.setTargetAngleDeg(stick * 90.0f); // ±90 derece + } + + turret.update(millis(), 20); + turretMotor.update(millis(), 20); + + Serial.printf("[TurretDemo] hedef=%.1f ölçüm=%.1f çıkış=%.2f\n", + turret.getTargetAngleDeg(), + turret.getCurrentAngleDeg(), + turretMotor.lastOutput()); + + delay(20); +} + +void autonomousInit() { + Serial.println("[TurretDemo] autonomousInit: Rastgele preset taraması"); +} + +void autonomousLoop() { + static uint32_t stateStart = millis(); + static int state = 0; + uint32_t now = millis(); + + if (state == 0) { + turret.setTargetAngleDeg(-60.0f); + stateStart = now; + state = 1; + } else if (state == 1 && now - stateStart > 1500) { + turret.setTargetAngleDeg(60.0f); + stateStart = now; + state = 2; + } else if (state == 2 && now - stateStart > 1500) { + turret.setTargetAngleDeg(0.0f); + state = 3; + } + + turret.update(now, 20); + turretMotor.update(now, 20); + delay(20); +} diff --git a/examples/__library_impl/.gitkeep b/examples/__library_impl/.gitkeep deleted file mode 100644 index 0519ecb..0000000 --- a/examples/__library_impl/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/__library_impl/Basic/Basic.ino b/examples/__library_impl/Basic/Basic.ino deleted file mode 100644 index 01e03c3..0000000 --- a/examples/__library_impl/Basic/Basic.ino +++ /dev/null @@ -1,45 +0,0 @@ -#include - -static probot::control::BlinkPid* g_pid = nullptr; - -void robotInit() { - Serial.printf("[USER ] robotInit (core %d)\n", xPortGetCoreID()); -} - -void robotEnd() { - Serial.printf("[USER ] robotEnd (core %d)\n", xPortGetCoreID()); -} - -void teleopInit() { - static probot::control::BlinkPid pid_instance; // program ömrü boyunca - g_pid = &pid_instance; - control::setGlobalPeriodMs(20); - control::attach(g_pid); - Serial.println("[USER ] teleopInit: BlinkPid attached (20 ms global)"); -} - -void teleopLoop() { - uint32_t last_sec = joystick::get_ms() / 1000u; - while (true) { - uint32_t ms = joystick::get_ms(); - uint32_t sec = ms / 1000u; - if (sec != last_sec && g_pid){ - last_sec = sec; - g_pid->setReference(sec); - Serial.printf("[USER] setReference(sec=%lu)\n", (unsigned long)sec); - } - auto js = joystick::read(); - Serial.printf("[USER/teleopLoop] seq=%lu ms=%lums (core %d)\n", - (unsigned long)js.seq, (unsigned long)js.ms, xPortGetCoreID()); - delay(500); - } -} - -void autonomousInit() { - Serial.printf("[USER ] autonomousInit (core %d)\n", xPortGetCoreID()); -} - -void autonomousLoop() { - Serial.printf("[USER ] autonomousLoop (core %d)\n", xPortGetCoreID()); - delay(1000); -} diff --git a/examples/__library_impl/ClosedLoopDemo/ClosedLoopDemo.ino b/examples/__library_impl/ClosedLoopDemo/ClosedLoopDemo.ino deleted file mode 100644 index ae3f5f7..0000000 --- a/examples/__library_impl/ClosedLoopDemo/ClosedLoopDemo.ino +++ /dev/null @@ -1,64 +0,0 @@ -#include - -// TestClosedLoopDemo -// Demonstrates ClosedLoopMotor with simulated components - -static probot::test::TestMotor g_motor; -static probot::test::TestEncoder g_encoder; -static probot::control::PidConfig g_cfg_vel{ 0.0008f, 0.0004f, 0.0f, 0.0f, -1.0f, 1.0f }; -static probot::control::PID g_pid(g_cfg_vel); -static probot::control::ClosedLoopMotor* g_axis = nullptr; -static probot::test::TestPlant* g_plant = nullptr; - -class LedVisualizer : public control::IUpdatable { -public: - LedVisualizer(probot::sensors::IEncoder* enc, float full_scale_tps) - : enc_(enc), full_scale_tps_(full_scale_tps) {} - void update(uint32_t now_ms, uint32_t dt_ms) override { - (void)now_ms; (void)dt_ms; - int32_t tps = enc_ ? enc_->readTicksPerSecond() : 0; - if (tps < 0) tps = -tps; - float x = (float)tps / (full_scale_tps_ > 1.0f ? full_scale_tps_ : 1.0f); - if (x > 1.0f) x = 1.0f; - uint8_t br = (uint8_t)(x * 255.0f); - probot::builtinled::setBrightness(br); - probot::builtinled::set(true); - } -private: - probot::sensors::IEncoder* enc_; - float full_scale_tps_; -}; - -static LedVisualizer* g_ledviz = nullptr; - -void robotInit() {} -void robotEnd() {} - -void teleopInit() { - static probot::control::ClosedLoopMotor axis(&g_encoder, &g_pid, &g_motor, 1.0f, 1.0f); - g_axis = &axis; - - g_axis->setPidSlotConfig(0, g_cfg_vel); - g_axis->selectDefaultSlot(probot::control::ControlType::kVelocity, 0); - - g_motor.setInverted(false); - - static probot::test::TestPlant plant(&g_motor, &g_encoder); - g_plant = &plant; - - static LedVisualizer ledviz(&g_encoder, 400.0f); // full brightness at ~400 tps - g_ledviz = &ledviz; - - control::setGlobalPeriodMs(20); - control::attach(g_axis); - control::attach(g_plant); - control::attach(g_ledviz); - - Serial.println("[DEMO ] teleopInit: ClosedLoop + Plant + LED attached (20 ms)"); -} - -void teleopLoop() { delay(500); } - -void autonomousInit() {} - -void autonomousLoop() { delay(1000); } diff --git a/examples/__library_impl/DriverStationDemo/DriverStationDemo.ino b/examples/__library_impl/DriverStationDemo/DriverStationDemo.ino deleted file mode 100644 index 7bc0e7a..0000000 --- a/examples/__library_impl/DriverStationDemo/DriverStationDemo.ino +++ /dev/null @@ -1,18 +0,0 @@ -#include -#include -#include - -static probot::robot::StateService g_state; -static probot::io::GamepadService g_gamepad; -static probot::platform::esp32::DriverStation g_ds(g_state, g_gamepad); - -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -// TestDriverStationDemo - -void robotInit() {} -void robotEnd() {} -void teleopInit() {} -void teleopLoop() { delay(100); } -void autonomousInit() {} -void autonomousLoop() { delay(1000); } \ No newline at end of file diff --git a/examples/__library_impl/LoopPeriodStress/LoopPeriodStress.ino b/examples/__library_impl/LoopPeriodStress/LoopPeriodStress.ino deleted file mode 100644 index f188b8b..0000000 --- a/examples/__library_impl/LoopPeriodStress/LoopPeriodStress.ino +++ /dev/null @@ -1,10 +0,0 @@ -#include - -// TestLoopPeriodStress - -void robotInit() {} -void robotEnd() {} -void teleopInit() {} -void teleopLoop() { delay(50); } -void autonomousInit() {} -void autonomousLoop() { delay(1000); } \ No newline at end of file diff --git a/examples/__library_impl/RGBClosedLoop/RGBClosedLoop.ino b/examples/__library_impl/RGBClosedLoop/RGBClosedLoop.ino deleted file mode 100644 index 8a49a08..0000000 --- a/examples/__library_impl/RGBClosedLoop/RGBClosedLoop.ino +++ /dev/null @@ -1,10 +0,0 @@ -#include - -// TestRGBClosedLoop - -void robotInit() {} -void robotEnd() {} -void teleopInit() {} -void teleopLoop() { delay(500); } -void autonomousInit() {} -void autonomousLoop() { delay(1000); } \ No newline at end of file diff --git a/examples/__library_impl/TestSlider/TestSlider.ino b/examples/__library_impl/TestSlider/TestSlider.ino deleted file mode 100644 index b380ed7..0000000 --- a/examples/__library_impl/TestSlider/TestSlider.ino +++ /dev/null @@ -1,44 +0,0 @@ -#include -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -// TestSlider: demonstrate Slider controlling position of a single axis - -static probot::test::TestMotor mot; -static probot::test::TestEncoder enc; -static probot::control::PidConfig cfgP{ 0.0012f, 0.0f, 0.00002f, 0.0f, -1.0f, 1.0f }; -static probot::control::PID pid(cfgP); -static probot::control::ClosedLoopMotor* axis=nullptr; -static probot::mechanism::Slider* slider=nullptr; - -void robotInit(){ - control::setGlobalPeriodMs(20); - static probot::control::ClosedLoopMotor x(&enc, &pid, &mot, 1.0f, 1.0f); - axis = &x; - axis->setPidSlotConfig(1, cfgP); - axis->selectDefaultSlot(probot::control::ControlType::kPosition, 1); - static probot::test::TestPlant plant(&mot, &enc); - control::attach(axis); - control::attach(&plant); - - static probot::mechanism::Slider s(axis); - slider = &s; - slider->setLengthToTicks(100.0f); // 1 unit -> 100 ticks - control::attach(slider); -} - -void robotEnd() {} -void teleopInit(){ Serial.println("[SLID] teleopInit"); } - -void teleopLoop(){ - static uint32_t last=0; uint32_t now=joystick::get_ms(); - if (now - last >= 2000){ - last = now; - static bool toggle=false; toggle=!toggle; - slider->setTargetLength(toggle ? 5.0f : 0.0f); // units - Serial.printf("[SLID] target=%.1f units\n", slider->getTargetLength()); - } - delay(20); -} - -void autonomousInit(){} -void autonomousLoop(){ delay(1000); } diff --git a/examples/__library_impl/TestTankDrive/TestTankDrive.ino b/examples/__library_impl/TestTankDrive/TestTankDrive.ino deleted file mode 100644 index d63499c..0000000 --- a/examples/__library_impl/TestTankDrive/TestTankDrive.ino +++ /dev/null @@ -1,59 +0,0 @@ -#include -PROBOT_SET_DRIVER_STATION_PASSWORD("ProBot1234"); - -// TestTankDrive: demonstrate BasicTankDrive chassis and Slider with sims - -static probot::test::TestMotor motL, motR; -static probot::test::TestEncoder encL, encR; -static probot::control::PidConfig cfgV{ 0.0009f, 0.0003f, 0.00002f, 0.0f, -1.0f, 1.0f }; -static probot::control::PidConfig cfgP{ 0.0008f, 0.0f, 0.00001f, 0.0f, -1.0f, 1.0f }; -static probot::control::PID pidL(cfgV), pidR(cfgV); -static probot::control::ClosedLoopMotor *axisL=nullptr, *axisR=nullptr; -static probot::drive::BasicTankDrive* chassis=nullptr; - -void robotInit() { - control::setGlobalPeriodMs(20); - static probot::control::ClosedLoopMotor xL(&encL, &pidL, &motL, 1.0f, 1.0f); - static probot::control::ClosedLoopMotor xR(&encR, &pidR, &motR, 1.0f, 1.0f); - axisL = &xL; axisR = &xR; - axisL->setPidSlotConfig(0, cfgV); axisL->setPidSlotConfig(1, cfgP); - axisR->setPidSlotConfig(0, cfgV); axisR->setPidSlotConfig(1, cfgP); - axisL->selectDefaultSlot(probot::control::ControlType::kVelocity, 0); - axisR->selectDefaultSlot(probot::control::ControlType::kVelocity, 0); - - static probot::test::TestPlant pL(&motL, &encL); - static probot::test::TestPlant pR(&motR, &encR); - - control::attach(axisL); - control::attach(axisR); - control::attach(&pL); - control::attach(&pR); - - static probot::drive::BasicTankDrive ch(axisL, axisR); - chassis = &ch; - chassis->setWheelCircumference(20.0f); // arbitrary units - chassis->setTrackWidth(30.0f); -} - -void robotEnd() {} - -void teleopInit() { - Serial.println("[TANK] teleopInit: velocity drive"); -} - -void teleopLoop() { - // Alternate velocity and position commands - static uint32_t last=0; uint32_t now=joystick::get_ms(); - if (now - last >= 2000){ - last = now; - static int step=0; step = (step+1)%4; - if (step==0){ chassis->setVelocity(200.0f, 200.0f); Serial.println("[TANK] vel fwd"); } - else if (step==1){ chassis->setVelocity(-200.0f, -200.0f); Serial.println("[TANK] vel back"); } - else if (step==2){ chassis->driveDistance(10.0f); Serial.println("[TANK] drive +10"); } - else { chassis->turnDegrees(90.0f); Serial.println("[TANK] turn +90"); } - } - delay(20); -} - -void autonomousInit() { Serial.println("[TANK] auto: drive 10, turn 90"); chassis->driveDistance(10.0f); } -void autonomousLoop() { delay(1000); } diff --git a/examples/nfr/AdvancedMecanumDrive/AdvancedMecanumDrive.ino b/examples/nfr/AdvancedMecanumDrive/AdvancedMecanumDrive.ino deleted file mode 100644 index f80ce5b..0000000 --- a/examples/nfr/AdvancedMecanumDrive/AdvancedMecanumDrive.ino +++ /dev/null @@ -1,81 +0,0 @@ -#include -#include -#include -#include -#include - -static probot::motor::NullMotor flHW, frHW, rlHW, rrHW; -static probot::motor::MotorHandle flHandle(flHW), frHandle(frHW), rlHandle(rlHW), rrHandle(rrHW); - -class NullMotorController : public probot::control::IMotorController { -public: - explicit NullMotorController(probot::motor::MotorHandle& handle) : handle_(handle) {} - bool claim(void* owner) override { return handle_.underlying().claim(owner); } - void release(void* owner) override { handle_.underlying().release(owner); } - bool setPower(float power, void* owner) override { return handle_.underlying().setPower(power, owner); } - bool isClaimed() const override { return handle_.underlying().isClaimed(); } - void* currentOwner() const override { return handle_.underlying().currentOwner(); } - void setInverted(bool inv) override { handle_.setInverted(inv); } - bool getInverted() const override { return handle_.getInverted(); } - void setSetpoint(float, probot::controllers::ControlType, int) override {} - void setTimeoutMs(uint32_t) override {} - void setPidSlotConfig(int, const probot::control::PidConfig&) override {} - void selectDefaultSlot(probot::controllers::ControlType, int) override {} - int defaultSlot(probot::controllers::ControlType) const override { return 0; } - float lastSetpoint() const override { return 0.0f; } - float lastMeasurement() const override { return 0.0f; } - float lastOutput() const override { return 0.0f; } - probot::controllers::ControlType activeMode() const override { return probot::controllers::ControlType::kVelocity; } - bool isAtTarget(float) const override { return false; } - void setMotionProfile(probot::control::MotionProfileType type) override { profileType_ = type; } - probot::control::MotionProfileType motionProfile() const override { return profileType_; } - void setMotionProfileConfig(const probot::control::MotionProfileConfig& cfg) override { profileCfg_ = cfg; } - probot::control::MotionProfileConfig motionProfileConfig() const override { return profileCfg_; } - void update(uint32_t, uint32_t) override {} -private: - probot::motor::MotorHandle& handle_; - probot::control::MotionProfileType profileType_{probot::control::MotionProfileType::kNone}; - probot::control::MotionProfileConfig profileCfg_{}; -}; - -static NullMotorController fl(flHandle), fr(frHandle), rl(rlHandle), rr(rrHandle); -static probot::chassis::NfrAdvancedMecanumDrive::Config config; -static probot::chassis::NfrAdvancedMecanumDrive chassis(&fl, &fr, &rl, &rr, config); -static probot::sensors::imu::Mpu6050 imu; - -static probot::control::kinematics::WheelPositions4 wheelPositions{}; - -void robotInit(){ - Serial.println("[AdvancedMecanumDrive] robotInit"); - chassis.setImu(&imu); - chassis.setFeedforwardGains(0.05f, 0.9f, 0.1f); - chassis.setWheelMotionProfile(probot::control::MotionProfileType::kTrapezoid, - {0.6f, 1.8f, 0.0f}); - chassis.useMotorControllerVelocityLoop(true); - chassis.resetPose(probot::control::Pose2d(), 0.0f, wheelPositions); -} - -void teleopInit(){ Serial.println("[AdvancedMecanumDrive] teleopInit"); } - -void teleopLoop(){ - static uint32_t lastMs = 0; - uint32_t now = millis(); - float dt = (now - lastMs) * 0.001f; - if (dt <= 0.0f) dt = 0.02f; - lastMs = now; - - // Simulated wheel motion for demo (forward motion) - float delta = 0.3f * dt; - wheelPositions.frontLeft += delta; - wheelPositions.frontRight += delta; - wheelPositions.rearLeft += delta; - wheelPositions.rearRight += delta; - - chassis.setTargetSpeeds(probot::control::ChassisSpeeds(0.3f, 0.0f, 0.0f)); - chassis.update(now * 0.001f, wheelPositions); - delay(20); -} - -void autonomousInit(){ Serial.println("[AdvancedMecanumDrive] autonomousInit"); } -void autonomousLoop(){ delay(20); } -void robotEnd(){ Serial.println("[AdvancedMecanumDrive] robotEnd"); } diff --git a/examples/nfr/AdvancedTankDrive/AdvancedTankDrive.ino b/examples/nfr/AdvancedTankDrive/AdvancedTankDrive.ino deleted file mode 100644 index 2a83069..0000000 --- a/examples/nfr/AdvancedTankDrive/AdvancedTankDrive.ino +++ /dev/null @@ -1,96 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -// Example demonstrating the NFRAdvancedTankDrive structure with simulated hardware. - -static probot::motor::NullMotor leftHW; -static probot::motor::NullMotor rightHW; -static probot::motor::MotorHandle leftHandle(leftHW); -static probot::motor::MotorHandle rightHandle(rightHW); - -static probot::control::PID pidLeft({0.4f,0.0f,0.0f,-1.0f,1.0f}); -static probot::control::PID pidRight({0.4f,0.0f,0.0f,-1.0f,1.0f}); - -// Null controllers stand in for real motor controllers – swap with real ones on hardware. -class NullMotorController : public probot::control::IMotorController { -public: - bool claim(void* owner) override { return handle_.underlying().claim(owner); } - void release(void* owner) override { handle_.underlying().release(owner); } - bool setPower(float power, void* owner) override { return handle_.underlying().setPower(power, owner); } - bool isClaimed() const override { return handle_.underlying().isClaimed(); } - void* currentOwner() const override { return handle_.underlying().currentOwner(); } - void setInverted(bool inv) override { handle_.setInverted(inv); } - bool getInverted() const override { return handle_.getInverted(); } - - void setSetpoint(float value, probot::control::ControlType mode, int slot = -1) override {} - void setTimeoutMs(uint32_t) override {} - void setPidSlotConfig(int, const probot::control::PidConfig&) override {} - void selectDefaultSlot(probot::control::ControlType, int) override {} - int defaultSlot(probot::control::ControlType) const override { return 0; } - float lastSetpoint() const override { return 0.0f; } - float lastMeasurement() const override { return 0.0f; } - float lastOutput() const override { return 0.0f; } - probot::control::ControlType activeMode() const override { return probot::control::ControlType::kVelocity; } - bool isAtTarget(float) const override { return false; } - void setMotionProfile(probot::control::MotionProfileType type) override { profileType_ = type; } - probot::control::MotionProfileType motionProfile() const override { return profileType_; } - void setMotionProfileConfig(const probot::control::MotionProfileConfig& cfg) override { profileCfg_ = cfg; } - probot::control::MotionProfileConfig motionProfileConfig() const override { return profileCfg_; } - void update(uint32_t, uint32_t) override {} - - NullMotorController(probot::motor::MotorHandle& handle) : handle_(handle) {} -private: - probot::motor::MotorHandle& handle_; - probot::control::MotionProfileType profileType_{probot::control::MotionProfileType::kNone}; - probot::control::MotionProfileConfig profileCfg_{}; -}; - -static NullMotorController leftController(leftHandle); -static NullMotorController rightController(rightHandle); - -static probot::chassis::NfrAdvancedTankDrive::Config config; -static probot::chassis::NfrAdvancedTankDrive chassis(&leftController, &rightController, config); -static probot::sensors::imu::Mpu6050 imu; - -// Simulated encoder positions -static float leftPosition = 0.0f; -static float rightPosition = 0.0f; - -void robotInit(){ - Serial.println("[AdvancedTankDrive] robotInit"); - chassis.setImu(&imu); - chassis.setFeedforwardGains(0.1f, 1.0f, 0.1f); - chassis.setWheelMotionProfile(probot::control::MotionProfileType::kTrapezoid, - {0.8f, 2.0f, 0.0f}); - chassis.useMotorControllerVelocityLoop(true); - chassis.setVelocityPidConfig({0.5f,0.0f,0.0f,0.0f,-1.0f,1.0f}); - chassis.setRamseteParams(2.0f, 0.7f); - chassis.resetPose(probot::control::Pose2d(), leftPosition, rightPosition); -} - -void teleopInit(){ Serial.println("[AdvancedTankDrive] teleopInit"); } - -void teleopLoop(){ - static uint32_t last = 0; - uint32_t now = millis(); - float dt = (now - last) * 0.001f; - if (dt <= 0.0f) dt = 0.02f; - last = now; - - float desiredV = 0.5f; - leftPosition += desiredV * dt; - rightPosition += desiredV * dt; - - chassis.setTargetSpeeds(probot::control::ChassisSpeeds(desiredV, 0.0f, 0.0f)); - chassis.update(now * 0.001f, leftPosition, rightPosition); - delay(20); -} - -void autonomousInit(){ Serial.println("[AdvancedTankDrive] autonomousInit"); } -void autonomousLoop(){ delay(20); } -void robotEnd(){ Serial.println("[AdvancedTankDrive] robotEnd"); } diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..d0bcdef --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,8 @@ +version: 0.1.4 +description: ESP32-S3 robotics control library +url: https://github.com/nfrproducts/probot-lib +dependencies: + idf: + version: ">=5.0" + espressif/arduino-esp32: + version: ">=3.0.0" diff --git a/library.json b/library.json new file mode 100644 index 0000000..8649faf --- /dev/null +++ b/library.json @@ -0,0 +1,30 @@ +{ + "build": { + "flags": [ + "-DESP32S3", + "-DARDUINO_USB_MODE=1" + ] + }, + "dependencies": [ + { + "name": "Adafruit NeoPixel", + "version": ">=1.12.0" + } + ], + "description": "ESP32-S3 robotics control library", + "frameworks": "arduino", + "keywords": [ + "robotics", + "pid", + "motion-control" + ], + "name": "Probot Lib", + "platforms": [ + "espressif32" + ], + "repository": { + "type": "git", + "url": "https://github.com/nfrproducts/probot-lib" + }, + "version": "0.1.4" +} diff --git a/library.properties b/library.properties index a946ea2..39712d3 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=probot -version=0.1.2 +version=0.1.4 author=Tuna Gül maintainer=Tuna Gül sentence=ProBot Library for Robotics Competitions. diff --git a/src/core/scheduler.cpp b/src/core/scheduler.cpp index 39b36b2..102cd5b 100644 --- a/src/core/scheduler.cpp +++ b/src/core/scheduler.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace control { enum CmdType : uint8_t { CMD_ATTACH=0, CMD_DETACH=1 }; @@ -34,6 +35,7 @@ namespace control { if (!qCmd){ qCmd = xQueueCreate(queue_len, sizeof(Cmd)); } + probot::logging::configureDefaults(); } static int find_slot(IUpdatable* obj){ @@ -84,11 +86,13 @@ namespace control { Cmd cmd; while (xQueueReceive(qCmd, &cmd, 0) == pdTRUE){ if (cmd.type == CMD_ATTACH){ - int idx = find_slot(cmd.obj); + auto existing = probot::logging::resolveForDetach(cmd.obj); + int idx = find_slot(existing); if (idx < 0) idx = find_free_slot(); if (idx >= 0){ slots[idx].in_use = true; - slots[idx].obj = cmd.obj; + auto wrapped = probot::logging::wrapForScheduler(cmd.obj); + slots[idx].obj = wrapped; uint32_t t = millis(); slots[idx].last_call_ms = t; slots[idx].next_due_ms = t + g_global_period_ms; @@ -101,9 +105,11 @@ namespace control { #endif } } else { - int idx = find_slot(cmd.obj); + auto wrapped = probot::logging::resolveForDetach(cmd.obj); + int idx = find_slot(wrapped); if (idx >= 0){ slots[idx] = {false, nullptr, 0, 0}; + probot::logging::releaseForDetach(cmd.obj); #ifndef PROBOT_SCHED_NOLOG Serial.printf("[CMD ] detach ok (idx=%d)\n", idx); #endif @@ -144,8 +150,9 @@ namespace control { } if (deadlineMiss){ probot::robot::state().setDeadlineMiss(now, true); + probot::logging::notifySchedulerOverrun(now, maxOverrun); #ifndef PROBOT_SCHED_NOLOG - Serial.printf("[SCHED] ⚠️ CONTROL LOOP OVERRUN! Period: %lums, Overrun: +%lums\n", + Serial.printf("[SCHED] ⚠️ CONTROL LOOP OVERRUN! Period: %lums, Overrun: +%lums\n", (unsigned long)g_global_period_ms, (unsigned long)maxOverrun); #endif } @@ -161,11 +168,13 @@ namespace control { } if (xQueueReceive(qCmd, &cmd, waitTicks) == pdTRUE){ if (cmd.type == CMD_ATTACH){ - int idx = find_slot(cmd.obj); + auto existing = probot::logging::resolveForDetach(cmd.obj); + int idx = find_slot(existing); if (idx < 0) idx = find_free_slot(); if (idx >= 0){ slots[idx].in_use = true; - slots[idx].obj = cmd.obj; + auto wrapped = probot::logging::wrapForScheduler(cmd.obj); + slots[idx].obj = wrapped; uint32_t t = millis(); slots[idx].last_call_ms = t; slots[idx].next_due_ms = t + g_global_period_ms; @@ -178,9 +187,11 @@ namespace control { #endif } } else { - int idx = find_slot(cmd.obj); + auto wrapped = probot::logging::resolveForDetach(cmd.obj); + int idx = find_slot(wrapped); if (idx >= 0){ slots[idx] = {false, nullptr, 0, 0}; + probot::logging::releaseForDetach(cmd.obj); #ifndef PROBOT_SCHED_NOLOG Serial.printf("[CMD ] detach ok (idx=%d)\n", idx); #endif @@ -193,4 +204,4 @@ namespace control { } } } -} \ No newline at end of file +} diff --git a/src/platform/esp32s3/web/driver_station_esp32.hpp b/src/platform/esp32s3/web/driver_station_esp32.hpp index 2e0775b..11886d1 100644 --- a/src/platform/esp32s3/web/driver_station_esp32.hpp +++ b/src/platform/esp32s3/web/driver_station_esp32.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "index_html.h" #if defined(PROBOT_WITH_DS) && !defined(PROBOT_WIFI_AP_PASSWORD) @@ -31,6 +32,7 @@ namespace probot::platform::esp32 { return; } String ssid = generateSSID(); + ap_ssid_ = ssid; WiFi.mode(WIFI_AP); WiFi.softAP(ssid.c_str(), pw); @@ -47,6 +49,11 @@ namespace probot::platform::esp32 { _server.on("/updateController", HTTP_POST, [this](){ if (!enforceOwner()) return; handleUpdateController(); }); _server.on("/robotControl", HTTP_GET, [this](){ if (!enforceOwner()) return; handleRobotControl(); }); _server.on("/getBattery", HTTP_GET, [this](){ handleGetBattery(); }); + _server.on("/logging/status", HTTP_GET, [this](){ handleLoggingStatus(); }); + _server.on("/logging/stream", HTTP_GET, [this](){ handleLoggingStream(); }); + _server.on("/logging/clear", HTTP_POST, [this](){ handleLoggingClear(); }); + _server.on("/logging/toggle", HTTP_POST, [this](){ handleLoggingToggle(); }); + _server.on("/logging/download", HTTP_GET, [this](){ handleLoggingDownload(); }); _server.begin(); } @@ -129,12 +136,77 @@ namespace probot::platform::esp32 { _server.send(200, "text/plain", "OK"); } + void handleLoggingStatus(){ + probot::logging::LoggingStatus status{}; + probot::logging::loggingStatus(status); + String json = "{"; + bool wifiConnected = WiFi.getMode() != WIFI_MODE_NULL; + json += "\"connected\":"; + json += (wifiConnected ? "true" : "false"); + json += ",\"streaming\":"; + json += (status.wifi_streaming && wifiConnected ? "true" : "false"); + json += ",\"ssid\":\""; + json += ap_ssid_; + json += "\",\"ip\":\""; + json += WiFi.softAPIP().toString(); + json += "\",\"clients\":"; + json += WiFi.softAPgetStationNum(); + json += ",\"rate\":"; + float rate = 0.0f; + if (status.wifi_stream_start_ms > 0){ + uint32_t elapsed = millis() - status.wifi_stream_start_ms; + if (elapsed > 0){ + rate = (static_cast(status.wifi_bytes_sent) * 1000.0f) / static_cast(elapsed); + } + } + json += static_cast(rate); + json += ",\"serial_drop\":"; + json += status.serial_drop; + json += ",\"wifi_drop\":"; + json += status.wifi_drop; + json += ",\"detail\":\""; + json += (status.wifi_streaming ? "Streaming" : "Ready"); + json += "\"}"; + _server.send(200, "application/json", json); + } + + void handleLoggingStream(){ + int tail = 200; + if (_server.hasArg("tail")){ + tail = _server.arg("tail").toInt(); + if (tail < 0) tail = 0; + } + std::string buffer; + probot::logging::loggingStream(buffer, static_cast(tail)); + _server.send(200, "text/plain", buffer.c_str()); + } + + void handleLoggingClear(){ + probot::logging::clearLoggingStream(); + _server.send(200, "text/plain", "cleared"); + } + + void handleLoggingToggle(){ + String payload = _server.arg("plain"); + bool enable = payload.indexOf("stop") < 0; + probot::logging::setWifiStreamingEnabled(enable); + _server.send(200, "text/plain", enable ? "started" : "stopped"); + } + + void handleLoggingDownload(){ + std::string buffer; + probot::logging::loggingStream(buffer, 0); + _server.sendHeader("Content-Disposition", "attachment; filename=probot_logs.txt"); + _server.send(200, "text/plain", buffer.c_str()); + } + robot::StateService& _rs; io::GamepadService& _gs; WebServer _server; const char* _apPass; bool _owner_set=false; IPAddress _owner; + String ap_ssid_; }; } -#endif // ESP32 \ No newline at end of file +#endif // ESP32 diff --git a/src/platform/esp32s3/web/index_html.h b/src/platform/esp32s3/web/index_html.h index 14b7596..5be22bc 100644 --- a/src/platform/esp32s3/web/index_html.h +++ b/src/platform/esp32s3/web/index_html.h @@ -7,70 +7,1435 @@ const char MAIN_page[] PROGMEM = R"=====( - + - ProBot UI + ProBot Minimal Stack - -
-
-

Main Drive

-

Joystick Status: Not Connected

- -

-

-

+
+
+

ProBot DriveStation

+ by NFR Products · c2025
-
-

Joystick Test

-

Connect one or more controllers, press a button to register them, then pick which to use: - -

-

Selected Gamepad Info

-
No gamepad selected.
-

Axes

-
No data yet...
-

Buttons

-
No data yet...
+ +
+ Status + Standby + Awaiting command
+
+
+
+
+

Match Control

+
+ +
+ + + Seconds +
+
+ Autonomous + +
+
+
+
+ Autonomous Countdown + 00.0 s +
+
+
+
+
+
+
+

Joystick Check

+
+
+
+
+
+
+ Axes +
+
+
+
+ Not Connected + +
+
+
+
+
+

Wi-Fi Logging

+
+ Disconnected + Waiting for Wi-Fi module... +
+
+
+ Network + +
+
+ Robot IP + +
+
+ Clients + 0 +
+
+ Rate + 0 KB/s +
+
+
+ + + +
+
+ + + Last sync: never +
+
+
+

System Logs

+ +
No gamepad selected.
+
No axis data...
+
No button data...
+
+
+ Wi-Fi Log Stream + +
+
Waiting for log data...
+
+
+
+ let controlState="idle"; + let autoModeEnabled=false; + let selectedGamepadIndex=-1; + let gamepads={}; + let gamepadDetected=false; + + const headerEl=document.getElementById('appHeader'); +const headerStatusValue=document.getElementById('headerStatusValue'); +const headerStatusDetail=document.getElementById('headerStatusDetail'); + +const loggingElements={ + pill:document.getElementById('loggingStatePill'), + detail:document.getElementById('loggingDetail'), + ssid:document.getElementById('loggingSsid'), + ip:document.getElementById('loggingIp'), + clients:document.getElementById('loggingClients'), + rate:document.getElementById('loggingRate'), + toggleBtn:document.getElementById('loggingToggleButton'), + refreshBtn:document.getElementById('loggingRefreshButton'), + clearBtn:document.getElementById('loggingClearButton'), + autoRefresh:document.getElementById('loggingAutoRefresh'), + autoScroll:document.getElementById('loggingAutoScroll'), + lastUpdated:document.getElementById('loggingLastUpdated'), + stream:document.getElementById('wifiLogStream'), + downloadBtn:document.getElementById('loggingDownloadButton') +}; + +const LOGGING_ENDPOINTS={ + status:"/logging/status", + toggle:"/logging/toggle", + clear:"/logging/clear", + stream:"/logging/stream?tail=200", + download:"/logging/download" +}; + +let loggingState={ + connected:false, + streaming:false, + ssid:"—", + ip:"—", + clients:0, + rate:0, + lastSync:null +}; +let loggingPollingTimer=null; +let loggingBusy=false; + +function setLoggingLastUpdated(text){ + if(loggingElements.lastUpdated){ + loggingElements.lastUpdated.textContent=`Last sync: ${text}`; + } +} + +function formatRate(value){ + if(typeof value!=="number" || !isFinite(value) || value<0){ + return "0 KB/s"; + } + const kb=value/1024; + if(kb>=1024){ + return `${(kb/1024).toFixed(1)} MB/s`; + } + return `${Math.max(0,kb).toFixed(1)} KB/s`; +} + +function applyLoggingState(data){ + loggingState.connected=!!data.connected; + loggingState.streaming=!!data.streaming; + loggingState.ssid=data.ssid || "—"; + loggingState.ip=data.ip || "—"; + loggingState.clients=Number.isFinite(data.clients)?data.clients:0; + loggingState.rate=Number.isFinite(data.rate)?data.rate:0; + loggingState.lastSync=new Date(); + + if(loggingElements.ssid) loggingElements.ssid.textContent=loggingState.ssid; + if(loggingElements.ip) loggingElements.ip.textContent=loggingState.ip; + if(loggingElements.clients) loggingElements.clients.textContent=String(loggingState.clients); + if(loggingElements.rate) loggingElements.rate.textContent=formatRate(loggingState.rate); + + if(loggingElements.pill){ + const mode=loggingState.streaming ? "streaming" : (loggingState.connected ? "connected" : "disconnected"); + loggingElements.pill.dataset.state=mode; + loggingElements.pill.textContent=mode==="streaming" ? "Streaming" : mode==="connected" ? "Connected" : "Disconnected"; + } + if(loggingElements.detail){ + loggingElements.detail.textContent=data.detail || (loggingState.connected ? "Ready to stream logs." : "Waiting for Wi-Fi module..."); + } + + if(loggingElements.toggleBtn){ + loggingElements.toggleBtn.disabled=false; + loggingElements.toggleBtn.classList.toggle('primary',!loggingState.streaming); + loggingElements.toggleBtn.classList.toggle('danger',loggingState.streaming); + loggingElements.toggleBtn.textContent=loggingState.streaming ? "Stop Logging" : "Start Logging"; + } + + setLoggingLastUpdated(loggingState.lastSync.toLocaleTimeString()); +} + +function setLoggingOffline(message){ + loggingState.connected=false; + loggingState.streaming=false; + loggingState.ssid="—"; + loggingState.ip="—"; + loggingState.clients=0; + loggingState.rate=0; + if(loggingElements.ssid) loggingElements.ssid.textContent="—"; + if(loggingElements.ip) loggingElements.ip.textContent="—"; + if(loggingElements.clients) loggingElements.clients.textContent="0"; + if(loggingElements.rate) loggingElements.rate.textContent="0 KB/s"; + if(loggingElements.pill){ + loggingElements.pill.dataset.state="disconnected"; + loggingElements.pill.textContent="Disconnected"; + } + if(loggingElements.detail){ + loggingElements.detail.textContent=message || "Wi-Fi logging unavailable."; + } + if(loggingElements.toggleBtn){ + loggingElements.toggleBtn.disabled=false; + loggingElements.toggleBtn.classList.add('primary'); + loggingElements.toggleBtn.classList.remove('danger'); + loggingElements.toggleBtn.textContent="Start Logging"; + } + setLoggingLastUpdated("offline"); +} + +function setLoggingError(message){ + if(loggingElements.pill){ + loggingElements.pill.dataset.state="error"; + loggingElements.pill.textContent="Error"; + } + if(loggingElements.detail){ + loggingElements.detail.textContent=message || "Command failed."; + } +} + +async function refreshLoggingStatus(silent=false){ + try{ + const res=await fetch(LOGGING_ENDPOINTS.status,{cache:"no-store"}); + if(!res.ok) throw new Error(`status ${res.status}`); + const payload=await res.json(); + applyLoggingState(payload); + }catch(err){ + if(!silent) console.error("logging status error",err); + setLoggingOffline("No response from robot."); + } +} + +async function refreshLogStream(silent=false){ + if(!loggingElements.stream) return; + try{ + const res=await fetch(LOGGING_ENDPOINTS.stream,{cache:"no-store"}); + if(!res.ok) throw new Error(`stream ${res.status}`); + const text=await res.text(); + updateLogStream(text || "No log data yet."); + }catch(err){ + if(!silent) console.error("log stream error",err); + } +} + +function updateLogStream(content){ + if(!loggingElements.stream) return; + loggingElements.stream.textContent=content; + if(loggingElements.autoScroll && loggingElements.autoScroll.checked){ + loggingElements.stream.scrollTop=loggingElements.stream.scrollHeight; + } +} + +function setLoggingAutoRefresh(enabled){ + if(loggingPollingTimer){ + clearInterval(loggingPollingTimer); + loggingPollingTimer=null; + } + if(enabled){ + loggingPollingTimer=setInterval(()=>{ + refreshLoggingStatus(true); + refreshLogStream(true); + },4000); + } +} + +async function handleLoggingToggle(){ + if(loggingBusy) return; + if(!loggingElements.toggleBtn) return; + loggingBusy=true; + loggingElements.toggleBtn.disabled=true; + const targetAction=loggingState.streaming ? "stop" : "start"; + loggingElements.toggleBtn.textContent=targetAction==="stop" ? "Stopping..." : "Starting..."; + try{ + const res=await fetch(LOGGING_ENDPOINTS.toggle,{ + method:"POST", + headers:{"Content-Type":"application/json"}, + body:JSON.stringify({action:targetAction}) + }); + if(!res.ok) throw new Error(`toggle ${res.status}`); + }catch(err){ + console.error("logging toggle error",err); + setLoggingError("Unable to toggle logging."); + }finally{ + loggingBusy=false; + await refreshLoggingStatus(true); + await refreshLogStream(true); + } +} + +async function handleLoggingClear(){ + if(loggingBusy) return; + try{ + const res=await fetch(LOGGING_ENDPOINTS.clear,{method:"POST"}); + if(!res.ok) throw new Error(`clear ${res.status}`); + updateLogStream("Log buffer cleared."); + await refreshLogStream(true); + }catch(err){ + console.error("logging clear error",err); + setLoggingError("Unable to clear buffer."); + } +} + +function handleLoggingDownload(){ + window.open(LOGGING_ENDPOINTS.download,"_blank","noopener"); +} + +function handleLoggingRefresh(){ + refreshLoggingStatus(); + refreshLogStream(); +} + +function setPhaseDisplay(mode){ + const map={ + init:{title:"Init", detail:"Systems primed"}, + auto:{title:"Autonomous", detail:"Running script"}, + teleop:{title:"Teleop", detail:"Drivers in control"}, + stopped:{title:"Stopped", detail:"Motors safe"}, + standby:{title:"Standby", detail:"Awaiting command"} + }; + const next = map[mode] || map.standby; + if(headerStatusValue){ + headerStatusValue.textContent = next.title; + } + if(headerStatusDetail){ + if(mode === 'auto'){ + headerStatusDetail.textContent = `Auto running – ${autoRemaining.toFixed(1)} s`; + }else{ + headerStatusDetail.textContent = next.detail; + } + } + document.body.dataset.state = mode; +} + +let autoTimer=null; +let autoRemaining=0; +const DEFAULT_BUTTON_COUNT=12; +const BUTTON_LABELS=["A","B","X","Y","LB","RB","LT","RT","Back","Start","L3","R3","P13","P14","P15","P16","P17","P18","P19","P20"]; + +function ensureJoyButtons(count){ + const container=document.getElementById('joyButtons'); + if(container.childElementCount === count) return; + container.innerHTML=""; + for(let i=0;i0 ? gp.axes[0] : 0; + const y=gp.axes && gp.axes.length>1 ? gp.axes[1] : 0; + dot.style.left=`${50 + x*45}%`; + dot.style.top=`${50 + y*45}%`; + ensureJoyButtons(gp.buttons.length); + gp.buttons.forEach((btn,i)=>{ + const node=container.children[i]; + if(node) node.classList.toggle('active', !!btn.pressed); + }); + }else{ + ensureJoyButtons(DEFAULT_BUTTON_COUNT); + dot.style.left='50%'; + dot.style.top='50%'; + Array.from(container.children).forEach(node=>node.classList.remove('active')); + } +} + +function updateAutoDisplay(){ + const display=document.getElementById('autoCountdown'); + const fill=document.getElementById('autoProgress'); + const duration=parseFloat(document.getElementById('autoPeriod').value)||0; + display.textContent=`${autoRemaining.toFixed(1)} s`; + const pct = duration>0 ? Math.max(0,Math.min(100,(autoRemaining/duration)*100)) : 0; + fill.style.width=`${pct}%`; + if(autoModeEnabled){ + setPhaseDisplay('auto'); + } +} + +function startAutoTimer(duration){ + autoModeEnabled = true; + autoRemaining = duration; + updateAutoDisplay(); + clearInterval(autoTimer); + autoTimer = setInterval(()=>{ + autoRemaining = Math.max(0, autoRemaining - 0.1); + updateAutoDisplay(); + if(autoRemaining <= 0){ + clearInterval(autoTimer); + autoTimer=null; + autoModeEnabled = false; + setPhaseDisplay('teleop'); + } + }, 100); +} + +function stopAutoTimer(){ + clearInterval(autoTimer); + autoTimer=null; + autoRemaining=0; + autoModeEnabled=false; + updateAutoDisplay(); +} + + async function handleRobotButton(){ + let cmd=""; + const enableAuto=document.getElementById('enableAutonomous').checked; + const autoLen=Math.max(0, parseFloat(document.getElementById('autoPeriod').value) || 0); + + switch(controlState){ + case "idle": cmd="init"; break; + case "armed": cmd="start"; break; + default: cmd="stop"; break; + } + + const url=`/robotControl?cmd=${cmd}&auto=${enableAuto?1:0}&autoLen=${autoLen}`; + try{ + const r=await fetch(url); + if(!r.ok) throw new Error("Robot command failed"); + }catch(err){ + console.error(err); + return; + } + + const btn=document.getElementById('robotButton'); + if(controlState==="idle"){ + controlState="armed"; + btn.textContent="Start"; + btn.style.background="var(--start)"; + btn.style.color="var(--ice)"; + stopAutoTimer(); + autoRemaining = autoLen; + updateAutoDisplay(); + setPhaseDisplay('init'); + }else if(controlState==="armed"){ + controlState="running"; + btn.textContent="Stop"; + btn.style.background="var(--stop)"; + btn.style.color="var(--ice)"; + if(enableAuto && autoLen>0){ + startAutoTimer(autoLen); + setPhaseDisplay('auto'); + }else{ + stopAutoTimer(); + setPhaseDisplay('teleop'); + } + }else{ + controlState="idle"; + btn.textContent="Init"; + btn.style.background="var(--navy)"; + btn.style.color="var(--ice)"; + stopAutoTimer(); + setPhaseDisplay('stopped'); + } + } + document.getElementById('robotButton').addEventListener('click',handleRobotButton); + + function updateGamepads(){ + const gpList=navigator.getGamepads?navigator.getGamepads():[]; + gamepads={}; + for(let i=0;i1){ selectEl.remove(1); } + Object.keys(gamepads).forEach(idx=>{ + const gp=gamepads[idx]; + const option=document.createElement('option'); + option.value=idx; + option.text=`${gp.id} (idx ${gp.index})`; + selectEl.add(option); + }); + if(Object.keys(gamepads).length>0 && selectedGamepadIndex<0){ + const firstIdx=Object.keys(gamepads)[0]; + selectEl.value=firstIdx; + selectedGamepadIndex=parseInt(firstIdx,10); + } + if(Object.keys(gamepads).length===0){ + selectedGamepadIndex=-1; + selectEl.value="-1"; + } + } + function changeSelectedGamepad(){ + const val=document.getElementById('joystickSelect').value; + selectedGamepadIndex=parseInt(val,10); + if(isNaN(selectedGamepadIndex)) selectedGamepadIndex=-1; + } + async function sendGamepadData(gp){ + const data={axes:gp.axes,buttons:gp.buttons.map(b=>b.pressed)}; + try{ + await fetch("/updateController",{ + method:"POST", + headers:{"Content-Type":"application/json"}, + body:JSON.stringify(data) + }); + }catch(err){ + console.error(err); + } + } + function displayJoystickData(gp){ + document.getElementById('joystickStatus').textContent=`${gp.id}\nAxes:${gp.axes.length} Buttons:${gp.buttons.length}`; + document.getElementById('axisData').textContent=gp.axes.map((v,i)=>`Axis ${i}: ${v.toFixed(2)}`).join("\n"); + document.getElementById('buttonData').textContent=gp.buttons.map((b,i)=>`${b.pressed?"●":"○"} ${i}`).join(" "); + document.getElementById('joystickStatusTxt').textContent="Connected"; + updateJoyVisuals(gp); + } + function gamepadLoop(){ + updateGamepads(); + rebuildGamepadSelect(); + const txt=document.getElementById('joystickStatusTxt'); + const hint=document.getElementById('gamepadHint'); + const gp=gamepads[selectedGamepadIndex]; + if(gp){ + txt.textContent="Connected"; + hint.style.display="none"; + gamepadDetected=true; + displayJoystickData(gp); + sendGamepadData(gp); + }else{ + txt.textContent="Not Connected"; + if(!gamepadDetected) hint.style.display="block"; + document.getElementById('joystickStatus').textContent="No gamepad selected."; + document.getElementById('axisData').textContent="No axis data..."; + document.getElementById('buttonData').textContent="No button data..."; + updateJoyVisuals(null); + } + requestAnimationFrame(gamepadLoop); + } + +if(loggingElements.toggleBtn) loggingElements.toggleBtn.addEventListener('click',handleLoggingToggle); +if(loggingElements.refreshBtn) loggingElements.refreshBtn.addEventListener('click',handleLoggingRefresh); +if(loggingElements.clearBtn) loggingElements.clearBtn.addEventListener('click',handleLoggingClear); +if(loggingElements.downloadBtn) loggingElements.downloadBtn.addEventListener('click',handleLoggingDownload); +if(loggingElements.autoRefresh) loggingElements.autoRefresh.addEventListener('change',e=>{ + setLoggingAutoRefresh(e.target.checked); + if(e.target.checked){ + handleLoggingRefresh(); + } +}); +if(loggingElements.autoScroll) loggingElements.autoScroll.addEventListener('change',()=>{ + if(loggingElements.autoScroll.checked && loggingElements.stream){ + loggingElements.stream.scrollTop=loggingElements.stream.scrollHeight; + } +}); + + window.addEventListener('gamepadconnected',()=>{ + updateGamepads(); + rebuildGamepadSelect(); + }); + window.addEventListener('gamepaddisconnected',e=>{ + delete gamepads[e.gamepad.index]; + }); + window.addEventListener('load',()=>{ + ensureJoyButtons(DEFAULT_BUTTON_COUNT); + updateJoyVisuals(null); + autoRemaining=parseFloat(document.getElementById('autoPeriod').value)||0; + updateAutoDisplay(); + setPhaseDisplay('standby'); + applyHeaderMode(); + handleLoggingRefresh(); + if(loggingElements.autoRefresh){ + setLoggingAutoRefresh(loggingElements.autoRefresh.checked); + } + requestAnimationFrame(gamepadLoop); + }); + + document.getElementById('autoPeriod').addEventListener('input',e=>{ + const duration=parseFloat(e.target.value)||0; + if(autoTimer){ + autoRemaining=Math.min(autoRemaining,duration); + } else { + autoRemaining=duration; + } + updateAutoDisplay(); + }); + + document.getElementById('enableAutonomous').addEventListener('change',e=>{ + if(!e.target.checked){ + stopAutoTimer(); + } + }); + -)====="; \ No newline at end of file + +)====="; diff --git a/src/probot.h b/src/probot.h index f73e385..ab396f7 100644 --- a/src/probot.h +++ b/src/probot.h @@ -18,7 +18,6 @@ #include #include #include -#include #include #include diff --git a/src/probot/chassis/basic_tank_drive.hpp b/src/probot/chassis/basic_tank_drive.hpp index ecb7492..093c231 100644 --- a/src/probot/chassis/basic_tank_drive.hpp +++ b/src/probot/chassis/basic_tank_drive.hpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace probot::drive { struct IChassis { @@ -30,6 +32,20 @@ namespace probot::drive { right_->selectDefaultSlot(probot::control::ControlType::kVelocity, velocity_slot_); right_->selectDefaultSlot(probot::control::ControlType::kPosition, position_slot_); } + probot::logging::SourceRegistration reg{ + "tank_drive", + nullptr, + probot::logging::Priority::kBackground, + false, + false, + probot::logging::profiles::tankDriveDynamic, + nullptr + }; + probot::logging::registerSource(this, reg); + } + + ~BasicTankDrive() override { + probot::logging::unregisterSource(this); } void configurePid(const probot::control::PidConfig& velocityCfg, diff --git a/src/probot/chassis/nfr_advanced_mecanum_drive.hpp b/src/probot/chassis/nfr_advanced_mecanum_drive.hpp index 13cd10a..657ef15 100644 --- a/src/probot/chassis/nfr_advanced_mecanum_drive.hpp +++ b/src/probot/chassis/nfr_advanced_mecanum_drive.hpp @@ -53,19 +53,14 @@ namespace probot::chassis { limiterVx_(config.slewRateVx, 0.0f), limiterVy_(config.slewRateVy, 0.0f) { - if (fl_) { fl_->claim(&tokenFL_); applyWheelMotionProfile(fl_); } - if (fr_) { fr_->claim(&tokenFR_); applyWheelMotionProfile(fr_); } - if (rl_) { rl_->claim(&tokenRL_); applyWheelMotionProfile(rl_); } - if (rr_) { rr_->claim(&tokenRR_); applyWheelMotionProfile(rr_); } + applyWheelMotionProfile(fl_); + applyWheelMotionProfile(fr_); + applyWheelMotionProfile(rl_); + applyWheelMotionProfile(rr_); controller_.setEnabled(true); } - ~NfrAdvancedMecanumDrive(){ - if (fl_) fl_->release(&tokenFL_); - if (fr_) fr_->release(&tokenFR_); - if (rl_) rl_->release(&tokenRL_); - if (rr_) rr_->release(&tokenRR_); - } + ~NfrAdvancedMecanumDrive() = default; void setImu(probot::sensors::imu::IImu* imu){ imu_ = imu; } probot::sensors::imu::IImu* imu() const { return imu_; } @@ -206,10 +201,10 @@ namespace probot::chassis { for (float o : outputs) maxMag = std::max(maxMag, std::fabs(o)); float scale = (maxMag > config_.maxOutput) ? config_.maxOutput / maxMag : 1.0f; - if (fl_) fl_->setPower(std::clamp(outputs[0] * scale / config_.maxOutput, -1.0f, 1.0f), &tokenFL_); - if (fr_) fr_->setPower(std::clamp(outputs[1] * scale / config_.maxOutput, -1.0f, 1.0f), &tokenFR_); - if (rl_) rl_->setPower(std::clamp(outputs[2] * scale / config_.maxOutput, -1.0f, 1.0f), &tokenRL_); - if (rr_) rr_->setPower(std::clamp(outputs[3] * scale / config_.maxOutput, -1.0f, 1.0f), &tokenRR_); + if (fl_) fl_->setPower(std::clamp(outputs[0] * scale / config_.maxOutput, -1.0f, 1.0f)); + if (fr_) fr_->setPower(std::clamp(outputs[1] * scale / config_.maxOutput, -1.0f, 1.0f)); + if (rl_) rl_->setPower(std::clamp(outputs[2] * scale / config_.maxOutput, -1.0f, 1.0f)); + if (rr_) rr_->setPower(std::clamp(outputs[3] * scale / config_.maxOutput, -1.0f, 1.0f)); } prevWheelTargets_ = wheelTargets; @@ -232,10 +227,6 @@ namespace probot::chassis { probot::control::IMotorController* fr_; probot::control::IMotorController* rl_; probot::control::IMotorController* rr_; - void* tokenFL_ = reinterpret_cast(reinterpret_cast(this) + 0); - void* tokenFR_ = reinterpret_cast(reinterpret_cast(this) + 1); - void* tokenRL_ = reinterpret_cast(reinterpret_cast(this) + 2); - void* tokenRR_ = reinterpret_cast(reinterpret_cast(this) + 3); Config config_; probot::control::kinematics::MecanumDriveKinematics kinematics_; diff --git a/src/probot/chassis/nfr_advanced_tank_drive.hpp b/src/probot/chassis/nfr_advanced_tank_drive.hpp index 1f70d6d..9921416 100644 --- a/src/probot/chassis/nfr_advanced_tank_drive.hpp +++ b/src/probot/chassis/nfr_advanced_tank_drive.hpp @@ -42,20 +42,11 @@ namespace probot::chassis { pidLeft_(config.velocityPid), pidRight_(config.velocityPid), feedforward_(config.feedforward) { - if (left_) { - left_->claim(&tokenLeft_); - applyWheelMotionProfile(left_); - } - if (right_) { - right_->claim(&tokenRight_); - applyWheelMotionProfile(right_); - } + applyWheelMotionProfile(left_); + applyWheelMotionProfile(right_); } - ~NfrAdvancedTankDrive(){ - if (left_) left_->release(&tokenLeft_); - if (right_) right_->release(&tokenRight_); - } + ~NfrAdvancedTankDrive() = default; void setImu(probot::sensors::imu::IImu* imu){ imu_ = imu; } probot::sensors::imu::IImu* imu() const { return imu_; } @@ -192,8 +183,8 @@ namespace probot::chassis { float leftPower = std::clamp(leftOutput * scale / config_.maxOutput, -1.0f, 1.0f); float rightPower = std::clamp(rightOutput * scale / config_.maxOutput, -1.0f, 1.0f); - if (left_) left_->setPower(leftPower, &tokenLeft_); - if (right_) right_->setPower(rightPower, &tokenRight_); + if (left_) left_->setPower(leftPower); + if (right_) right_->setPower(rightPower); } prevLeftTarget_ = leftTarget; @@ -206,8 +197,6 @@ namespace probot::chassis { private: probot::control::IMotorController* left_; probot::control::IMotorController* right_; - void* tokenLeft_ = this; - void* tokenRight_ = reinterpret_cast(reinterpret_cast(this) + 1); Config config_; probot::control::kinematics::DifferentialDriveKinematics kinematics_; diff --git a/src/probot/chassis/simple_mecanum.hpp b/src/probot/chassis/simple_mecanum.hpp index 85cbc82..33b38fe 100644 --- a/src/probot/chassis/simple_mecanum.hpp +++ b/src/probot/chassis/simple_mecanum.hpp @@ -13,19 +13,7 @@ namespace probot::chassis { probot::motor::IMotorDriver* frontRight, probot::motor::IMotorDriver* rearLeft, probot::motor::IMotorDriver* rearRight) - : fl_(frontLeft), fr_(frontRight), rl_(rearLeft), rr_(rearRight) { - if (fl_) fl_->claim(&tokenFL_); - if (fr_) fr_->claim(&tokenFR_); - if (rl_) rl_->claim(&tokenRL_); - if (rr_) rr_->claim(&tokenRR_); - } - - ~SimpleMecanumDrive(){ - if (fl_) fl_->release(&tokenFL_); - if (fr_) fr_->release(&tokenFR_); - if (rl_) rl_->release(&tokenRL_); - if (rr_) rr_->release(&tokenRR_); - } + : fl_(frontLeft), fr_(frontRight), rl_(rearLeft), rr_(rearRight) {} void setInverted(bool frontLeft, bool frontRight, bool rearLeft, bool rearRight){ if (fl_) fl_->setInverted(frontLeft); @@ -41,10 +29,10 @@ namespace probot::chassis { float rr = vx - vy + omega; float maxMag = std::max({std::fabs(fl), std::fabs(fr), std::fabs(rl), std::fabs(rr), 1.0f}); fl /= maxMag; fr /= maxMag; rl /= maxMag; rr /= maxMag; - if (fl_) fl_->setPower(fl, &tokenFL_); - if (fr_) fr_->setPower(fr, &tokenFR_); - if (rl_) rl_->setPower(rl, &tokenRL_); - if (rr_) rr_->setPower(rr, &tokenRR_); + if (fl_) fl_->setPower(fl); + if (fr_) fr_->setPower(fr); + if (rl_) rl_->setPower(rl); + if (rr_) rr_->setPower(rr); } void stop(){ driveCartesian(0.0f, 0.0f, 0.0f); } @@ -54,9 +42,5 @@ namespace probot::chassis { probot::motor::IMotorDriver* fr_; probot::motor::IMotorDriver* rl_; probot::motor::IMotorDriver* rr_; - void* tokenFL_ = this; - void* tokenFR_ = reinterpret_cast(reinterpret_cast(this) + 1); - void* tokenRL_ = reinterpret_cast(reinterpret_cast(this) + 2); - void* tokenRR_ = reinterpret_cast(reinterpret_cast(this) + 3); }; } diff --git a/src/probot/chassis/simple_tank.hpp b/src/probot/chassis/simple_tank.hpp index aca7900..770459b 100644 --- a/src/probot/chassis/simple_tank.hpp +++ b/src/probot/chassis/simple_tank.hpp @@ -10,15 +10,7 @@ namespace probot::chassis { class SimpleTankDrive { public: SimpleTankDrive(probot::motor::IMotorDriver* left, probot::motor::IMotorDriver* right) - : left_(left), right_(right), ownerLeft_(this), ownerRight_(reinterpret_cast(reinterpret_cast(this)+1)) { - if (left_) left_->claim(ownerLeft_); - if (right_) right_->claim(ownerRight_); - } - - ~SimpleTankDrive(){ - if (left_) left_->release(ownerLeft_); - if (right_) right_->release(ownerRight_); - } + : left_(left), right_(right) {} void setInverted(bool left, bool right){ if (left_) left_->setInverted(left); @@ -28,19 +20,17 @@ namespace probot::chassis { void drive(float leftPower, float rightPower){ leftPower = std::clamp(leftPower, -1.0f, 1.0f); rightPower = std::clamp(rightPower, -1.0f, 1.0f); - if (left_) left_->setPower(leftPower, ownerLeft_); - if (right_) right_->setPower(rightPower, ownerRight_); + if (left_) left_->setPower(leftPower); + if (right_) right_->setPower(rightPower); } void stop(){ - if (left_) left_->setPower(0.0f, ownerLeft_); - if (right_) right_->setPower(0.0f, ownerRight_); + if (left_) left_->setPower(0.0f); + if (right_) right_->setPower(0.0f); } private: probot::motor::IMotorDriver* left_; probot::motor::IMotorDriver* right_; - void* ownerLeft_; - void* ownerRight_; }; } diff --git a/src/probot/control/blink_pid.hpp b/src/probot/control/blink_pid.hpp index 8ed5535..ea145fd 100644 --- a/src/probot/control/blink_pid.hpp +++ b/src/probot/control/blink_pid.hpp @@ -3,12 +3,29 @@ #include #include #include +#include +#include namespace probot::control { class BlinkPid : public ::control::IUpdatable { public: BlinkPid() - : current_reference_(0), pending_reference_(0), has_pending_reference_(false), led_is_on_(false) {} + : current_reference_(0), pending_reference_(0), has_pending_reference_(false), led_is_on_(false) { + probot::logging::SourceRegistration reg{ + "blink_pid", + nullptr, + probot::logging::Priority::kUserMarked, + true, + false, + probot::logging::profiles::blinkPidDynamic, + nullptr + }; + probot::logging::registerSource(this, reg); + } + + ~BlinkPid() override { + probot::logging::unregisterSource(this); + } void setReference(uint32_t new_reference){ __atomic_store_n(&pending_reference_, new_reference, __ATOMIC_SEQ_CST); @@ -27,6 +44,9 @@ namespace probot::control { } } + uint32_t currentReference() const { return current_reference_; } + bool ledState() const { return led_is_on_; } + private: uint32_t current_reference_; uint32_t pending_reference_; diff --git a/src/probot/control/closed_loop_motor.hpp b/src/probot/control/closed_loop_motor.hpp index d5ee386..867387b 100644 --- a/src/probot/control/closed_loop_motor.hpp +++ b/src/probot/control/closed_loop_motor.hpp @@ -12,6 +12,10 @@ #include // #include // Disabled for now #include +#ifndef PROBOT_CLM_NOLOG +#include +#include +#endif namespace probot::control { class ClosedLoopMotor : public IMotorController { @@ -28,20 +32,31 @@ namespace probot::control { active_mode_(ControlType::kVelocity), default_slot_velocity_(0), default_slot_position_(1), - owner_token_(this), - external_owner_(nullptr), inverted_(false) { for (int i=0;i<4;i++){ slot_cfg_[i] = {0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f}; } - if (driver_) driver_->claim(owner_token_); +#ifndef PROBOT_CLM_NOLOG + probot::logging::SourceRegistration reg{ + "motor_controller", + nullptr, + probot::logging::Priority::kUserMarked, + false, + true, + probot::logging::profiles::motorControllerDynamic, + probot::logging::profiles::motorControllerStatic + }; + probot::logging::registerSource(this, reg); +#endif } ~ClosedLoopMotor(){ +#ifndef PROBOT_CLM_NOLOG + probot::logging::unregisterSource(this); +#endif if (driver_) { - driver_->setPower(0.0f, owner_token_); - driver_->release(owner_token_); + driver_->setPower(0.0f); } } @@ -131,29 +146,21 @@ namespace probot::control { bool setPowerDirect(float power){ if (!driver_) return false; - if (external_owner_) return false; // Respect external ownership - return driver_->setPower(power, owner_token_); + bool ok = driver_->setPower(power); + if (ok){ + last_output_ = inverted_ ? -power : power; + } + return ok; } - // IMotor interface (external ownership control). External owner must claim/release. - bool claim(void* owner) override { - if (external_owner_ && external_owner_ != owner) return false; + bool setPower(float power) override { if (!driver_) return false; - external_owner_ = owner; - return true; - } - void release(void* owner) override { - if (!driver_ || external_owner_ != owner) return; - driver_->setPower(0.0f, owner_token_); - external_owner_ = nullptr; - } - bool setPower(float power, void* owner) override { - if (!driver_ || external_owner_ != owner) return false; - float p = inverted_ ? -power : power; - return driver_->setPower(p, owner_token_); + bool ok = driver_->setPower(power); + if (ok){ + last_output_ = inverted_ ? -power : power; + } + return ok; } - bool isClaimed() const override { return external_owner_ != nullptr; } - void* currentOwner() const override { return external_owner_; } void setInverted(bool inverted) override { inverted_ = inverted; @@ -165,18 +172,18 @@ namespace probot::control { if (!encoder_ || !pid_ || !driver_) return; if (timeout_ms_ > 0 && (now_ms - last_ref_ms_.load()) > timeout_ms_){ - driver_->setPower(0.0f, owner_token_); + driver_->setPower(0.0f); last_output_ = 0.0f; return; } if (active_mode_ == ControlType::kPercent){ float target_val = target_value_.load(); - float output = inverted_ ? -target_val : target_val; + float applied = inverted_ ? -target_val : target_val; ref_value_.store(target_val); - driver_->setPower(output, owner_token_); + driver_->setPower(target_val); last_measurement_ = target_val; - last_output_ = output; + last_output_ = applied; return; } @@ -223,9 +230,9 @@ namespace probot::control { float cmd = pid_out + ff; cmd = std::clamp(cmd, slot_cfg_[slot].out_min, slot_cfg_[slot].out_max); - driver_->setPower(cmd, owner_token_); + driver_->setPower(cmd); last_measurement_ = meas; - last_output_ = cmd; + last_output_ = inverted_ ? -cmd : cmd; #ifndef PROBOT_CLM_NOLOG Serial.printf("[CLM ] mode=%s ref=%.3f meas=%.3f err=%.3f out=%.3f slot=%d\n", @@ -383,8 +390,6 @@ namespace probot::control { ControlType profile_mode_ = ControlType::kPercent; std::unique_ptr motion_profile_; - void* owner_token_; - void* external_owner_; bool inverted_; float last_measurement_ = 0.0f; float last_output_ = 0.0f; diff --git a/src/probot/control/closed_loop_motor_group.hpp b/src/probot/control/closed_loop_motor_group.hpp index 1a2894c..1f2e6dd 100644 --- a/src/probot/control/closed_loop_motor_group.hpp +++ b/src/probot/control/closed_loop_motor_group.hpp @@ -1,12 +1,35 @@ #pragma once #include #include +#ifndef PROBOT_CLM_NOLOG +#include +#include +#endif namespace probot::control { class ClosedLoopMotorGroup : public IMotorController { public: ClosedLoopMotorGroup(IMotorController* a, IMotorController* b) - : a_(a), b_(b), owner_(nullptr), inverted_(false) {} + : a_(a), b_(b), inverted_(false) { +#ifndef PROBOT_CLM_NOLOG + probot::logging::SourceRegistration reg{ + "motor_group", + nullptr, + probot::logging::Priority::kUserMarked, + false, + true, + probot::logging::profiles::motorControllerDynamic, + probot::logging::profiles::motorControllerStatic + }; + probot::logging::registerSource(this, reg); +#endif + } + + ~ClosedLoopMotorGroup() override { +#ifndef PROBOT_CLM_NOLOG + probot::logging::unregisterSource(this); +#endif + } // Group control API void setSetpoint(float value, ControlType mode, int slot = -1) override { @@ -62,29 +85,11 @@ namespace probot::control { } // IMotor for raw power when needed - bool claim(void* owner) override { - if (owner_ && owner_ != owner) return false; - if (!a_ || !b_) return false; - if (!a_->claim(owner)) return false; - if (!b_->claim(owner)) { a_->release(owner); return false; } - owner_ = owner; - return true; - } - void release(void* owner) override { - if (owner_ != owner) return; - if (b_) b_->release(owner); - if (a_) a_->release(owner); - owner_ = nullptr; - } - bool setPower(float power, void* owner) override { - if (owner_ != owner) return false; - float p = inverted_ ? -power : power; - bool ok1 = a_ ? a_->setPower(p, owner) : false; - bool ok2 = b_ ? b_->setPower(p, owner) : false; + bool setPower(float power) override { + bool ok1 = a_ ? a_->setPower(power) : false; + bool ok2 = b_ ? b_->setPower(power) : false; return ok1 && ok2; } - bool isClaimed() const override { return owner_ != nullptr; } - void* currentOwner() const override { return owner_; } void setInverted(bool inverted) override { inverted_ = inverted; @@ -96,7 +101,6 @@ namespace probot::control { private: IMotorController* a_; IMotorController* b_; - void* owner_; bool inverted_; }; } // namespace probot::control diff --git a/src/probot/devices/motors/boardoza_ba6208_driver.cpp b/src/probot/devices/motors/boardoza_ba6208_driver.cpp index 383d786..84ab72b 100644 --- a/src/probot/devices/motors/boardoza_ba6208_driver.cpp +++ b/src/probot/devices/motors/boardoza_ba6208_driver.cpp @@ -50,20 +50,7 @@ void BoardozaBA6208Driver::setBrakeMode(bool enabled){ applyStop(); } -bool BoardozaBA6208Driver::claim(void* owner){ - if (owner_ && owner_ != owner) return false; - owner_ = owner; - return true; -} - -void BoardozaBA6208Driver::release(void* owner){ - if (owner_ != owner) return; - owner_ = nullptr; - applyStop(); -} - -bool BoardozaBA6208Driver::setPower(float power, void* owner){ - if (owner_ && owner_ != owner) return false; +bool BoardozaBA6208Driver::setPower(float power){ ensureInitialized(); if (!initialized_) return false; diff --git a/src/probot/devices/motors/boardoza_ba6208_driver.hpp b/src/probot/devices/motors/boardoza_ba6208_driver.hpp index bcb4d76..230c9c4 100644 --- a/src/probot/devices/motors/boardoza_ba6208_driver.hpp +++ b/src/probot/devices/motors/boardoza_ba6208_driver.hpp @@ -18,11 +18,7 @@ class BoardozaBA6208Driver : public IMotorDriver { bool brakeMode() const { return brake_mode_; } // IMotorDriver overrides - bool claim(void* owner) override; - void release(void* owner) override; - bool setPower(float power, void* owner) override; - bool isClaimed() const override { return owner_ != nullptr; } - void* currentOwner() const override { return owner_; } + bool setPower(float power) override; void setInverted(bool inverted) override { inverted_ = inverted; } bool getInverted() const override { return inverted_; } @@ -39,7 +35,6 @@ class BoardozaBA6208Driver : public IMotorDriver { uint32_t pwm_frequency_; uint8_t pwm_resolution_; - void* owner_ = nullptr; bool inverted_ = false; bool initialized_ = false; bool brake_mode_ = true; diff --git a/src/probot/devices/motors/boardoza_vnh_motor_driver.cpp b/src/probot/devices/motors/boardoza_vnh_motor_driver.cpp index dbdb041..67d33c7 100644 --- a/src/probot/devices/motors/boardoza_vnh_motor_driver.cpp +++ b/src/probot/devices/motors/boardoza_vnh_motor_driver.cpp @@ -85,21 +85,7 @@ void BoardozaVNHMotorDriver::setBrakeStrength(float dutyFraction){ } } -bool BoardozaVNHMotorDriver::claim(void* owner){ - if (owner_ && owner_ != owner) return false; - owner_ = owner; - return true; -} - -void BoardozaVNHMotorDriver::release(void* owner){ - if (owner_ != owner) return; - owner_ = nullptr; - applyStop(); -} - -bool BoardozaVNHMotorDriver::setPower(float power, void* owner){ - if (owner_ && owner_ != owner) return false; - +bool BoardozaVNHMotorDriver::setPower(float power){ ensureInitialized(); if (!initialized_) return false; diff --git a/src/probot/devices/motors/boardoza_vnh_motor_driver.hpp b/src/probot/devices/motors/boardoza_vnh_motor_driver.hpp index e5efb48..8d3d995 100644 --- a/src/probot/devices/motors/boardoza_vnh_motor_driver.hpp +++ b/src/probot/devices/motors/boardoza_vnh_motor_driver.hpp @@ -20,11 +20,7 @@ class BoardozaVNHMotorDriver : public IMotorDriver { float brakeStrength() const { return brake_strength_; } // IMotorDriver - bool claim(void* owner) override; - void release(void* owner) override; - bool setPower(float power, void* owner) override; - bool isClaimed() const override { return owner_ != nullptr; } - void* currentOwner() const override { return owner_; } + bool setPower(float power) override; void setInverted(bool inverted) override { inverted_ = inverted; } bool getInverted() const override { return inverted_; } @@ -40,7 +36,6 @@ class BoardozaVNHMotorDriver : public IMotorDriver { int pwm_pin_; int ena_pin_; int enb_pin_; - void* owner_ = nullptr; bool inverted_ = false; bool initialized_ = false; bool brake_mode_ = true; diff --git a/src/probot/devices/motors/imotor_driver.hpp b/src/probot/devices/motors/imotor_driver.hpp index 887529e..4e54e30 100644 --- a/src/probot/devices/motors/imotor_driver.hpp +++ b/src/probot/devices/motors/imotor_driver.hpp @@ -1,13 +1,8 @@ #pragma once -#include namespace probot::motor { struct IMotorDriver { - virtual bool claim(void* owner) = 0; // exclusive write claim - virtual void release(void* owner) = 0; // release claim if owner matches - virtual bool setPower(float power, void* owner) = 0; // -1.0..1.0 normalized output - virtual bool isClaimed() const = 0; - virtual void* currentOwner() const = 0; + virtual bool setPower(float power) = 0; // -1.0..1.0 normalized output // Direction inversion: if inverted, drivers should negate applied power internally virtual void setInverted(bool inverted) = 0; @@ -15,4 +10,4 @@ namespace probot::motor { virtual ~IMotorDriver() {} }; -} // namespace probot::motor +} // namespace probot::motor diff --git a/src/probot/devices/motors/motor_group.hpp b/src/probot/devices/motors/motor_group.hpp index 3e2ef73..7e4cc3e 100644 --- a/src/probot/devices/motors/motor_group.hpp +++ b/src/probot/devices/motors/motor_group.hpp @@ -5,37 +5,15 @@ namespace probot::motor { class MotorGroup : public IMotorDriver { public: MotorGroup(IMotorDriver* a, IMotorDriver* b) - : a_(a), b_(b), owner_(nullptr), inverted_(false) {} + : a_(a), b_(b), inverted_(false) {} - bool claim(void* owner) override { + bool setPower(float power) override { if (!a_ || !b_) return false; - if (owner_) return owner_ == owner; - if (!a_->claim(owner)) return false; - if (!b_->claim(owner)) { a_->release(owner); return false; } - owner_ = owner; - return true; - } - - void release(void* owner) override { - if (!a_ || !b_) return; - if (owner_ != owner) return; - b_->release(owner); - a_->release(owner); - owner_ = nullptr; - } - - bool setPower(float power, void* owner) override { - if (!a_ || !b_) return false; - if (owner_ != owner) return false; - float p = inverted_ ? -power : power; - bool ok1 = a_->setPower(p, owner); - bool ok2 = b_->setPower(p, owner); + bool ok1 = a_->setPower(power); + bool ok2 = b_->setPower(power); return ok1 && ok2; } - bool isClaimed() const override { return owner_ != nullptr; } - void* currentOwner() const override { return owner_; } - void setInverted(bool inverted) override { inverted_ = inverted; if (a_) a_->setInverted(inverted); @@ -46,7 +24,6 @@ namespace probot::motor { private: IMotorDriver* a_; IMotorDriver* b_; - void* owner_; bool inverted_; }; } // namespace probot::motor diff --git a/src/probot/devices/motors/motor_handle.hpp b/src/probot/devices/motors/motor_handle.hpp deleted file mode 100644 index 1443462..0000000 --- a/src/probot/devices/motors/motor_handle.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef PROBOT_DEVICES_MOTORS_MOTOR_HANDLE_HPP -#define PROBOT_DEVICES_MOTORS_MOTOR_HANDLE_HPP -#pragma once -#include - -namespace probot::motor { - -class MotorHandle { -public: - explicit MotorHandle(IMotorDriver& motor) - : _motor(&motor), _owner(this) - { - _motor->claim(_owner); - } - - void setPower(float value){ _motor->setPower(value, _owner); } - void setInverted(bool inv){ _motor->setInverted(inv); } - bool getInverted() const { return _motor->getInverted(); } - - void release(){ _motor->release(_owner); } - - IMotorDriver& underlying() { return *_motor; } - const IMotorDriver& underlying() const { return *_motor; } - -private: - IMotorDriver* _motor; - void* _owner; // unique owner token -}; - -} // namespace probot::motor - -#endif // PROBOT_DEVICES_MOTORS_MOTOR_HANDLE_HPP diff --git a/src/probot/logging/logger.cpp b/src/probot/logging/logger.cpp new file mode 100644 index 0000000..5808f01 --- /dev/null +++ b/src/probot/logging/logger.cpp @@ -0,0 +1,1078 @@ +#include +#include +#include +#include +#include +#include +#if defined(ESP32) +#include +#include +#else +#include +#endif + +namespace probot::logging { + +namespace { + constexpr size_t kMaxSources = 128; + constexpr size_t kMaxWrappers = 128; + constexpr size_t kQueueCapacity = 96; + constexpr size_t kPriorityCount = 3; + constexpr size_t kMaxTypeLen = 24; + constexpr size_t kMaxInstanceLen = 32; + constexpr size_t kMaxFieldLen = 24; + constexpr size_t kMaxTextLen = 64; + constexpr size_t kLogLineCapacity = 160; + +#if defined(ESP32) + static portMUX_TYPE g_log_mux = portMUX_INITIALIZER_UNLOCKED; + struct ScopedLock { + ScopedLock(){ portENTER_CRITICAL(&g_log_mux); } + ~ScopedLock(){ portEXIT_CRITICAL(&g_log_mux); } + }; +#else + static std::mutex g_log_mutex; + struct ScopedLock { + ScopedLock(){ g_log_mutex.lock(); } + ~ScopedLock(){ g_log_mutex.unlock(); } + }; +#endif + + struct SourceRecord { + bool used; + const void* object; + SourceRegistration reg; + char type_buf[kMaxTypeLen]; + char instance_buf[kMaxInstanceLen]; + }; + + struct WrapperRecord { + bool used; + const ::control::IUpdatable* original; + ::control::IUpdatable* wrapper; + }; + + struct LogSample { + const SourceRegistration* reg; + const void* source; + char field[kMaxFieldLen]; + ValueType type; + Priority priority; + int32_t int_value; + float float_value; + bool bool_value; + char text[kMaxTextLen]; + bool has_text; + uint32_t timestamp_ms; + }; + + template + struct RingBuffer { + LogSample data[Capacity]; + size_t head; + size_t tail; + size_t count; + + RingBuffer() : head(0), tail(0), count(0) {} + + bool full() const { return count == Capacity; } + bool empty() const { return count == 0; } + + bool push(const LogSample& sample){ + if (full()) return false; + data[tail] = sample; + tail = (tail + 1) % Capacity; + ++count; + return true; + } + + bool pop(LogSample& out){ + if (empty()) return false; + out = data[head]; + head = (head + 1) % Capacity; + --count; + return true; + } + + bool dropOldest(){ + if (empty()) return false; + head = (head + 1) % Capacity; + --count; + return true; + } + }; + + struct SchedulerTag { + int dummy; + }; + + static SourceRecord g_sources[kMaxSources]; + static WrapperRecord g_wrappers[kMaxWrappers]; + static SchedulerTag g_scheduler_tag{}; + static const SourceRegistration* g_scheduler_reg = nullptr; + + uint16_t budgetForMode(BandwidthMode mode){ + switch (mode){ + case BandwidthMode::kNever: return 0; + case BandwidthMode::kLow: return 2; + case BandwidthMode::kNormal: return 6; + case BandwidthMode::kHigh: return 12; + case BandwidthMode::kFull: return 0xFFFFu; + default: return 0; + } + } + + Priority resolvePriority(Priority override, Priority base){ + return (override == Priority::kUseDefault) ? base : override; + } + + void copyString(char* dest, size_t dest_size, const char* src){ + if (!dest || !dest_size) return; + if (!src){ + dest[0] = '\0'; + return; + } + strncpy(dest, src, dest_size - 1); + dest[dest_size - 1] = '\0'; + } + + SourceRecord* findSourceRecord(const void* object){ + if (!object) return nullptr; + for (size_t i=0;iupdate(now_ms, dt_ms); + LoggingManager& mgr = LoggingManager::instance(); + if (!registration_) return; + TelemetryCollector collector(mgr, *registration_, inner_, now_ms); + collector.addInt("dt_ms", static_cast(dt_ms)); + if (registration_->dynamic_collector){ + registration_->dynamic_collector(inner_, collector, now_ms, dt_ms); + } + } + + ::control::IUpdatable* inner() const { return inner_; } + + private: + ::control::IUpdatable* inner_; + const SourceRegistration* registration_; + }; + + const SourceRegistration* ensureDefaultRegistration(const void* object){ + SourceRecord* rec = findSourceRecord(object); + if (rec) return &rec->reg; + SourceRegistration tmp{}; + tmp.type_name = "updatable"; + tmp.instance_name = nullptr; + tmp.default_priority = Priority::kBackground; + tmp.allow_background_on_wifi = true; + tmp.supports_static_dump = false; + tmp.dynamic_collector = nullptr; + tmp.static_collector = nullptr; + return registerSource(object, tmp); + } + +} // namespace + +struct LoggingManager::Impl { + template + struct PriorityRing : public RingBuffer {}; + + PriorityRing queues[kPriorityCount]; + + struct TransportState { + bool enabled; + BandwidthMode mode; + uint16_t sequence; + uint16_t drop_counter; + uint32_t endpoint_ip_be; + uint16_t endpoint_port; + + TransportState() + : enabled(false), mode(BandwidthMode::kNever), sequence(0), drop_counter(0), + endpoint_ip_be(0), endpoint_port(0) {} + }; + + TransportState serial; + TransportState wifi; + WifiSendFn wifi_writer; + bool wifi_streaming_enabled; + uint32_t wifi_bytes_sent; + uint32_t wifi_stream_start_ms; + uint32_t wifi_last_send_ms; + static constexpr size_t kHttpLogCapacity = 256; + char http_log[kHttpLogCapacity][kLogLineCapacity]; + uint8_t http_log_len[kHttpLogCapacity]; + size_t http_log_head; + size_t http_log_count; + uint32_t http_log_total; + uint32_t last_entry_ms; + + void appendHttpLog(const LogSample& sample); + void copyHttpLog(std::string& out, size_t max_lines) const; + void clearHttpLog(); +}; + +LoggingManager& manager(){ + return LoggingManager::instance(); +} + +LoggingManager::LoggingManager() +: impl_(new Impl()) { + impl_->serial.enabled = true; + impl_->serial.mode = BandwidthMode::kNormal; + impl_->serial.sequence = 0; + impl_->serial.drop_counter = 0; + impl_->serial.endpoint_ip_be = 0; + impl_->serial.endpoint_port = 0; + impl_->wifi.enabled = false; + impl_->wifi.mode = BandwidthMode::kNever; + impl_->wifi.sequence = 0; + impl_->wifi.drop_counter = 0; + impl_->wifi.endpoint_ip_be = 0; + impl_->wifi.endpoint_port = 0; + impl_->wifi_writer = nullptr; + impl_->wifi_streaming_enabled = true; + impl_->wifi_bytes_sent = 0; + impl_->wifi_stream_start_ms = 0; + impl_->wifi_last_send_ms = 0; + impl_->http_log_head = 0; + impl_->http_log_count = 0; + impl_->http_log_total = 0; + impl_->last_entry_ms = 0; + memset(impl_->http_log_len, 0, sizeof(impl_->http_log_len)); + for (size_t i=0;ihttp_log[i][0] = '\0'; + } +} + +LoggingManager::~LoggingManager(){ + delete impl_; + impl_ = nullptr; +} + +LoggingManager& LoggingManager::instance(){ + static LoggingManager g_instance; + return g_instance; +} + +void LoggingManager::enableSerial(bool enabled){ + ScopedLock guard; + impl_->serial.enabled = enabled; +} + +void LoggingManager::setSerialBandwidth(BandwidthMode mode){ + ScopedLock guard; + impl_->serial.mode = mode; +} + +void LoggingManager::enableWifi(bool enabled){ + ScopedLock guard; + impl_->wifi.enabled = enabled; +} + +void LoggingManager::setWifiBandwidth(BandwidthMode mode){ + ScopedLock guard; + impl_->wifi.mode = mode; +} + +void LoggingManager::setWifiSendCallbackInternal(WifiSendFn fn){ + ScopedLock guard; + impl_->wifi_writer = fn; +} + +void LoggingManager::setWifiEndpointInternal(uint32_t ipv4_be, uint16_t port){ + ScopedLock guard; + impl_->wifi.endpoint_ip_be = ipv4_be; + impl_->wifi.endpoint_port = port; +} + +uint32_t LoggingManager::wifiEndpointIpInternal() const{ + return impl_->wifi.endpoint_ip_be; +} + +uint16_t LoggingManager::wifiEndpointPortInternal() const{ + return impl_->wifi.endpoint_port; +} + +void LoggingManager::enqueueValue(const SourceRegistration& reg, + const void* source, + const char* field, + ValueType type, + Priority priority, + int32_t int_value, + float float_value, + bool bool_value, + const char* str_value, + uint32_t timestamp_ms){ + Priority final_priority = resolvePriority(priority, reg.default_priority); + size_t idx = static_cast(final_priority); + if (idx >= kPriorityCount){ + final_priority = reg.default_priority; + idx = static_cast(final_priority); + } + + LogSample sample{}; + sample.reg = ® + sample.source = source; + copyString(sample.field, sizeof(sample.field), field ? field : "value"); + sample.type = type; + sample.priority = final_priority; + sample.int_value = int_value; + sample.float_value = float_value; + sample.bool_value = bool_value; + sample.timestamp_ms = timestamp_ms; + sample.has_text = false; + if (type == ValueType::kString && str_value){ + copyString(sample.text, sizeof(sample.text), str_value); + sample.has_text = true; + } + + auto& queue = impl_->queues[idx]; + if (!queue.push(sample)){ + if (final_priority == Priority::kBackground){ + recordDrop(true, true); + return; + } + if (final_priority == Priority::kUserMarked){ + auto& background = impl_->queues[static_cast(Priority::kBackground)]; + if (!background.dropOldest()){ + if (queue.dropOldest()){ + recordDrop(true, true); + } + } else { + recordDrop(true, true); + } + } else { // system critical + auto& background = impl_->queues[static_cast(Priority::kBackground)]; + if (!background.dropOldest()){ + auto& user = impl_->queues[static_cast(Priority::kUserMarked)]; + if (!user.dropOldest()){ + if (queue.dropOldest()){ + recordDrop(true, true); + } + } else { + recordDrop(true, true); + } + } else { + recordDrop(true, true); + } + } + if (!queue.push(sample)){ + recordDrop(true, true); + } + } +} + +void LoggingManager::publishSystemEvent(const char* origin, + const char* message, + uint32_t timestamp_ms){ + const SourceRegistration* reg = g_scheduler_reg; + if (!reg){ + SourceRegistration tmp{}; + tmp.type_name = origin ? origin : "system"; + tmp.instance_name = "core"; + tmp.default_priority = Priority::kSystemCritical; + tmp.allow_background_on_wifi = true; + tmp.supports_static_dump = false; + tmp.dynamic_collector = nullptr; + tmp.static_collector = nullptr; + g_scheduler_reg = registerSource(&g_scheduler_tag, tmp); + reg = g_scheduler_reg; + } + if (!reg) return; + enqueueValue(*reg, + &g_scheduler_tag, + "event", + ValueType::kString, + Priority::kSystemCritical, + 0, + 0.0f, + false, + message, + timestamp_ms); +} + +void LoggingManager::update(uint32_t now_ms, uint32_t dt_ms){ + (void)now_ms; + (void)dt_ms; + uint16_t serial_budget = impl_->serial.enabled ? budgetForMode(impl_->serial.mode) : 0; + uint16_t wifi_budget = impl_->wifi.enabled ? budgetForMode(impl_->wifi.mode) : 0; + + auto encodeFrame = [&](Impl::TransportState& transport, + const LogSample& sample, + uint8_t* frame, + size_t capacity, + size_t& out_len)->bool { + size_t pos = 0; + bool ok = true; + + auto putByte = [&](uint8_t value){ + if (!ok) return; + if (pos >= capacity){ + ok = false; + return; + } + frame[pos++] = value; + }; + + auto putBytes = [&](const uint8_t* data, size_t len){ + if (!ok) return; + if (pos + len > capacity){ + ok = false; + return; + } + memcpy(frame + pos, data, len); + pos += len; + }; + + auto putText = [&](const char* text){ + if (!ok) return; + size_t len = text ? strlen(text) : 0; + if (len > 255) len = 255; + putByte(static_cast(len)); + if (len > 0 && ok){ + putBytes(reinterpret_cast(text), len); + } + }; + + putByte(0xAA); + putByte(0x55); + size_t len_pos = pos; + putByte(0u); + putByte(0u); + + uint16_t seq = transport.sequence++; + putByte(static_cast((seq >> 8) & 0xFF)); + putByte(static_cast(seq & 0xFF)); + putByte(static_cast(sample.priority)); + putByte(static_cast(sample.type)); + + uint32_t ts = sample.timestamp_ms; + putByte(static_cast((ts >> 24) & 0xFF)); + putByte(static_cast((ts >> 16) & 0xFF)); + putByte(static_cast((ts >> 8) & 0xFF)); + putByte(static_cast(ts & 0xFF)); + + uint16_t drops = transport.drop_counter; + putByte(static_cast((drops >> 8) & 0xFF)); + putByte(static_cast(drops & 0xFF)); + + const char* type_name = (sample.reg && sample.reg->type_name) ? sample.reg->type_name : ""; + const char* instance_name = (sample.reg && sample.reg->instance_name) ? sample.reg->instance_name : ""; + putText(type_name); + putText(instance_name); + putText(sample.field); + + if (!ok){ + return false; + } + + switch (sample.type){ + case ValueType::kInt: { + int32_t val = sample.int_value; + putByte(static_cast((val >> 24) & 0xFF)); + putByte(static_cast((val >> 16) & 0xFF)); + putByte(static_cast((val >> 8) & 0xFF)); + putByte(static_cast(val & 0xFF)); + break; + } + case ValueType::kFloat: { + static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32 bits"); + uint32_t raw = 0; + memcpy(&raw, &sample.float_value, sizeof(float)); + putByte(static_cast((raw >> 24) & 0xFF)); + putByte(static_cast((raw >> 16) & 0xFF)); + putByte(static_cast((raw >> 8) & 0xFF)); + putByte(static_cast(raw & 0xFF)); + break; + } + case ValueType::kBool: { + putByte(sample.bool_value ? 1u : 0u); + break; + } + case ValueType::kString: { + const char* text = sample.has_text ? sample.text : ""; + putText(text); + break; + } + } + + if (!ok){ + return false; + } + + uint16_t payload_len = static_cast(pos - (len_pos + 2)); + frame[len_pos] = static_cast((payload_len >> 8) & 0xFF); + frame[len_pos + 1] = static_cast(payload_len & 0xFF); + + uint8_t crc = 0; + for (size_t i = 2; i < pos; ++i){ + crc ^= frame[i]; + } + putByte(crc); + + if (!ok){ + return false; + } + + out_len = pos; + return true; + }; + + auto sendSerialFrame = [&](const LogSample& sample){ + uint8_t frame[256]; + size_t len = 0; + bool encoded = false; + { + ScopedLock guard; + encoded = encodeFrame(impl_->serial, sample, frame, sizeof(frame), len); + } + if (!encoded){ + recordDrop(true, false); + return; + } + size_t written = Serial.write(frame, len); + if (written != len){ + recordDrop(true, false); + return; + } + { + ScopedLock guard; + impl_->serial.drop_counter = 0; + } + }; + + auto sendWifiFrame = [&](const LogSample& sample){ + uint8_t frame[256]; + size_t len = 0; + WifiSendFn writer = nullptr; + bool encoded = false; + bool streaming = false; + { + ScopedLock guard; + writer = impl_->wifi_writer; + streaming = impl_->wifi_streaming_enabled && impl_->wifi.enabled; + if (writer && streaming){ + encoded = encodeFrame(impl_->wifi, sample, frame, sizeof(frame), len); + } + } + if (!writer || !streaming){ + return; + } + if (!encoded){ + recordDrop(false, true); + return; + } + if (!writer(frame, len)){ + recordDrop(false, true); + return; + } + { + ScopedLock guard; + impl_->wifi.drop_counter = 0; + impl_->wifi_bytes_sent += static_cast(len); + impl_->wifi_last_send_ms = millis(); + if (impl_->wifi_stream_start_ms == 0){ + impl_->wifi_stream_start_ms = impl_->wifi_last_send_ms; + } + } + }; + + while ((serial_budget > 0) || (wifi_budget > 0)){ + LogSample sample{}; + bool found = false; + for (size_t i=0;iqueues[i].pop(sample)){ + found = true; + break; + } + } + if (!found) break; + + if (serial_budget > 0){ + sendSerialFrame(sample); + --serial_budget; + } + + if (wifi_budget > 0){ + if (sample.priority != Priority::kBackground){ + sendWifiFrame(sample); + --wifi_budget; + } + } + } +} + +void LoggingManager::emitStatic(const void* source){ + const SourceRegistration* reg = findSource(source); + if (!reg || !reg->supports_static_dump || !reg->static_collector) return; + TelemetryCollector collector(*this, *reg, source, millis()); + reg->static_collector(source, collector); +} + +void LoggingManager::recordDrop(bool serial, bool wifi){ + ScopedLock guard; + if (serial && impl_->serial.drop_counter < 0xFFFFu){ + impl_->serial.drop_counter += 1; + } + if (wifi && impl_->wifi.drop_counter < 0xFFFFu){ + impl_->wifi.drop_counter += 1; + } +} + +inline void LoggingManager::Impl::appendHttpLog(const LogSample& sample){ + ScopedLock guard; + char* dest = http_log[http_log_head]; + size_t cap = kLogLineCapacity; + size_t pos = 0; + auto appendChar = [&](char c){ if (pos + 1 < cap){ dest[pos++] = c; } }; + auto appendCStr = [&](const char* s){ if (!s) return; while (*s && pos + 1 < cap){ dest[pos++] = *s++; } }; + auto appendUInt = [&](uint32_t v){ char buf[16]; snprintf(buf, sizeof(buf), "%lu", static_cast(v)); appendCStr(buf); }; + auto appendInt = [&](int32_t v){ char buf[16]; snprintf(buf, sizeof(buf), "%ld", static_cast(v)); appendCStr(buf); }; + auto appendFloat = [&](float v){ char buf[20]; snprintf(buf, sizeof(buf), "%.4f", static_cast(v)); appendCStr(buf); }; + + appendChar('['); + appendUInt(sample.timestamp_ms); + appendCStr("] "); + switch (sample.priority){ + case Priority::kSystemCritical: appendCStr("SYS"); break; + case Priority::kUserMarked: appendCStr("USR"); break; + default: appendCStr("BG"); break; + } + appendChar(' '); + if (sample.reg && sample.reg->type_name){ + appendCStr(sample.reg->type_name); + if (sample.reg->instance_name){ + appendChar(':'); + appendCStr(sample.reg->instance_name); + } + } else { + appendCStr("component"); + } + appendChar(' '); + appendCStr(sample.field); + appendChar('='); + switch (sample.type){ + case ValueType::kInt: + appendInt(sample.int_value); + break; + case ValueType::kFloat: + appendFloat(sample.float_value); + break; + case ValueType::kBool: + appendCStr(sample.bool_value ? "true" : "false"); + break; + case ValueType::kString: + if (sample.has_text){ appendCStr(sample.text); } + break; + } + dest[pos] = '\0'; + http_log_len[http_log_head] = static_cast(pos); + http_log_head = (http_log_head + 1) % kHttpLogCapacity; + if (http_log_count < kHttpLogCapacity){ + http_log_count += 1; + } + http_log_total += 1; + last_entry_ms = sample.timestamp_ms; +} + +void LoggingManager::Impl::copyHttpLog(std::string& out, size_t max_lines) const{ + ScopedLock guard; + out.clear(); + if (http_log_count == 0){ + return; + } + size_t lines = http_log_count; + if (max_lines > 0 && lines > max_lines){ + lines = max_lines; + } + size_t start = (http_log_head + kHttpLogCapacity - http_log_count) % kHttpLogCapacity; + size_t idx = start; + for (size_t i = 0; i < lines; ++i){ + out += http_log[idx]; + if (i + 1 < lines) out += '\n'; + idx = (idx + 1) % kHttpLogCapacity; + } +} + +void LoggingManager::Impl::clearHttpLog(){ + ScopedLock guard; + for (size_t i = 0; i < kHttpLogCapacity; ++i){ + http_log[i][0] = '\0'; + http_log_len[i] = 0; + } + http_log_head = 0; + http_log_count = 0; + http_log_total = 0; + last_entry_ms = 0; +} + +void LoggingManager::setWifiStreamingEnabledInternal(bool enabled){ + ScopedLock guard; + impl_->wifi_streaming_enabled = enabled; + if (enabled){ + impl_->wifi_stream_start_ms = millis(); + impl_->wifi_bytes_sent = 0; + impl_->wifi_last_send_ms = 0; + } else { + impl_->wifi_stream_start_ms = 0; + impl_->wifi_last_send_ms = 0; + } +} + +bool LoggingManager::wifiStreamingEnabledInternal() const{ + ScopedLock guard; + return impl_->wifi_streaming_enabled; +} + +void LoggingManager::statusInternal(LoggingStatus& status) const{ + ScopedLock guard; + status.wifi_enabled = impl_->wifi.enabled; + status.wifi_streaming = impl_->wifi_streaming_enabled && impl_->wifi.enabled; + status.serial_drop = impl_->serial.drop_counter; + status.wifi_drop = impl_->wifi.drop_counter; + status.total_entries = impl_->http_log_total; + status.last_entry_ms = impl_->last_entry_ms; + status.wifi_bytes_sent = impl_->wifi_bytes_sent; + status.wifi_stream_start_ms = impl_->wifi_stream_start_ms; + status.wifi_last_send_ms = impl_->wifi_last_send_ms; +} + +void LoggingManager::copyLogLines(std::string& out, size_t max_lines) const{ + impl_->copyHttpLog(out, max_lines); +} + +void LoggingManager::clearLogLines(){ + impl_->clearHttpLog(); +} + +::control::IUpdatable* LoggingManager::wrap(::control::IUpdatable* object){ + if (!object) return nullptr; + WrapperRecord* existing = findWrapperRecord(object); + if (existing){ + return existing->wrapper ? existing->wrapper : object; + } + + const SourceRegistration* reg = findSource(object); + if (!reg){ + reg = ensureDefaultRegistration(object); + } + WrapperRecord* slot = allocateWrapperRecord(object); + if (!slot) return object; + if (reg){ + auto* wrapper = new LoggedUpdatable(object, reg); + slot->wrapper = wrapper; + return wrapper; + } else { + slot->wrapper = nullptr; + return object; + } +} + +::control::IUpdatable* LoggingManager::lookupWrapper(const ::control::IUpdatable* original) const { + WrapperRecord* rec = findWrapperRecord(original); + if (!rec) return nullptr; + return rec->wrapper; +} + +void LoggingManager::releaseWrapper(const ::control::IUpdatable* original){ + WrapperRecord* rec = findWrapperRecord(original); + if (!rec) return; + if (rec->wrapper){ + auto* logged = static_cast(rec->wrapper); + delete logged; + } + rec->used = false; + rec->original = nullptr; + rec->wrapper = nullptr; +} + +void LoggingManager::onSourceRegistered(const void* object, + const SourceRegistration& reg){ + (void)object; + (void)reg; +} + +void LoggingManager::onSourceUnregistered(const void* object){ + WrapperRecord* rec = findWrapperRecord(static_cast(object)); + if (!rec) return; + if (rec->wrapper){ + delete static_cast(rec->wrapper); + } + rec->used = false; + rec->original = nullptr; + rec->wrapper = nullptr; +} + +const SourceRegistration* registerSource(const void* object, + const SourceRegistration& registration){ + if (!object) return nullptr; + SourceRecord* rec = findSourceRecord(object); + if (!rec){ + rec = allocateSourceRecord(object); + } + if (!rec) return nullptr; + + copyString(rec->type_buf, sizeof(rec->type_buf), + registration.type_name ? registration.type_name : "component"); + if (registration.instance_name){ + copyString(rec->instance_buf, sizeof(rec->instance_buf), + registration.instance_name); + } else { + snprintf(rec->instance_buf, + sizeof(rec->instance_buf), + "%s@%p", + rec->type_buf, + object); + } + + rec->reg.type_name = rec->type_buf; + rec->reg.instance_name = rec->instance_buf; + rec->reg.default_priority = registration.default_priority; + rec->reg.allow_background_on_wifi= registration.allow_background_on_wifi; + rec->reg.supports_static_dump = registration.supports_static_dump; + rec->reg.dynamic_collector = registration.dynamic_collector; + rec->reg.static_collector = registration.static_collector; + + LoggingManager::instance().onSourceRegistered(object, rec->reg); + return &rec->reg; +} + +void unregisterSource(const void* object){ + SourceRecord* rec = findSourceRecord(object); + if (!rec) return; + LoggingManager::instance().onSourceUnregistered(object); + rec->used = false; + rec->object = nullptr; +} + +const SourceRegistration* findSource(const void* object){ + SourceRecord* rec = findSourceRecord(object); + return rec ? &rec->reg : nullptr; +} + +TelemetryCollector::TelemetryCollector(LoggingManager& manager, + const SourceRegistration& registration, + const void* source, + uint32_t timestamp_ms) +: manager_(manager), + registration_(registration), + source_(source), + base_priority_(registration.default_priority), + timestamp_ms_(timestamp_ms) {} + +void TelemetryCollector::addInt(const char* field, + int32_t value, + Priority priority_override){ + manager_.enqueueValue(registration_, + source_, + field, + ValueType::kInt, + resolvePriority(priority_override, base_priority_), + value, + 0.0f, + false, + nullptr, + timestamp_ms_); +} + +void TelemetryCollector::addFloat(const char* field, + float value, + Priority priority_override){ + manager_.enqueueValue(registration_, + source_, + field, + ValueType::kFloat, + resolvePriority(priority_override, base_priority_), + 0, + value, + false, + nullptr, + timestamp_ms_); +} + +void TelemetryCollector::addBool(const char* field, + bool value, + Priority priority_override){ + manager_.enqueueValue(registration_, + source_, + field, + ValueType::kBool, + resolvePriority(priority_override, base_priority_), + 0, + 0.0f, + value, + nullptr, + timestamp_ms_); +} + +void TelemetryCollector::addString(const char* field, + const char* value, + Priority priority_override){ + manager_.enqueueValue(registration_, + source_, + field, + ValueType::kString, + resolvePriority(priority_override, base_priority_), + 0, + 0.0f, + false, + value, + timestamp_ms_); +} + +Priority TelemetryCollector::basePriority() const { return base_priority_; } +const SourceRegistration& TelemetryCollector::registration() const { return registration_; } +const void* TelemetryCollector::source() const { return source_; } +uint32_t TelemetryCollector::timestamp() const { return timestamp_ms_; } + +::control::IUpdatable* wrapForScheduler(::control::IUpdatable* object){ + if (!object) return nullptr; + ensureDefaultRegistration(object); + return LoggingManager::instance().wrap(object); +} + +::control::IUpdatable* resolveForDetach(const ::control::IUpdatable* original){ + if (!original) return nullptr; + ::control::IUpdatable* wrapper = LoggingManager::instance().lookupWrapper(original); + return wrapper ? wrapper : const_cast<::control::IUpdatable*>(original); +} + +void releaseForDetach(const ::control::IUpdatable* original){ + if (!original) return; + LoggingManager::instance().releaseWrapper(original); +} + +void notifySchedulerOverrun(uint32_t now_ms, uint32_t overrun_ms){ + if (!g_scheduler_reg){ + SourceRegistration tmp{}; + tmp.type_name = "scheduler"; + tmp.instance_name = "core"; + tmp.default_priority = Priority::kSystemCritical; + tmp.allow_background_on_wifi = true; + tmp.supports_static_dump = false; + tmp.dynamic_collector = nullptr; + tmp.static_collector = nullptr; + g_scheduler_reg = registerSource(&g_scheduler_tag, tmp); + } + if (!g_scheduler_reg) return; + LoggingManager& mgr = LoggingManager::instance(); + TelemetryCollector collector(mgr, *g_scheduler_reg, &g_scheduler_tag, now_ms); + collector.addInt("overrun_ms", static_cast(overrun_ms), Priority::kSystemCritical); +} + +void configureDefaults(){ + LoggingManager& mgr = LoggingManager::instance(); + mgr.enableSerial(true); + mgr.setSerialBandwidth(BandwidthMode::kNormal); + mgr.enableWifi(false); + mgr.setWifiBandwidth(BandwidthMode::kLow); +#ifndef PROBOT_LOGGER_NO_SCHED_ATTACH + control::attach(&mgr); +#endif +} + +void enableSerialLogging(bool enabled){ + LoggingManager::instance().enableSerial(enabled); +} + +void enableWifiLogging(bool enabled){ + LoggingManager::instance().enableWifi(enabled); +} + +void setSerialBandwidthMode(BandwidthMode mode){ + LoggingManager::instance().setSerialBandwidth(mode); +} + +void setWifiBandwidthMode(BandwidthMode mode){ + LoggingManager::instance().setWifiBandwidth(mode); +} + +void setWifiSendCallback(WifiSendFn fn){ + LoggingManager::instance().setWifiSendCallbackInternal(fn); +} + +void setWifiEndpoint(uint32_t ipv4_be, uint16_t port){ + LoggingManager::instance().setWifiEndpointInternal(ipv4_be, port); +} + +void emitStaticSnapshot(const void* source){ + LoggingManager::instance().emitStatic(source); +} + +uint32_t wifiEndpointIp(){ + return LoggingManager::instance().wifiEndpointIpInternal(); +} + +uint16_t wifiEndpointPort(){ + return LoggingManager::instance().wifiEndpointPortInternal(); +} + +void setWifiStreamingEnabled(bool enabled){ + LoggingManager::instance().setWifiStreamingEnabledInternal(enabled); +} + +bool wifiStreamingEnabled(){ + return LoggingManager::instance().wifiStreamingEnabledInternal(); +} + +void loggingStatus(LoggingStatus& status){ + LoggingManager::instance().statusInternal(status); +} + +void loggingStream(std::string& out, size_t max_lines){ + LoggingManager::instance().copyLogLines(out, max_lines); +} + +void clearLoggingStream(){ + LoggingManager::instance().clearLogLines(); +} + +} // namespace probot::logging diff --git a/src/probot/logging/logger.hpp b/src/probot/logging/logger.hpp new file mode 100644 index 0000000..27e1915 --- /dev/null +++ b/src/probot/logging/logger.hpp @@ -0,0 +1,186 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace probot::logging { + + enum class Priority : uint8_t { + kSystemCritical = 0, + kUserMarked = 1, + kBackground = 2, + kUseDefault = 255 + }; + + enum class BandwidthMode : uint8_t { + kNever = 0, + kLow = 1, + kNormal = 2, + kHigh = 3, + kFull = 4 + }; + + enum class ValueType : uint8_t { + kInt, + kFloat, + kBool, + kString + }; + + class TelemetryCollector; + class LoggingManager; + + using WifiSendFn = bool (*)(const uint8_t* data, size_t len); + + using DynamicCollectorFn = void (*)(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + + using StaticCollectorFn = void (*)(const void* object, + TelemetryCollector& collector); + + struct SourceRegistration { + const char* type_name; + const char* instance_name; // optional, can be nullptr + Priority default_priority; + bool allow_background_on_wifi; + bool supports_static_dump; + DynamicCollectorFn dynamic_collector; + StaticCollectorFn static_collector; + }; + + const SourceRegistration* registerSource(const void* object, + const SourceRegistration& registration); + void unregisterSource(const void* object); + const SourceRegistration* findSource(const void* object); + + LoggingManager& manager(); + void enableSerialLogging(bool enabled); + void enableWifiLogging(bool enabled); + void setSerialBandwidthMode(BandwidthMode mode); + void setWifiBandwidthMode(BandwidthMode mode); + void setWifiSendCallback(WifiSendFn fn); + void setWifiEndpoint(uint32_t ipv4_be, uint16_t port); + void emitStaticSnapshot(const void* source); + uint32_t wifiEndpointIp(); + uint16_t wifiEndpointPort(); + void setWifiStreamingEnabled(bool enabled); + bool wifiStreamingEnabled(); + + struct LoggingStatus { + bool wifi_enabled; + bool wifi_streaming; + uint16_t serial_drop; + uint16_t wifi_drop; + uint32_t total_entries; + uint32_t last_entry_ms; + uint32_t wifi_bytes_sent; + uint32_t wifi_stream_start_ms; + uint32_t wifi_last_send_ms; + }; + + void loggingStatus(LoggingStatus& status); + void loggingStream(std::string& out, size_t max_lines = 200); + void clearLoggingStream(); + + class TelemetryCollector { + public: + TelemetryCollector(LoggingManager& manager, + const SourceRegistration& registration, + const void* source, + uint32_t timestamp_ms); + + void addInt(const char* field, + int32_t value, + Priority priority_override = Priority::kUseDefault); + void addFloat(const char* field, + float value, + Priority priority_override = Priority::kUseDefault); + void addBool(const char* field, + bool value, + Priority priority_override = Priority::kUseDefault); + void addString(const char* field, + const char* value, + Priority priority_override = Priority::kUseDefault); + + Priority basePriority() const; + const SourceRegistration& registration() const; + const void* source() const; + uint32_t timestamp() const; + + private: + LoggingManager& manager_; + const SourceRegistration& registration_; + const void* source_; + Priority base_priority_; + uint32_t timestamp_ms_; + }; + + class LoggingManager : public ::control::IUpdatable { + public: + static LoggingManager& instance(); + + void update(uint32_t now_ms, uint32_t dt_ms) override; + + void enableSerial(bool enabled); + void setSerialBandwidth(BandwidthMode mode); + void enableWifi(bool enabled); + void setWifiBandwidth(BandwidthMode mode); + + void enqueueValue(const SourceRegistration& reg, + const void* source, + const char* field, + ValueType type, + Priority priority, + int32_t int_value, + float float_value, + bool bool_value, + const char* str_value, + uint32_t timestamp_ms); + + void publishSystemEvent(const char* origin, + const char* message, + uint32_t timestamp_ms); + + ::control::IUpdatable* wrap(::control::IUpdatable* object); + ::control::IUpdatable* lookupWrapper(const ::control::IUpdatable* original) const; + void releaseWrapper(const ::control::IUpdatable* original); + + void onSourceRegistered(const void* object, + const SourceRegistration& reg); + void onSourceUnregistered(const void* object); + void emitStatic(const void* source); + void recordDrop(bool serial = true, bool wifi = true); + void setWifiSendCallbackInternal(WifiSendFn fn); + void setWifiEndpointInternal(uint32_t ipv4_be, uint16_t port); + uint32_t wifiEndpointIpInternal() const; + uint16_t wifiEndpointPortInternal() const; + void setWifiStreamingEnabledInternal(bool enabled); + bool wifiStreamingEnabledInternal() const; + void statusInternal(struct LoggingStatus& status) const; + void copyLogLines(std::string& out, size_t max_lines) const; + void clearLogLines(); + + private: + LoggingManager(); + ~LoggingManager(); + LoggingManager(const LoggingManager&) = delete; + LoggingManager& operator=(const LoggingManager&) = delete; + + struct Impl; + Impl* impl_; + }; + + ::control::IUpdatable* wrapForScheduler(::control::IUpdatable* object); + ::control::IUpdatable* resolveForDetach(const ::control::IUpdatable* original); + void releaseForDetach(const ::control::IUpdatable* original); + + void notifySchedulerOverrun(uint32_t now_ms, uint32_t overrun_ms); + + void configureDefaults(); + +} // namespace probot::logging diff --git a/src/probot/logging/telemetry_profiles.cpp b/src/probot/logging/telemetry_profiles.cpp new file mode 100644 index 0000000..7abcdca --- /dev/null +++ b/src/probot/logging/telemetry_profiles.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace probot::logging::profiles { + + void motorControllerDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms){ + (void)now_ms; (void)dt_ms; + auto ctrl = static_cast(object); + if (!ctrl) return; + float setpoint = ctrl->lastSetpoint(); + float meas = ctrl->lastMeasurement(); + float output = ctrl->lastOutput(); + collector.addFloat("setpoint", setpoint, Priority::kUserMarked); + collector.addFloat("measurement", meas, Priority::kUserMarked); + collector.addFloat("output", output, Priority::kUserMarked); + collector.addFloat("error", setpoint - meas, Priority::kUserMarked); + collector.addInt("mode", static_cast(ctrl->activeMode()), Priority::kBackground); + collector.addInt("motion_profile", static_cast(ctrl->motionProfile()), Priority::kBackground); + } + + void motorControllerStatic(const void* object, + TelemetryCollector& collector){ + auto ctrl = static_cast(object); + if (!ctrl) return; + auto cfg = ctrl->motionProfileConfig(); + collector.addFloat("mp_max_velocity", cfg.maxVelocity); + collector.addFloat("mp_max_acceleration", cfg.maxAcceleration); + collector.addFloat("mp_max_jerk", cfg.maxJerk); + } + + void sliderDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms){ + (void)now_ms; (void)dt_ms; + auto slider = static_cast(object); + if (!slider) return; + float target = slider->getTargetLength(); + float current = slider->getCurrentLength(); + collector.addFloat("target_length", target); + collector.addFloat("current_length", current); + collector.addFloat("error_length", current - target); + } + + void armDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms){ + (void)now_ms; (void)dt_ms; + auto arm = static_cast(object); + if (!arm) return; + float target = arm->getTargetAngleDeg(); + float current = arm->getCurrentAngleDeg(); + collector.addFloat("target_angle_deg", target); + collector.addFloat("current_angle_deg", current); + collector.addFloat("error_angle_deg", current - target); + } + + void elevatorDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms){ + (void)now_ms; (void)dt_ms; + auto elevator = static_cast(object); + if (!elevator) return; + float target = elevator->getTargetHeight(); + float current = elevator->getCurrentHeight(); + collector.addFloat("target_height", target); + collector.addFloat("current_height", current); + collector.addFloat("error_height", current - target); + } + + void telescopicDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms){ + (void)now_ms; (void)dt_ms; + auto tube = static_cast(object); + if (!tube) return; + float target = tube->getTargetExtension(); + float current = tube->getCurrentExtension(); + collector.addFloat("target_extension", target); + collector.addFloat("current_extension", current); + collector.addFloat("error_extension", current - target); + } + + void turretDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms){ + (void)now_ms; (void)dt_ms; + auto turret = static_cast(object); + if (!turret) return; + float target = turret->getTargetAngleDeg(); + float current = turret->getCurrentAngleDeg(); + collector.addFloat("target_angle_deg", target); + collector.addFloat("current_angle_deg", current); + collector.addFloat("error_angle_deg", current - target); + } + + void tankDriveDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms){ + (void)now_ms; (void)dt_ms; + auto drive = static_cast(object); + if (!drive) return; + collector.addBool("position_mode", drive->positionActive()); + collector.addBool("position_goal_reached", + drive->positionGoalReached(0.5f)); + } + + void blinkPidDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms){ + (void)now_ms; (void)dt_ms; + auto pid = static_cast(object); + if (!pid) return; + collector.addInt("reference", static_cast(pid->currentReference()), Priority::kUserMarked); + collector.addBool("led_state", pid->ledState()); + } + +} // namespace probot::logging::profiles diff --git a/src/probot/logging/telemetry_profiles.hpp b/src/probot/logging/telemetry_profiles.hpp new file mode 100644 index 0000000..5c30a46 --- /dev/null +++ b/src/probot/logging/telemetry_profiles.hpp @@ -0,0 +1,50 @@ +#pragma once +#include +#include + +namespace probot::logging::profiles { + + void motorControllerDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + + void motorControllerStatic(const void* object, + TelemetryCollector& collector); + + void sliderDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + + void armDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + + void elevatorDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + + void telescopicDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + + void turretDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + + void tankDriveDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + + void blinkPidDynamic(const void* object, + TelemetryCollector& collector, + uint32_t now_ms, + uint32_t dt_ms); + +} // namespace probot::logging::profiles diff --git a/src/probot/logging/wifi_transport_esp32.cpp b/src/probot/logging/wifi_transport_esp32.cpp new file mode 100644 index 0000000..dfcac14 --- /dev/null +++ b/src/probot/logging/wifi_transport_esp32.cpp @@ -0,0 +1,54 @@ +#ifdef ESP32 +#include +#include +#include +#include + +namespace probot::logging { +namespace { + +const uint16_t kDefaultWifiLoggingPort = 49160; +WiFiUDP s_udp; + +bool sendUdp(const uint8_t* data, size_t len){ + if (!data || len == 0) return false; + uint16_t port = wifiEndpointPort(); + if (port == 0) port = kDefaultWifiLoggingPort; + uint32_t ip_be = wifiEndpointIp(); + IPAddress target; + if (ip_be == 0){ + target = IPAddress(255, 255, 255, 255); + } else { + target = IPAddress((ip_be >> 24) & 0xFF, + (ip_be >> 16) & 0xFF, + (ip_be >> 8) & 0xFF, + ip_be & 0xFF); + } + + // Bind local port if needed (ignore result to avoid repeated errors) + s_udp.begin(port); + + if (!s_udp.beginPacket(target, port)){ + return false; + } + size_t written = s_udp.write(data, len); + if (written != len){ + s_udp.endPacket(); + return false; + } + return s_udp.endPacket() == 1; +} + +struct WifiLoggingInitializer { + WifiLoggingInitializer(){ + setWifiEndpoint(0xFFFFFFFFu, kDefaultWifiLoggingPort); + setWifiSendCallback(sendUdp); + } +}; + +static WifiLoggingInitializer g_wifi_logging_initializer; + +} // namespace +} // namespace probot::logging + +#endif // ESP32 diff --git a/src/probot/mechanism/arm.hpp b/src/probot/mechanism/arm.hpp index 654d36a..85b9dcb 100644 --- a/src/probot/mechanism/arm.hpp +++ b/src/probot/mechanism/arm.hpp @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include namespace probot::mechanism { @@ -18,7 +20,22 @@ namespace probot::mechanism { public: explicit Arm(probot::control::IMotorController* controller) : controller_(controller), ticks_per_degree_(1.0f), target_angle_(0.0f), - min_angle_(-90.0f), max_angle_(90.0f), has_limits_(false) {} + min_angle_(-90.0f), max_angle_(90.0f), has_limits_(false) { + probot::logging::SourceRegistration reg{ + "arm", + nullptr, + probot::logging::Priority::kBackground, + false, + false, + probot::logging::profiles::armDynamic, + nullptr + }; + probot::logging::registerSource(this, reg); + } + + ~Arm() override { + probot::logging::unregisterSource(this); + } void setTargetAngleDeg(float degrees) override { if (has_limits_){ diff --git a/src/probot/mechanism/elevator.hpp b/src/probot/mechanism/elevator.hpp index 8c4b4aa..fd51adb 100644 --- a/src/probot/mechanism/elevator.hpp +++ b/src/probot/mechanism/elevator.hpp @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include namespace probot::mechanism { @@ -18,7 +20,22 @@ namespace probot::mechanism { public: explicit Elevator(probot::control::IMotorController* controller) : controller_(controller), ticks_per_unit_(1.0f), target_height_(0.0f), - min_height_(0.0f), max_height_(0.0f), has_limits_(false) {} + min_height_(0.0f), max_height_(0.0f), has_limits_(false) { + probot::logging::SourceRegistration reg{ + "elevator", + nullptr, + probot::logging::Priority::kBackground, + false, + false, + probot::logging::profiles::elevatorDynamic, + nullptr + }; + probot::logging::registerSource(this, reg); + } + + ~Elevator() override { + probot::logging::unregisterSource(this); + } void setTargetHeight(float units) override { if (has_limits_){ diff --git a/src/probot/mechanism/slider.hpp b/src/probot/mechanism/slider.hpp index e52ae9c..b1e38c1 100644 --- a/src/probot/mechanism/slider.hpp +++ b/src/probot/mechanism/slider.hpp @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include namespace probot::mechanism { struct ISlider { @@ -18,7 +20,22 @@ namespace probot::mechanism { public: explicit Slider(probot::control::IMotorController* controller) : controller_(controller), ticks_per_unit_(1.0f), target_len_(0.0f), - min_len_(0.0f), max_len_(0.0f), has_limits_(false) {} + min_len_(0.0f), max_len_(0.0f), has_limits_(false) { + probot::logging::SourceRegistration reg{ + "slider", + nullptr, + probot::logging::Priority::kBackground, + false, + false, + probot::logging::profiles::sliderDynamic, + nullptr + }; + probot::logging::registerSource(this, reg); + } + + ~Slider() override { + probot::logging::unregisterSource(this); + } void setTargetLength(float length_units) override { if (has_limits_){ diff --git a/src/probot/mechanism/telescopic_tube.hpp b/src/probot/mechanism/telescopic_tube.hpp index 1b06fa2..cecbb04 100644 --- a/src/probot/mechanism/telescopic_tube.hpp +++ b/src/probot/mechanism/telescopic_tube.hpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace probot::mechanism { @@ -32,12 +34,25 @@ namespace probot::mechanism { profile_dirty_(true), slew_rate_units_per_s_(0.0f), slew_limiter_(0.0f, 0.0f), - limiter_initialized_(false) - { + limiter_initialized_(false) { if (controller_) { controller_->selectDefaultSlot(probot::control::ControlType::kPosition, position_slot_); controller_->setPidSlotConfig(position_slot_, pid_config_); } + probot::logging::SourceRegistration reg{ + "telescopic_tube", + nullptr, + probot::logging::Priority::kBackground, + false, + false, + probot::logging::profiles::telescopicDynamic, + nullptr + }; + probot::logging::registerSource(this, reg); + } + + ~TelescopicTube() override { + probot::logging::unregisterSource(this); } void setTargetExtension(float units) override { diff --git a/src/probot/mechanism/turret.hpp b/src/probot/mechanism/turret.hpp index c881a93..1a8bfe2 100644 --- a/src/probot/mechanism/turret.hpp +++ b/src/probot/mechanism/turret.hpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace probot::mechanism { @@ -32,12 +34,25 @@ namespace probot::mechanism { profile_dirty_(true), slew_rate_deg_per_s_(0.0f), slew_limiter_(0.0f, 0.0f), - limiter_initialized_(false) - { + limiter_initialized_(false) { if (controller_) { controller_->selectDefaultSlot(probot::control::ControlType::kPosition, position_slot_); controller_->setPidSlotConfig(position_slot_, pid_config_); } + probot::logging::SourceRegistration reg{ + "turret", + nullptr, + probot::logging::Priority::kBackground, + false, + false, + probot::logging::profiles::turretDynamic, + nullptr + }; + probot::logging::registerSource(this, reg); + } + + ~Turret() override { + probot::logging::unregisterSource(this); } void setTargetAngleDeg(float degrees) override { diff --git a/src/probot/test/test_motor.hpp b/src/probot/test/test_motor.hpp index 9af3828..7d06ad7 100644 --- a/src/probot/test/test_motor.hpp +++ b/src/probot/test/test_motor.hpp @@ -4,29 +4,23 @@ namespace probot::test { class TestMotor : public probot::motor::IMotorDriver { public: - bool claim(void* owner) override { - if (owner_ == nullptr || owner_ == owner){ owner_ = owner; return true; } - return false; - } - void release(void* owner) override { if (owner_ == owner) owner_ = nullptr; } - bool setPower(float power, void* owner) override { - if (owner_ != owner) return false; + bool setPower(float power) override { if (power < -1.0f) power = -1.0f; else if (power > 1.0f) power = 1.0f; - last_cmd_ = power; + commanded_ = power; + last_cmd_ = inverted_ ? -power : power; return true; } - bool isClaimed() const override { return owner_ != nullptr; } - void* currentOwner() const override { return owner_; } void setInverted(bool inverted) override { inverted_ = inverted; } bool getInverted() const override { return inverted_; } - float appliedPower() const { return inverted_ ? -last_cmd_ : last_cmd_; } + float appliedPower() const { return last_cmd_; } + float commandedPower() const { return commanded_; } private: - void* owner_ = nullptr; bool inverted_= false; float last_cmd_= 0.0f; + float commanded_= 0.0f; }; } // namespace probot::test diff --git a/tests/stubs/Arduino.h b/tests/stubs/Arduino.h index dc7756b..318d60c 100644 --- a/tests/stubs/Arduino.h +++ b/tests/stubs/Arduino.h @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include #define HIGH 1 #define LOW 0 @@ -32,13 +34,39 @@ inline void analogWriteResolution(int, int) {} inline void analogWriteFrequency(int, int) {} struct __SerialStub { + std::vector buffer; void begin(unsigned long) {} void println(const char*) {} void println(int) {} void print(const char*) {} void printf(const char*, ...) {} + size_t write(uint8_t value){ + buffer.push_back(value); + return 1; + } + size_t write(const uint8_t* data, size_t len){ + if (data && len){ + buffer.insert(buffer.end(), data, data + len); + } + return len; + } + void clear(){ buffer.clear(); } } __attribute__((unused)); -static __SerialStub Serial; +inline __SerialStub Serial; + +inline void __serialStubClear(){ Serial.clear(); } +inline const std::vector& __serialStubBuffer(){ return Serial.buffer; } + +inline std::vector& __wifiStubBuffer(){ static std::vector buf; return buf; } +inline void __wifiStubClear(){ __wifiStubBuffer().clear(); } +inline bool __wifiStubSend(const uint8_t* data, size_t len){ + auto& buf = __wifiStubBuffer(); + buf.assign(data, data + len); + return true; +} +inline bool __wifiStubSendFail(const uint8_t*, size_t){ + return false; +} inline uint32_t micros(){ return millis() * 1000u; } diff --git a/tests/test_chassis.cpp b/tests/test_chassis.cpp index fa8c02d..d85e6a2 100644 --- a/tests/test_chassis.cpp +++ b/tests/test_chassis.cpp @@ -13,14 +13,9 @@ namespace { struct DummyMotor : probot::motor::IMotorDriver { - void* owner = nullptr; float lastPower = 0.0f; bool inverted = false; - bool claim(void* o) override { if (owner && owner != o) return false; owner = o; return true; } - void release(void* o) override { if (owner == o){ owner = nullptr; lastPower = 0.0f; } } - bool setPower(float power, void* o) override { if (owner != o) return false; lastPower = inverted ? -power : power; return true; } - bool isClaimed() const override { return owner != nullptr; } - void* currentOwner() const override { return owner; } + bool setPower(float power) override { lastPower = inverted ? -power : power; return true; } void setInverted(bool inv) override { inverted = inv; } bool getInverted() const override { return inverted; } }; @@ -33,7 +28,6 @@ namespace { }; struct MockMotorController : probot::control::IMotorController { - void* owner = nullptr; float lastSetpointValue = 0.0f; probot::control::ControlType lastMode = probot::control::ControlType::kPercent; int lastSlot = -1; @@ -43,20 +37,11 @@ namespace { bool setPowerCalled = false; float lastPower = 0.0f; - bool claim(void* o) override { - if (owner && owner != o) return false; - owner = o; - return true; - } - void release(void* o) override { if (owner == o) owner = nullptr; } - bool setPower(float power, void* o) override { - if (owner != o) return false; + bool setPower(float power) override { setPowerCalled = true; lastPower = inverted ? -power : power; return true; } - bool isClaimed() const override { return owner != nullptr; } - void* currentOwner() const override { return owner; } void setInverted(bool inv) override { inverted = inv; } bool getInverted() const override { return inverted; } @@ -150,8 +135,6 @@ TEST_CASE(nfr_tank_drive_closed_loop_should_command_power){ probot::control::ClosedLoopMotor clR(&encR, &pidR, &motorR, 1.0f, 1.0f); clL.setTimeoutMs(0); clR.setTimeoutMs(0); - EXPECT_TRUE(motorL.isClaimed()); - EXPECT_TRUE(motorR.isClaimed()); probot::chassis::NfrAdvancedTankDrive chassis(&clL, &clR); chassis.resetPose(probot::control::Pose2d(), 0.0f, 0.0f); @@ -177,10 +160,6 @@ TEST_CASE(nfr_mecanum_drive_closed_loop_should_command_power){ clFR.setTimeoutMs(0); clRL.setTimeoutMs(0); clRR.setTimeoutMs(0); - EXPECT_TRUE(motorFL.isClaimed()); - EXPECT_TRUE(motorFR.isClaimed()); - EXPECT_TRUE(motorRL.isClaimed()); - EXPECT_TRUE(motorRR.isClaimed()); probot::chassis::NfrAdvancedMecanumDrive chassis(&clFL, &clFR, &clRL, &clRR); probot::control::kinematics::WheelPositions4 wheels{0.0f, 0.0f, 0.0f, 0.0f}; diff --git a/tests/test_closed_loop.cpp b/tests/test_closed_loop.cpp index ffa8db6..96cb36a 100644 --- a/tests/test_closed_loop.cpp +++ b/tests/test_closed_loop.cpp @@ -19,25 +19,13 @@ namespace { }; struct MotorStub : probot::motor::IMotorDriver { - void* owner = nullptr; float lastPower = 0.0f; bool inverted = false; - bool claim(void* o) override { - if (owner && owner != o) return false; - owner = o; - return true; - } - void release(void* o) override { - if (owner == o){ owner = nullptr; lastPower = 0.0f; } - } - bool setPower(float power, void* o) override { - if (owner != o) return false; + bool setPower(float power) override { lastPower = inverted ? -power : power; return true; } - bool isClaimed() const override { return owner != nullptr; } - void* currentOwner() const override { return owner; } void setInverted(bool inv) override { inverted = inv; } bool getInverted() const override { return inverted; } }; @@ -50,24 +38,14 @@ namespace { float output = 0.0f; float lastCommand = 0.0f; bool inverted = false; - void* owner = nullptr; probot::control::MotionProfileType profileType = probot::control::MotionProfileType::kNone; probot::control::MotionProfileConfig profileCfg{}; - bool claim(void* o) override { - if (owner && owner != o) return false; - owner = o; - return true; - } - void release(void* o) override { if (owner == o) owner = nullptr; } - bool setPower(float power, void* o) override { - if (owner != o) return false; + bool setPower(float power) override { lastCommand = power; output = inverted ? -power : power; return true; } - bool isClaimed() const override { return owner != nullptr; } - void* currentOwner() const override { return owner; } void setInverted(bool inv) override { inverted = inv; } bool getInverted() const override { return inverted; } @@ -135,19 +113,16 @@ TEST_CASE(closed_loop_motor_group_broadcast){ EXPECT_TRUE(a.mode == probot::control::ControlType::kPosition); EXPECT_TRUE(b.mode == probot::control::ControlType::kPosition); - float token = 0.0f; - EXPECT_TRUE(group.claim(&token)); - EXPECT_TRUE(group.setPower(0.5f, &token)); + EXPECT_TRUE(group.setPower(0.5f)); EXPECT_NEAR(a.output, 0.5f, 1e-5f); EXPECT_NEAR(b.output, 0.5f, 1e-5f); + EXPECT_NEAR(a.lastCommand, 0.5f, 1e-5f); group.setInverted(true); EXPECT_TRUE(group.getInverted()); - EXPECT_TRUE(group.setPower(0.5f, &token)); - EXPECT_NEAR(a.lastCommand, -0.5f, 1e-5f); - - group.release(&token); - EXPECT_TRUE(!a.isClaimed()); + EXPECT_TRUE(group.setPower(0.5f)); + EXPECT_NEAR(a.output, -0.5f, 1e-5f); + EXPECT_NEAR(a.lastCommand, 0.5f, 1e-5f); } TEST_CASE(closed_loop_motor_percent_and_timeout){ @@ -174,24 +149,6 @@ TEST_CASE(closed_loop_motor_percent_and_timeout){ EXPECT_NEAR(driver.lastPower, 0.0f, 1e-5f); } -TEST_CASE(closed_loop_motor_external_claim_should_allow_control){ - EncoderStub encoder; - MotorStub driver; - auto cfg = makePid(0.2f); - probot::control::PID pid(cfg); - probot::control::ClosedLoopMotor controller(&encoder, &pid, &driver, 1.0f, 0.001f); - controller.setTimeoutMs(0); - controller.setPidSlotConfig(0, cfg); - EXPECT_TRUE(driver.owner != nullptr); - - float externalToken = 1.0f; - EXPECT_TRUE(controller.claim(&externalToken)); - EXPECT_TRUE(controller.setPower(0.3f, &externalToken)); - EXPECT_NEAR(driver.lastPower, 0.3f, 1e-5f); - controller.release(&externalToken); - EXPECT_TRUE(!controller.isClaimed()); -} - TEST_CASE(closed_loop_motor_trapezoid_profile_ramps){ EncoderStub encoder; MotorStub driver; diff --git a/tests/test_devices.cpp b/tests/test_devices.cpp index 4df611e..6ea565e 100644 --- a/tests/test_devices.cpp +++ b/tests/test_devices.cpp @@ -1,7 +1,6 @@ #include "test_harness.hpp" #include -#include #include #include @@ -9,47 +8,36 @@ namespace { struct MotorStub : probot::motor::IMotorDriver { - void* owner = nullptr; float lastPower = 0.0f; float lastCommand = 0.0f; bool inverted = false; - bool claim(void* o) override { if (owner && owner != o) return false; owner = o; return true; } - void release(void* o) override { if (owner == o){ owner = nullptr; lastPower = 0.0f; } } - bool setPower(float power, void* o) override { if (owner != o) return false; lastCommand = power; lastPower = inverted ? -power : power; return true; } - bool isClaimed() const override { return owner != nullptr; } - void* currentOwner() const override { return owner; } + bool setPower(float power) override { lastCommand = power; lastPower = inverted ? -power : power; return true; } void setInverted(bool inv) override { inverted = inv; } bool getInverted() const override { return inverted; } }; } -TEST_CASE(motor_group_claim_and_invert){ +TEST_CASE(motor_group_power_and_invert){ MotorStub a, b; probot::motor::MotorGroup group(&a, &b); - int token = 42; - EXPECT_TRUE(group.claim(&token)); - EXPECT_TRUE(group.setPower(0.3f, &token)); + EXPECT_TRUE(group.setPower(0.3f)); EXPECT_NEAR(a.lastPower, 0.3f, 1e-5f); EXPECT_NEAR(b.lastPower, 0.3f, 1e-5f); group.setInverted(true); - EXPECT_TRUE(group.setPower(0.4f, &token)); - EXPECT_NEAR(a.lastCommand, -0.4f, 1e-5f); - group.release(&token); - EXPECT_TRUE(!a.isClaimed()); + EXPECT_TRUE(group.setPower(0.4f)); + EXPECT_NEAR(a.lastPower, -0.4f, 1e-5f); + EXPECT_NEAR(a.lastCommand, 0.4f, 1e-5f); } -TEST_CASE(motor_handle_claims_motor){ +TEST_CASE(null_motor_behaves_like_driver){ probot::motor::NullMotor motor; - { - probot::motor::MotorHandle handle(motor); - handle.setPower(0.5f); - EXPECT_TRUE(motor.isClaimed()); - handle.setInverted(true); - EXPECT_TRUE(handle.getInverted()); - handle.release(); - } - EXPECT_TRUE(!motor.isClaimed()); + EXPECT_TRUE(motor.setPower(0.5f)); + EXPECT_NEAR(motor.appliedPower(), 0.5f, 1e-5f); + motor.setInverted(true); + EXPECT_TRUE(motor.getInverted()); + EXPECT_TRUE(motor.setPower(0.2f)); + EXPECT_NEAR(motor.appliedPower(), -0.2f, 1e-5f); } static_assert(std::is_base_of::value, diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp new file mode 100644 index 0000000..54d411d --- /dev/null +++ b/tests/test_logging.cpp @@ -0,0 +1,310 @@ +#include "test_harness.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace { + +struct ParsedFrame { + uint16_t sequence{}; + probot::logging::Priority priority{}; + probot::logging::ValueType value_type{}; + uint32_t timestamp{}; + uint16_t drop_count{}; + std::string type_name; + std::string instance_name; + std::string field; + int32_t int_value{}; + float float_value{}; + bool bool_value{}; + std::string text_value; +}; + +bool parseFrame(const std::vector& buffer, size_t offset, ParsedFrame& out){ + if (offset + 5 > buffer.size()) return false; + if (buffer[offset] != 0xAA || buffer[offset + 1] != 0x55) return false; + uint16_t payload_len = static_cast((buffer[offset + 2] << 8) | buffer[offset + 3]); + size_t frame_size = static_cast(payload_len) + 5; // sync(2) + len(2) + crc(1) + if (offset + frame_size > buffer.size()) return false; + + size_t idx = offset + 4; + + out.sequence = static_cast((buffer[idx] << 8) | buffer[idx + 1]); idx += 2; + out.priority = static_cast(buffer[idx++]); + out.value_type = static_cast(buffer[idx++]); + + out.timestamp = (static_cast(buffer[idx]) << 24) | + (static_cast(buffer[idx + 1]) << 16) | + (static_cast(buffer[idx + 2]) << 8) | + static_cast(buffer[idx + 3]); + idx += 4; + + out.drop_count = static_cast((buffer[idx] << 8) | buffer[idx + 1]); + idx += 2; + + auto readText = [&](std::string& dest)->bool{ + if (idx >= offset + frame_size - 1) return false; + uint8_t len = buffer[idx++]; + if (idx + len > offset + frame_size - 1) return false; + dest.assign(reinterpret_cast(&buffer[idx]), len); + idx += len; + return true; + }; + + if (!readText(out.type_name)) return false; + if (!readText(out.instance_name)) return false; + if (!readText(out.field)) return false; + + switch (out.value_type){ + case probot::logging::ValueType::kInt: { + if (idx + 4 > offset + frame_size - 1) return false; + out.int_value = (static_cast(buffer[idx]) << 24) | + (static_cast(buffer[idx + 1]) << 16) | + (static_cast(buffer[idx + 2]) << 8) | + static_cast(buffer[idx + 3]); + idx += 4; + break; + } + case probot::logging::ValueType::kFloat: { + if (idx + 4 > offset + frame_size - 1) return false; + uint32_t raw = (static_cast(buffer[idx]) << 24) | + (static_cast(buffer[idx + 1]) << 16) | + (static_cast(buffer[idx + 2]) << 8) | + static_cast(buffer[idx + 3]); + idx += 4; + std::memcpy(&out.float_value, &raw, sizeof(float)); + break; + } + case probot::logging::ValueType::kBool: { + if (idx + 1 > offset + frame_size - 1) return false; + out.bool_value = buffer[idx++] != 0; + break; + } + case probot::logging::ValueType::kString: { + if (!readText(out.text_value)) return false; + break; + } + } + + uint8_t crc_expected = 0; + for (size_t i = 2; i < frame_size - 1; ++i) { + crc_expected ^= buffer[offset + i]; + } + uint8_t crc_read = buffer[offset + frame_size - 1]; + if (crc_expected != crc_read) return false; + + if (idx != offset + frame_size - 1) return false; + return true; +} + +struct DummySource {}; + +} // namespace + +TEST_CASE(logging_serial_frame_basic){ + __serialStubClear(); + __wifiStubClear(); + auto& mgr = probot::logging::manager(); + probot::logging::enableSerialLogging(true); + probot::logging::setSerialBandwidthMode(probot::logging::BandwidthMode::kFull); + probot::logging::enableWifiLogging(false); + probot::logging::setWifiSendCallback(nullptr); + + DummySource src; + probot::logging::SourceRegistration reg{}; + reg.type_name = "test"; + reg.instance_name = "unit"; + reg.default_priority = probot::logging::Priority::kUserMarked; + reg.allow_background_on_wifi = false; + reg.supports_static_dump = false; + reg.dynamic_collector = nullptr; + reg.static_collector = nullptr; + const auto* stored = probot::logging::registerSource(&src, reg); + + mgr.enqueueValue(*stored, + &src, + "value", + probot::logging::ValueType::kInt, + probot::logging::Priority::kUseDefault, + 123, + 0.0f, + false, + nullptr, + 1234); + mgr.update(1234, 20); + + const auto& buffer = __serialStubBuffer(); + EXPECT_TRUE(!buffer.empty()); + + ParsedFrame frame{}; + EXPECT_TRUE(parseFrame(buffer, 0, frame)); + EXPECT_TRUE(frame.priority == probot::logging::Priority::kUserMarked); + EXPECT_TRUE(frame.value_type == probot::logging::ValueType::kInt); + EXPECT_TRUE(frame.timestamp == 1234); + EXPECT_TRUE(frame.drop_count == 0); + EXPECT_TRUE(frame.int_value == 123); + EXPECT_TRUE(frame.type_name == "test"); + EXPECT_TRUE(frame.instance_name == "unit"); + EXPECT_TRUE(frame.field == "value"); + + probot::logging::unregisterSource(&src); + __serialStubClear(); +} + +TEST_CASE(logging_serial_reports_drops){ + __serialStubClear(); + __wifiStubClear(); + auto& mgr = probot::logging::manager(); + probot::logging::enableSerialLogging(true); + probot::logging::setSerialBandwidthMode(probot::logging::BandwidthMode::kFull); + probot::logging::enableWifiLogging(false); + probot::logging::setWifiSendCallback(nullptr); + + DummySource src; + probot::logging::SourceRegistration reg{}; + reg.type_name = "queue"; + reg.instance_name = "overrun"; + reg.default_priority = probot::logging::Priority::kBackground; + reg.allow_background_on_wifi = false; + reg.supports_static_dump = false; + reg.dynamic_collector = nullptr; + reg.static_collector = nullptr; + const auto* stored = probot::logging::registerSource(&src, reg); + + for (int i = 0; i < 128; ++i){ + mgr.enqueueValue(*stored, + &src, + "load", + probot::logging::ValueType::kInt, + probot::logging::Priority::kUseDefault, + i, + 0.0f, + false, + nullptr, + 2000 + static_cast(i)); + } + + mgr.update(3000, 20); + const auto& buffer = __serialStubBuffer(); + EXPECT_TRUE(!buffer.empty()); + + ParsedFrame frame{}; + EXPECT_TRUE(parseFrame(buffer, 0, frame)); + EXPECT_TRUE(frame.drop_count > 0); + EXPECT_TRUE(frame.priority == probot::logging::Priority::kBackground); + + probot::logging::unregisterSource(&src); + __serialStubClear(); +} + +TEST_CASE(logging_wifi_frame_basic){ + __wifiStubClear(); + __serialStubClear(); + auto& mgr = probot::logging::manager(); + probot::logging::enableWifiLogging(true); + probot::logging::setWifiBandwidthMode(probot::logging::BandwidthMode::kFull); + probot::logging::setWifiSendCallback(__wifiStubSend); + probot::logging::setWifiEndpoint(0xFFFFFFFFu, 49160); + + DummySource src; + probot::logging::SourceRegistration reg{}; + reg.type_name = "wifi"; + reg.instance_name = "unit"; + reg.default_priority = probot::logging::Priority::kUserMarked; + reg.allow_background_on_wifi = false; + reg.supports_static_dump = false; + const auto* stored = probot::logging::registerSource(&src, reg); + + mgr.enqueueValue(*stored, + &src, + "value", + probot::logging::ValueType::kFloat, + probot::logging::Priority::kUseDefault, + 0, + 42.5f, + false, + nullptr, + 3210); + mgr.update(3210, 20); + + const auto& buffer = __wifiStubBuffer(); + EXPECT_TRUE(!buffer.empty()); + + ParsedFrame frame{}; + EXPECT_TRUE(parseFrame(buffer, 0, frame)); + EXPECT_TRUE(frame.priority == probot::logging::Priority::kUserMarked); + EXPECT_TRUE(frame.value_type == probot::logging::ValueType::kFloat); + EXPECT_NEAR(frame.float_value, 42.5f, 1e-3f); + EXPECT_TRUE(frame.type_name == "wifi"); + EXPECT_TRUE(frame.instance_name == "unit"); + + probot::logging::unregisterSource(&src); + __wifiStubClear(); + probot::logging::enableWifiLogging(false); + probot::logging::setWifiSendCallback(nullptr); +} + +TEST_CASE(logging_wifi_reports_drops){ + __wifiStubClear(); + __serialStubClear(); + auto& mgr = probot::logging::manager(); + probot::logging::enableWifiLogging(true); + probot::logging::setWifiBandwidthMode(probot::logging::BandwidthMode::kFull); + probot::logging::setWifiSendCallback(__wifiStubSendFail); + probot::logging::setWifiEndpoint(0xFFFFFFFFu, 49160); + + DummySource src; + probot::logging::SourceRegistration reg{}; + reg.type_name = "wifi"; + reg.instance_name = "drops"; + reg.default_priority = probot::logging::Priority::kUserMarked; + reg.allow_background_on_wifi = false; + reg.supports_static_dump = false; + const auto* stored = probot::logging::registerSource(&src, reg); + + mgr.enqueueValue(*stored, + &src, + "value", + probot::logging::ValueType::kInt, + probot::logging::Priority::kUseDefault, + 99, + 0.0f, + false, + nullptr, + 4000); + mgr.update(4000, 20); + + // First send should fail and not populate buffer + EXPECT_TRUE(__wifiStubBuffer().empty()); + + probot::logging::setWifiSendCallback(__wifiStubSend); + + mgr.enqueueValue(*stored, + &src, + "value", + probot::logging::ValueType::kInt, + probot::logging::Priority::kUseDefault, + 55, + 0.0f, + false, + nullptr, + 4050); + mgr.update(4050, 20); + + const auto& buffer = __wifiStubBuffer(); + EXPECT_TRUE(!buffer.empty()); + ParsedFrame frame{}; + EXPECT_TRUE(parseFrame(buffer, 0, frame)); + EXPECT_TRUE(frame.drop_count > 0); + EXPECT_TRUE(frame.int_value == 55); + + probot::logging::unregisterSource(&src); + probot::logging::enableWifiLogging(false); + probot::logging::setWifiSendCallback(nullptr); + __wifiStubClear(); +} diff --git a/tests/test_mechanisms.cpp b/tests/test_mechanisms.cpp index 0c0e3aa..bab65a9 100644 --- a/tests/test_mechanisms.cpp +++ b/tests/test_mechanisms.cpp @@ -22,11 +22,7 @@ namespace { probot::control::MotionProfileType profileType = probot::control::MotionProfileType::kNone; probot::control::MotionProfileConfig profileCfg{}; - bool claim(void*) override { return true; } - void release(void*) override {} - bool setPower(float, void*) override { return true; } - bool isClaimed() const override { return false; } - void* currentOwner() const override { return nullptr; } + bool setPower(float) override { return true; } void setInverted(bool) override {} bool getInverted() const override { return false; } diff --git a/tools/sync_version.py b/tools/sync_version.py new file mode 100755 index 0000000..4a14e23 --- /dev/null +++ b/tools/sync_version.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Synchronise project version across metadata files. + +Reads the semver string from VERSION and ensures both library.properties +and library.json expose the same value. The script is idempotent and only +touches the files when an update is required. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +class VersionSyncError(RuntimeError): + pass + + +def read_version(version_path: Path) -> str: + try: + version = version_path.read_text(encoding="utf-8").strip() + except OSError as exc: # pragma: no cover - catastrophic failure + raise VersionSyncError(f"Failed to read {version_path}") from exc + + if not version: + raise VersionSyncError(f"VERSION file {version_path} is empty") + return version + + +def update_library_properties(path: Path, version: str) -> bool: + if not path.exists(): + raise VersionSyncError(f"Missing {path} required for Arduino metadata") + + text = path.read_text(encoding="utf-8").splitlines() + updated = False + for idx, line in enumerate(text): + if line.startswith("version="): + if line != f"version={version}": + text[idx] = f"version={version}" + updated = True + break + else: + text.append(f"version={version}") + updated = True + + if updated: + path.write_text("\n".join(text) + "\n", encoding="utf-8") + return updated + + +def default_library_json() -> dict: + return { + "name": "Probot Lib", + "version": "0.0.0", + "description": "ESP32-S3 robotics control library", + "keywords": ["robotics", "pid", "motion-control"], + "repository": { + "type": "git", + "url": "https://github.com/nfrproducts/probot-lib" + }, + "frameworks": "arduino", + "platforms": ["espressif32"], + "build": { + "flags": ["-DESP32S3", "-DARDUINO_USB_MODE=1"] + }, + "dependencies": [ + { + "name": "Adafruit NeoPixel", + "version": ">=1.12.0" + } + ] + } + + +def update_library_json(path: Path, version: str) -> bool: + if path.exists(): + try: + data = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise VersionSyncError(f"Invalid JSON in {path}") from exc + else: + data = default_library_json() + + if data.get("version") == version: + return False + + data["version"] = version + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return True + + +def update_idf_component(path: Path, version: str) -> bool: + if not path.exists(): + template = [ + f"version: {version}", + "description: ESP32-S3 robotics control library", + "url: https://github.com/nfrproducts/probot-lib", + "dependencies:", + " idf:", + " version: \">=5.0\"", + " espressif/arduino-esp32:", + " version: \">=3.0.0\"", + ] + path.write_text("\n".join(template) + "\n", encoding="utf-8") + return True + + lines = path.read_text(encoding="utf-8").splitlines() + updated = False + for idx, line in enumerate(lines): + stripped = line.lstrip() + if stripped.startswith("version:"): + indent = line[: len(line) - len(stripped)] + replacement = f"{indent}version: {version}" + if line != replacement: + lines[idx] = replacement + updated = True + break + else: + lines.insert(0, f"version: {version}") + updated = True + + if updated: + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + return updated + + +def main() -> int: + root = Path(__file__).resolve().parent.parent + version_path = root / "VERSION" + lib_props_path = root / "library.properties" + lib_json_path = root / "library.json" + + version = read_version(version_path) + + props_changed = update_library_properties(lib_props_path, version) + json_changed = update_library_json(lib_json_path, version) + + idf_yaml_path = root / "idf_component.yml" + idf_changed = update_idf_component(idf_yaml_path, version) + + if props_changed or json_changed or idf_changed: + sys.stdout.write(f"Version metadata synced to {version}\n") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except VersionSyncError as exc: + sys.stderr.write(f"error: {exc}\n") + raise SystemExit(1) diff --git a/ui-testing/README.md b/ui-testing/README.md new file mode 100644 index 0000000..e5dcfdf --- /dev/null +++ b/ui-testing/README.md @@ -0,0 +1,90 @@ +# UI Testing Helpers + +Bu klasör, ESP32'ye ihtiyaç duymadan driver station arayüzünü çalıştırmak için +kullanabileceğimiz küçük bir Python sunucusu içerir. Sunucu, +`src/platform/esp32s3/web/index_html.h` dosyasındaki HTML verisini doğrudan +okur, böylece ESP'ye flash edeceğimiz içerikle birebir aynı çıktıyı görürüz. + +## Başlangıç + +```bash +python3 ui-testing/server.py +# veya belirli bir port için: +python3 ui-testing/server.py --port 9000 --open-browser +``` + +- Sunucu açıldığında `http://localhost:/` adresinden UI'ı görüntüleyebilirsiniz. +- Frontend'in kullandığı `/robotControl`, `/updateController` ve `/getBattery` + uç noktaları temel stub cevapları döndürür; bu sayede UI hata vermeden çalışır. +- Konsolda yapılan isteklere dair loglar görünür, böylece gamepad POST'ları veya + robot komutlarını takip edebilirsiniz. + +## Boyut Raporu + +Arayüzün ne kadar flash alanı kapladığını ve örnek bir derlemede toplam +kullanımı görmek için: + +```bash +# Sadece HTML boyutunu ölç +python3 ui-testing/server.py --report-only + +# HTML + DriverStationDemo derlemesiyle toplam kullanım raporu +python3 ui-testing/server.py --report-only --build-report +``` + +- HTML boyutu 6584 byte civarındadır ve 4 MiB flash'ın yaklaşık %0.16'sını kaplar. +- `--build-report` seçeneği `arduino-cli` ile + `examples/__library_impl/DriverStationDemo/DriverStationDemo.ino` dosyasını + derler ve hem flash hem de RAM kullanımını raporlar. + Gerekirse `--example`, `--fqbn` veya `--build-dir` argümanlarıyla özelleştirebilirsiniz. +- Sadece rapor alıp çıkmak için `--report-only` yeterlidir; sunucu başlatılmaz. + +## Varyasyonlarla Çalışma + +- `ui-testing/variants/` klasöründe artık temel arayüz olarak yalnızca + `minimal_stack.html` bulunur. +- Diğer örnekler (önceki `command_deck.html` ve `pit_wall.html` dahil olmak + üzere `aero_hud.html`, `base.html`, `circuit_glow.html`, `control_tower.html`, + `neon_grid.html`, `retro_terminal.html`, `zen_flow.html`) referans olarak + `ui-testing/variants/archive/` altına taşındı; dilediğiniz zaman geri almak + için bu klasörden çıkarabilirsiniz. +- Seçili bir varyasyonu denemek için: + +```bash +python3 ui-testing/server.py --html ui-testing/variants/base.html +``` + +- `--html` parametresi verildiğinde hem sunucu hem de boyut raporu bu dosyayı + kullanır. Parametreyi boş bırakırsanız kütüphanedeki gömülü `index_html.h` + okunmaya devam eder (donanım gerçekliğini test etmek için yararlı). + +## Wi-Fi Logging UI + +- `minimal_stack.html` arayüzü içinde yeni **Wi-Fi Logging** kartı bulunur. +- Kart, `/logging/status` uç noktasından durumu okur, `Start/Stop` düğmesiyle + `/logging/toggle` komutunu gönderir, `Clear` ise `/logging/clear` çağırır. +- Log akışı `System Logs` kartında `Wi-Fi Log Stream` alanına `/logging/stream` + cevabını yazar; `Download` düğmesi `/logging/download` bağlantısını yeni + sekmede açar. +- Varsayılan olarak her 4 saniyede bir durum ve log bilgileri tazelenir. +- Otomatik yenileme ve kaydırma kapatılabilir; bu sayede ESP tarafındaki + Wi-Fi logging altyapısı hazır olduğunda arayüz ek gelişmeye gerek kalmadan + çalışır. + +## Tüm Varyantları Aynı Anda Görmek + +- 10 örnek tasarımı (port 9030–9039) aynı anda açmak için: + + ```bash + python3 ui-testing/server.py --suite + ``` + +- Varsayılan port aralığı 9030'dan başlar. Gerekirse `--suite-base-port` veya + `--suite-limit` ile özelleştirebilirsiniz. Sadece rapor almak isterseniz + `--report-only --suite` komutu yeterlidir; sunucular başlatılmaz. + +## Notlar + +- Sunucu HTML içeriğini her istekte yeniden okuduğu için `index_html.h` + üzerinde yaptığınız değişiklikleri görmek için sunucuyu yeniden başlatmanıza gerek yoktur. +- Python standart kütüphanesinden başka bir bağımlılık gerektirmez. diff --git a/ui-testing/server.py b/ui-testing/server.py new file mode 100644 index 0000000..f08f9be --- /dev/null +++ b/ui-testing/server.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python3 +""" +Lightweight local simulator for the Probot driver station web UI. + +It serves the HTML bundle embedded in `src/platform/esp32s3/web/index_html.h` +and provides stub endpoints so the frontend behaves as if it were talking to +the ESP32 firmware. This lets us iterate on the UI without flashing hardware. +""" +from __future__ import annotations + +import argparse +import http.server +import json +import logging +import re +import shutil +import socketserver +import subprocess +import sys +import threading +import time +import urllib.parse +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +REPO_ROOT = Path(__file__).resolve().parents[1] +INDEX_HEADER = REPO_ROOT / "src/platform/esp32s3/web/index_html.h" +START_TOKEN = 'R"=====(' +END_TOKEN = ')=====";' +ESP32S3_FLASH_BYTES = 4 * 1024 * 1024 # 4 MiB default flash size +VARIANTS_DIR = Path(__file__).resolve().parent / "variants" +def load_embedded_html() -> str: + """Extract the HTML payload from the C++ header without modifying the source.""" + try: + text = INDEX_HEADER.read_text(encoding="utf-8") + except FileNotFoundError as exc: # pragma: no cover - guard rail + raise SystemExit( + f"Could not find driver station header at {INDEX_HEADER}. " + "Run from inside the repository root." + ) from exc + + try: + start = text.index(START_TOKEN) + len(START_TOKEN) + end = text.index(END_TOKEN, start) + except ValueError as exc: # pragma: no cover - guard rail + raise SystemExit( + "Unable to locate embedded HTML block. " + "Expected tokens were missing in index_html.h." + ) from exc + + # Preserve exact formatting so we are looking at the same bytes flashed to the ESP. + return text[start:end] + + +def load_html(html_override: Optional[Path]) -> str: + if html_override: + return html_override.read_text(encoding="utf-8") + return load_embedded_html() + + +def compute_html_stats(html: str) -> Tuple[int, float]: + """Return payload size (including terminator) and flash percentage.""" + byte_count = len(html.encode("utf-8")) + 1 # include null terminator stored in PROGMEM + percent = (byte_count / ESP32S3_FLASH_BYTES) * 100.0 + return byte_count, percent + + +def run_build_report( + example: Path, + build_dir: Path, + arduino_cli: str, + fqbn: str, +) -> Optional[Dict[str, float]]: + """Compile the chosen example and parse flash/RAM usage.""" + if shutil.which(arduino_cli) is None: + logging.error("arduino-cli not found on PATH; skipping build report.") + return None + + if not example.exists(): + logging.error("Example sketch not found at %s", example) + return None + + build_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + arduino_cli, + "compile", + "--fqbn", + fqbn, + "--warnings", + "none", + "--library", + str(REPO_ROOT), + "--build-path", + str(build_dir), + str(example), + ] + + logging.info("Running: %s", " ".join(cmd)) + proc = subprocess.run(cmd, capture_output=True, text=True) + output = (proc.stdout or "") + (proc.stderr or "") + + if proc.returncode != 0: + logging.error("arduino-cli compile failed with exit code %s", proc.returncode) + logging.error(output.strip()) + return None + + flash_match = re.search( + r"Sketch uses (\d+) bytes \(~?([0-9.]+)%\) of program storage space\. Maximum is (\d+) bytes\.", + output, + ) + ram_match = re.search( + r"Global variables use (\d+) bytes \(~?([0-9.]+)%\) of dynamic memory, " + r"leaving (\d+) bytes for local variables\. Maximum is (\d+) bytes\.", + output, + ) + + if not flash_match: + logging.warning("Could not parse flash usage from arduino-cli output.") + logging.debug(output) + return None + + flash_used = int(flash_match.group(1)) + flash_percent = float(flash_match.group(2)) + flash_max = int(flash_match.group(3)) + + stats: Dict[str, float] = { + "flash_used": flash_used, + "flash_percent": flash_percent, + "flash_max": flash_max, + } + + if ram_match: + stats.update( + ram_used=int(ram_match.group(1)), + ram_percent=float(ram_match.group(2)), + ram_max=int(ram_match.group(4)), + ) + + stats["log"] = output.strip() + return stats + + +class DriverStationHandler(http.server.BaseHTTPRequestHandler): + """HTTP handler that mimics the ESP32 endpoints exposed by the driver station.""" + + server_version = "ProbotUISim/0.1" + + # Shared simulator state. This is intentionally minimal for now. + robot_state = { + "status": "init", # init | start | stop (matches UI expectations) + "autonomous": True, + "auto_period": 30, + "battery": 12.4, + "last_gamepad_update": 0.0, + } + state_lock = threading.Lock() + + def do_GET(self) -> None: # noqa: N802 - keep BaseHTTPRequestHandler signature + parsed = urllib.parse.urlparse(self.path) + path = parsed.path + + if path in ("/", "/index.html"): + self._serve_index() + return + + if path == "/robotControl": + self._handle_robot_control(parsed.query) + return + + if path == "/getBattery": + self._send_text("text/plain", f"{self.robot_state['battery']:.1f}") + return + + if path == "/favicon.ico": + self.send_error(404) + return + + self.send_error(404, f"Unsupported path: {path}") + + def do_POST(self) -> None: # noqa: N802 - keep BaseHTTPRequestHandler signature + parsed = urllib.parse.urlparse(self.path) + if parsed.path == "/updateController": + self._handle_update_controller() + return + + self.send_error(404, f"Unsupported path: {parsed.path}") + + # ===== Helpers ============================================================ + def _serve_index(self) -> None: + try: + html = load_html(getattr(self.server, "html_override", None)) + except OSError as exc: + logging.error("Failed to load HTML content: %s", exc) + self.send_error(500, "Unable to load UI content.") + return + payload = html.encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def _handle_robot_control(self, query: str) -> None: + params = urllib.parse.parse_qs(query) + cmd = params.get("cmd", [""])[0] + auto = params.get("auto", ["1"])[0] == "1" + auto_len = params.get("autoLen", ["30"])[0] + + with self.state_lock: + self.robot_state["autonomous"] = auto + try: + self.robot_state["auto_period"] = int(auto_len) + except ValueError: + pass + if cmd in ("init", "start", "stop"): + self.robot_state["status"] = cmd + + logging.info( + "robotControl cmd=%s auto=%s autoLen=%s", cmd, auto, auto_len + ) + + self._send_text("text/plain", "OK") + + def _handle_update_controller(self) -> None: + length_header = self.headers.get("Content-Length") + try: + length = int(length_header or "0") + except ValueError: + self.send_error(400, "Invalid Content-Length") + return + + body = self.rfile.read(length) + try: + payload = json.loads(body.decode("utf-8")) + except json.JSONDecodeError: + logging.warning("Received malformed controller payload: %r", body) + payload = {} + + with self.state_lock: + self.robot_state["last_gamepad_update"] = time.time() + + logging.debug("Controller update: %s", payload) + self._send_text("text/plain", "OK") + + def log_message(self, format: str, *args: Tuple[object, ...]) -> None: # type: ignore[override] + # Route all logs through logging module for consistency. + logging.info("%s - %s", self.address_string(), format % args) + + def _send_text(self, content_type: str, payload: str) -> None: + data = payload.encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Serve the Probot driver station UI locally." + ) + parser.add_argument( + "--port", "-p", type=int, default=8080, + help="Port to bind (default: 8080).", + ) + parser.add_argument( + "--open-browser", action="store_true", + help="Attempt to open the UI in the default browser after starting.", + ) + parser.add_argument( + "--log-level", default="INFO", + choices=("DEBUG", "INFO", "WARNING", "ERROR"), + help="Logging verbosity (default: INFO).", + ) + parser.add_argument( + "--build-report", action="store_true", + help="Run arduino-cli compile to capture flash/RAM usage before serving.", + ) + parser.add_argument( + "--report-only", action="store_true", + help="Emit size report (with optional build report) then exit.", + ) + parser.add_argument( + "--suite", action="store_true", + help="Serve every HTML variant on consecutive ports starting at --suite-base-port.", + ) + parser.add_argument( + "--suite-base-port", type=int, default=9030, + help="Base port for --suite mode (default: 9030).", + ) + parser.add_argument( + "--suite-limit", type=int, default=10, + help="Maximum number of variants to serve when --suite is enabled (default: 10).", + ) + parser.add_argument( + "--suite-dir", + default=str(VARIANTS_DIR), + help="Directory scanned for variant HTML files in --suite mode.", + ) + parser.add_argument( + "--example", + default=str(REPO_ROOT / "examples/__library_impl/DriverStationDemo/DriverStationDemo.ino"), + help="Sketch path to compile for build report.", + ) + parser.add_argument( + "--arduino-cli", + default="arduino-cli", + help="arduino-cli executable to use for build report.", + ) + parser.add_argument( + "--fqbn", + default="esp32:esp32:esp32s3", + help="FQBN passed to arduino-cli during build report.", + ) + parser.add_argument( + "--build-dir", + default=str(REPO_ROOT / ".build/ui-testing-report"), + help="Output directory for build artifacts when computing build report.", + ) + parser.add_argument( + "--html", + help="Path to an HTML variant to serve/measure instead of the embedded header.", + ) + return parser.parse_args(argv) + + +class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer): + daemon_threads = True + allow_reuse_address = True + + def __init__(self, server_address, RequestHandlerClass, html_override=None): + self.html_override = html_override + super().__init__(server_address, RequestHandlerClass) + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + logging.basicConfig( + level=getattr(logging, args.log_level), + format="[%(levelname)s] %(message)s", + ) + + if not INDEX_HEADER.exists(): + logging.error("Driver station header not found at %s", INDEX_HEADER) + return 1 + + if args.html and args.suite: + logging.error("--html and --suite cannot be used together.") + return 1 + + html_variants: List[Optional[Path]] = [] + if args.suite: + suite_dir = Path(args.suite_dir).resolve() + if not suite_dir.exists(): + logging.error("Variant directory not found: %s", suite_dir) + return 1 + html_variants = sorted(suite_dir.glob("*.html"))[: max(args.suite_limit, 0)] + if not html_variants: + logging.error("No HTML variants found in %s", suite_dir) + return 1 + else: + override = None + if args.html: + override = Path(args.html) + if not override.is_absolute(): + override = (REPO_ROOT / args.html).resolve() + if not override.exists(): + logging.error("HTML override file not found at %s", override) + return 1 + html_variants.append(override) + + build_stats = None + if args.build_report: + build_stats = run_build_report( + example=Path(args.example), + build_dir=Path(args.build_dir), + arduino_cli=args.arduino_cli, + fqbn=args.fqbn, + ) + + flash_mib = ESP32S3_FLASH_BYTES / (1024 * 1024) + for html_path in html_variants: + payload = load_html(html_path) + bytes_used, pct = compute_html_stats(payload) + name = html_path.name if html_path else "embedded" + logging.info( + "Embedded UI payload (%s): %d bytes (%.4f%% of %.2f MiB flash)", + name, + bytes_used, + pct, + flash_mib, + ) + if build_stats: + logging.info( + "Sketch flash usage: %d bytes (%.2f%% of %d bytes)", + int(build_stats["flash_used"]), + build_stats["flash_percent"], + int(build_stats["flash_max"]), + ) + if build_stats["flash_used"]: + logging.info( + "UI payload share: %.3f%% of compiled image (%d / %d bytes)", + (bytes_used / build_stats["flash_used"]) * 100.0, + bytes_used, + int(build_stats["flash_used"]), + ) + if build_stats.get("ram_used") is not None: + logging.info( + "Sketch RAM usage: %d bytes (%.2f%% of %d bytes)", + int(build_stats["ram_used"]), + build_stats["ram_percent"], + int(build_stats["ram_max"]), + ) + + if args.report_only: + return 0 + + handler = DriverStationHandler + + if args.suite: + servers: List[ThreadedHTTPServer] = [] + threads: List[threading.Thread] = [] + try: + for idx, html_path in enumerate(html_variants): + port = args.suite_base_port + idx + httpd = ThreadedHTTPServer(("", port), handler, html_override=html_path) + thread = threading.Thread(target=httpd.serve_forever, daemon=True) + thread.start() + servers.append(httpd) + threads.append(thread) + logging.info("Serving %s at http://localhost:%d/", html_path.name, port) + logging.info("Press Ctrl+C to stop all servers.") + while True: + time.sleep(1) + except KeyboardInterrupt: + logging.info("Stopping suite servers...") + finally: + for httpd in servers: + httpd.shutdown() + for thread in threads: + thread.join(timeout=1) + return 0 + + html_override = html_variants[0] + with ThreadedHTTPServer(("", args.port), handler, html_override=html_override) as httpd: + url = f"http://localhost:{args.port}/" + logging.info("Serving driver station UI at %s", url) + + if args.open_browser: + try: + import webbrowser + + webbrowser.open(url) + except Exception as exc: # pragma: no cover - guard rail + logging.warning("Failed to open browser: %s", exc) + + try: + httpd.serve_forever() + except KeyboardInterrupt: + logging.info("Shutting down simulator.") + httpd.shutdown() + + return 0 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main(sys.argv[1:])) diff --git a/ui-testing/variants/archive/aero_hud.html b/ui-testing/variants/archive/aero_hud.html new file mode 100644 index 0000000..32b1e9f --- /dev/null +++ b/ui-testing/variants/archive/aero_hud.html @@ -0,0 +1,398 @@ + + + + + ProBot Flight HUD + + + +
ProBot Flight HUD
+
+
+
+
+
+
+
Status: INIT
+
+
+
+ +
No Gamepad
+
+
Phase: Standby
+
+
+ +
+
+ AUTO MODE +
Switch autonomous drive
+
+ +
+
+ DURATION + +
+ +
+
+
+

Joystick Telemetry

+
+ +
+
No gamepad selected.
+
No axis data...
+
No button data...
+
+
+ + + diff --git a/ui-testing/variants/archive/base.html b/ui-testing/variants/archive/base.html new file mode 100644 index 0000000..8d32759 --- /dev/null +++ b/ui-testing/variants/archive/base.html @@ -0,0 +1,67 @@ + + + + + ProBot UI + + + + +
+
+

Main Drive

+

Joystick Status: Not Connected

+ +

+

+

+
+
+

Joystick Test

+

Connect one or more controllers, press a button to register them, then pick which to use: + +

+

Selected Gamepad Info

+
No gamepad selected.
+

Axes

+
No data yet...
+

Buttons

+
No data yet...
+
+
+ + + diff --git a/ui-testing/variants/archive/circuit_glow.html b/ui-testing/variants/archive/circuit_glow.html new file mode 100644 index 0000000..f4987c0 --- /dev/null +++ b/ui-testing/variants/archive/circuit_glow.html @@ -0,0 +1,365 @@ + + + + + ProBot Circuit Glow + + + +
+
+

Circuit Glow

+ ProBot Control Mesh +
+
Standby
+
+
+
+

Drive Lattice

+
+ +
+ +
+ Mode + +
+
+
+ + + Set to match format +
+
+ +

Joystick: Awaiting link...

+
+
+

Telemetry Weave

+ +
No gamepad selected.
+
Axis lines idle...
+
Button lines idle...
+
+
+ + + diff --git a/ui-testing/variants/archive/command_deck.html b/ui-testing/variants/archive/command_deck.html new file mode 100644 index 0000000..7ceda62 --- /dev/null +++ b/ui-testing/variants/archive/command_deck.html @@ -0,0 +1,634 @@ + + + + + ProBot Command Deck + + + +
+

ProBot Command Deck

+ + +
Phase: Standby
+
+
+
+

+ Match Control + + + Not Connected + +

+
00:30
+
+ +
+ + +
+
+ + +
+
+ +
+
+

Joystick Console

+
+ + +
+
No gamepad selected.
+
No data yet...
+
No data yet...
+
+
+ + + diff --git a/ui-testing/variants/archive/control_tower.html b/ui-testing/variants/archive/control_tower.html new file mode 100644 index 0000000..0ed02c9 --- /dev/null +++ b/ui-testing/variants/archive/control_tower.html @@ -0,0 +1,352 @@ + + + + + ProBot Control Tower + + + +
ProBot Control Tower
+
+ +
+
+ Match Operation + Driver Station Control Pane +
+
+
+ + +
+
+ +
+ Enabled + +
+
+
+
+

Input Telemetry

+
No gamepad selected.
+
Axis data not streaming...
+
Button data not streaming...
+
+
+
+ + + diff --git a/ui-testing/variants/archive/neon_grid.html b/ui-testing/variants/archive/neon_grid.html new file mode 100644 index 0000000..40d803b --- /dev/null +++ b/ui-testing/variants/archive/neon_grid.html @@ -0,0 +1,356 @@ + + + + + ProBot Neon Grid + + + +
+
+

Neon Grid

+ ProBot Control Sphere +
+
INIT
+
+
+
+

Match Vector

+
+ +
+ +
+ Mode + +
+
+
+ + + Set per match constraints +
+
+ +

Joystick Status: Awaiting link

+
+
+

Input Matrix

+ +
No gamepad selected.
+
Axis stream idle...
+
Button stream idle...
+
+
+ + + diff --git a/ui-testing/variants/archive/pit_wall.html b/ui-testing/variants/archive/pit_wall.html new file mode 100644 index 0000000..02cf174 --- /dev/null +++ b/ui-testing/variants/archive/pit_wall.html @@ -0,0 +1,433 @@ + + + + + ProBot Pit Wall + + + +
+

ProBot Pit Wall

+ +
+
+ PHASE + Standby +
+
+ JOYSTICK + No Link +
+
+ BATTERY + --.- V +
+
+
+
+
+

Match Control INIT

+
+ +
+ + +
+
+ + + Match rule default: 30s +
+
+ +
+
+

Joystick Telemetry

+ +
No gamepad selected.
+
No axis data...
+
No button data...
+
+
+ + + diff --git a/ui-testing/variants/archive/retro_terminal.html b/ui-testing/variants/archive/retro_terminal.html new file mode 100644 index 0000000..e2ba12a --- /dev/null +++ b/ui-testing/variants/archive/retro_terminal.html @@ -0,0 +1,287 @@ + + + + + ProBot Retro Terminal + + + +
ProBot Retro Terminal
+
+
+

Match Console

+
+ PHASE: INIT + JOYSTICK: NONE +
+
+
+ + +
+
+ + + +
+
+ + +
+
+
+
+

Input Feed

+ +
No gamepad selected.
+
Axis feed idle...
+
Buttons idle...
+
+
+ + + diff --git a/ui-testing/variants/archive/zen_flow.html b/ui-testing/variants/archive/zen_flow.html new file mode 100644 index 0000000..ccaf730 --- /dev/null +++ b/ui-testing/variants/archive/zen_flow.html @@ -0,0 +1,359 @@ + + + + + ProBot Zen Flow + + + +
+
+

Zen Flow Control

+ Balanced driver station +
+
Standby
+
+
+
+

Match Overview

+
+
Robot Command
+
Autonomous
+
Telemetry
+
+ +
+
+ + + Match default 30s +
+
+ +
+ Enabled + +
+ +
+
+
Joystick Status: Disconnected
+
+
+

Input Telemetry

+ +
No gamepad selected.
+
Axis data pending...
+
Button data pending...
+
+
+ + + diff --git a/ui-testing/variants/minimal_stack.html b/ui-testing/variants/minimal_stack.html new file mode 100644 index 0000000..d1976b2 --- /dev/null +++ b/ui-testing/variants/minimal_stack.html @@ -0,0 +1,1458 @@ + + + + + ProBot Driver Station + + + +
+
+

ProBot Driver Station

+ by NFR Products · c2025 +
+ +
+ Status + Standby + Awaiting command +
+
+
+
+
+

Match Control

+
+ +
+ + + Seconds +
+
+ Autonomous + +
+
+
+
+ Autonomous Countdown + 00.0 s +
+
+
+
+
+
+
+

Joystick Check

+
+
+
+
+
+
+ Axes +
+
+
+
+ Not Connected + +
+
+
+
+
+

Wi-Fi Logging

+
+ Disconnected + Waiting for Wi-Fi module... +
+
+
+ Network + +
+
+ Robot IP + +
+
+ Clients + 0 +
+
+ Rate + 0 KB/s +
+
+
+ + + +
+
+ + + Last sync: never +
+
+
+

System Logs

+ +
No gamepad selected.
+
No axis data...
+
No button data...
+
+
+ Wi-Fi Log Stream + +
+
Waiting for log data...
+
+
+
+
+ + +