diff --git a/.gitignore b/.gitignore index 833db759c..9484444a0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,11 @@ blacktip_dpv/generated/ blacktip_dpv/README.dist.md blacktip_dpv/ui.dist.qml blacktip_dpv/.git_branch_cache +ebike/generated/ +ebike/README.dist.md +ebike/ui.dist.qml +ebike/.git_branch_cache +vescGPT/ **/__pycache__/ .DS_Store **/.DS_Store diff --git a/Makefile b/Makefile index 4996b629f..faa20f62d 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ -PKGS = balance blacktip_dpv refloat tnt vbms32 vbms32_micro +PKGS = balance blacktip_dpv ebike refloat tnt vbms32 vbms32_micro PKGS += lib_files lib_interpolation lib_nau7802 lib_pn532 PKGS += lib_ws2812 logui lib_code_server lib_midi lib_disp_ui PKGS += vdisp lib_tca9535 vbms_harmony32 vbms_harmony16 PKGS += dash35b vl_bike_39p lib_bq27441 boosted_doctor dash16 PKGS += lib_tca9534 UnleashedCreativityLights -TEST_PKGS = blacktip_dpv +TEST_PKGS = ebike all: vesc_pkg_all.rcc diff --git a/ebike/DEVELOPMENT.md b/ebike/DEVELOPMENT.md new file mode 100644 index 000000000..f938341d4 --- /dev/null +++ b/ebike/DEVELOPMENT.md @@ -0,0 +1,297 @@ +# E-Bike Pkg Development Documentation + +## lispBM Runtime Architecture Overview + +The E-Bike Pkg lispBM runtime is organized as a set of cooperative threads and event-driven state machines, designed for reliability and clarity on resource-constrained VESC hardware. Key architectural components: + +* **State Machine:** + * Manages button presses, clicks, long holds, and transitions between operational states (off, running, Smart Cruise, etc.). + * Implements logic for Smart Cruise activation, speed changes, and safety features. +* **Display Update Loop:** + * Periodically updates the OLED display with speed, battery, Smart Cruise status, error codes, and timer bar. + * Uses a lookup table for LED timer bar mapping (left-to-right countdown). + * Loads display frames from a binary file generated from a CSV asset. +* **Smart Cruise Logic:** + * Allows hands-free operation with configurable timeout and auto-engage. + * Visual feedback via display and 8-LED timer bar; warning mode triggers slowdown and beep. + * Speed changes require a long hold before tap, preventing accidental adjustments. +* **Peripheral Loops:** + * Separate threads for motor control, trigger/button polling, battery monitoring, and Smart Cruise background checks. + * Each loop uses minimal stack and sleep intervals to conserve memory and CPU. +* **Logging and Debugging:** + * Conditional debug logging via `debug_log` and `when-debug` macros to minimize memory usage. + * Debug output can be enabled for troubleshooting in VESC Tool. +* **Configuration and EEPROM:** + * Settings are stored in EEPROM and loaded at startup; configuration is managed via QML UI in VESC Tool. + * Battery calculation supports voltage-based or ampere-hour-based methods. + +For more details, see the sections below and refer to `ebike.lisp` for implementation specifics. + +This document contains information for developers working on the E-Bike Pkg VESC package. + +## Build System + +The project uses GNU Make for building the VESC package: + + make # Build ebike.vescpkg + make clean # Remove generated files + make test # Run code quality checks and smoke tests + make smoke-tests # Run unit-style smoke tests only + make binary # Generate binary LUT files only + +### Version Management + +The build system automatically generates version information for the package: + +* **Version source**: Base version (e.g., `1.0.0`) is stored in `README.md` in the line `**Version:** 1.0.0` +* **Version format:** + * On `main` branch: `--` (e.g., `1.0.0-20251013-120605-2eacfa2`) + * On other branches: `--` (e.g., `1.0.0-feature-xyz-2eacfa2`) +* **Distribution**: During build, `tools/update_version.sh` creates `README.dist.md` with the full version and build timestamp +* **Distribution**: During build, `tools/update_version.sh` creates `ui.dist.qml` with the full version and build timestamp +* **Package**: The `.vescpkg` includes `README.dist.md` and `ui.dist.qml` (referenced in `pkgdesc.qml`) so users see the detailed version info +* **Repository**: The `README.md` in the repository shows only the base version for simplicity +* **Rebuild behavior**: The package is only rebuilt when source files change (`ebike.lisp`, `ui.qml`, `pkgdesc.qml`, `README.md`) + +To update the version: + +1. Edit the version line in `README.md`: `**Version:** 1.1.0` +1. Run `make` - the distribution file will be generated automatically with a new timestamp + +**Note**: `README.dist.md` and `ui.dist.qml` are generated during build and should not be committed to git. + +### Test Suite + +The project includes smoke tests for pure functions to catch regressions before flashing hardware: + + make smoke-tests # Run 30+ unit tests for pure functions + +**Tested functions:** + +* `clamp` - Value clamping (7 tests) +* `validate_boolean` - Boolean validation (5 tests) +* `state_name_for` - State name mapping (6 tests) +* `speed_percentage_at` - Speed percentage lookup (5 tests) +* `calculate_rpm` - RPM calculation (7 tests) + +Tests are implemented in Python (`tests/run_tests.py`) to mirror the LispBM implementations and verify correctness without needing hardware. + +## Project Structure + + ebike/ + ├── assets/ // Source data files + │ └── display_lut.csv // Display frames (124 frames × 4 rotations) + ├── tools/ // Build and development tools + │ ├── generate_lut_binary.py // Generates binary files from CSV + │ └── preview_display.py // ASCII/PGM visualization tool + ├── generated/ // Auto-generated files (not in git) + │ └── display_lut.bin // Binary display data (1992 bytes) + ├── ebike.lisp // Main lispBM runtime source + ├── ui.qml // User interface + ├── pkgdesc.qml // Package descriptor + └── README.md // User-facing documentation + +## Display Assets and Tooling + +### Asset Files + +The OLED screen artwork lives in `assets/display_lut.csv` as a CSV file: + +* **`display_lut.csv`** — All display frames (124 rows, one per screen/rotation) + +This CSV file is the source of truth. The build system automatically generates the binary display LUT from it. + +### Preview Tool + +Visualize and verify display artwork before building: + + // List all available screens + python tools/preview_display.py --list + // Preview a specific frame by index + python tools/preview_display.py --index 0 + // Preview by name and rotation + python tools/preview_display.py --name "Display Battery 4 Bars" --rotation 0 + // Show all screens for a given rotation + python tools/preview_display.py --show-all-rotation 0 + // Export as PGM image + python tools/preview_display.py --index 0 --output preview.pgm + +The preview tool applies the correct 90° clockwise rotation and vertical flip to match the physical hardware orientation. + +### Binary File Format + +The lispBM runtime loads display data from a binary file at runtime using LispBM's `import` statement. This file is automatically generated from the CSV asset during the build process. + +**Display LUT** (`generated/display_lut.bin`): + +* Header (8 bytes): + * Magic number: 0x4C555444 ("LUTD" in ASCII) + * Version: u16 (currently 1) + * Frame count: u16 (currently 124) +* Frame data: 124 frames × 4 rotations × 16 bytes per frame = 1984 bytes +* Total size: 1992 bytes + +## Development Workflow + +### Editing Display Artwork + +1. Edit `assets/display_lut.csv` (modify existing frames or add new ones) +1. Preview your changes: `python tools/preview_display.py --index N` +1. Build package: `make` + +The binary files will be automatically regenerated from the CSV. + +### Adding New Displays + +1. Add four rows to `display_lut.csv` (one per rotation 0-3). The `index` field must be sequential and unique. Each row has 16 byte fields (`b0` through `b15`) representing the 8×8 pixel matrix as interleaved low/high column bytes. + +## Display Orientation + +The display hardware is rotated 90° clockwise relative to the natural orientation. The preview tool and lispBM runtime both handle this transformation automatically. + +Each display frame consists of: + +* 8 columns × 8 rows = 64 pixels +* Stored as 16 bytes (8 column pairs, each pair = low byte + high byte) +* Bit 7 (MSB) = top pixel, Bit 0 (LSB) = bottom pixel in each column + +## Code Quality + +### Testing + + make test # Runs whitespace checks + +### Code Style + +* Use snake\_case for LispBM variable and function names +* Use 4 spaces for indentation (not tabs) +* No trailing whitespace +* Brace style: 1TBS (One True Brace Style) +* See `.github/instructions/copilot-instructions.md` for full style guide + +## Logging Hygiene + +The lispBM runtime uses conditional debug logging to minimize memory pressure on the resource-constrained VESC hardware. + +### Debug Logging Functions + +Two mechanisms are available for debug logging: + +**`debug_log` function** - For static strings: + + (debug_log "Motor: Stopping motor") + +**`when-debug` macro** - For dynamic strings with expensive operations: + + (when-debug (str-merge "Speed: Set to " (to-str clamped_speed))) + +### When to Use `when-debug` + +Use the `when-debug` macro when logging requires: + +* String concatenation (`str-merge`) +* Number-to-string conversion (`to-str`) +* Any other expensive operations + +The macro only evaluates these expressions when `debug_enabled` is 1, preventing unnecessary memory allocation and CPU cycles in hot paths. + +### Hot Paths Requiring `when-debug` + +* `set_speed_safe` - Called every speed change (multiple times per second) +* Motor control loop - Runs continuously +* State machine handlers - Active during user interactions +* Click action handlers - Called on every button press + +### Example + +**Bad** (evaluates `str-merge` even when debug is off): + + (debug_log (str-merge "Speed: Set to " (to-str speed))) + +**Good** (only evaluates when debug is enabled): + + (when-debug (str-merge "Speed: Set to " (to-str speed))) + +## Testing Best Practices + +### Adding Tests for Pure Functions + +When adding new pure functions (functions without side effects), add corresponding tests to `tests/run_tests.py`: + +1. **Identify pure functions** - Functions that always return the same output for the same input, with no side effects (no I/O, no state modification) +2. **Implement Python equivalent** - Create a Python version that mirrors the LispBM logic exactly +3. **Write test cases** - Cover: + +* Normal/expected inputs +* Boundary conditions (min, max, zero) +* Edge cases (negative, overflow, empty) +* Error conditions + +4. **Run tests before committing**: + +### What to Test + +✅ **Pure functions** - Calculations, validation, state mappings +✅ **Boundary conditions** - Min/max values, thresholds +✅ **Edge cases** - Empty lists, negative values, overflow +❌ **Hardware I/O** - Not feasible without full simulation +❌ **State machines** - Complex runtime behavior, test manually + +### Test Structure + +Each test function should: + +* Have a descriptive name (`test_function_name`) +* Print a section header +* Use `assert_eq` or `assert_near` for validation +* Include descriptive test names explaining what's being tested + +Example: + + def test_new_function(): + print("\n=== Testing new_function ===") + assert_eq(new_function(5), 10, "new_function: basic case") + assert_eq(new_function(0), 0, "new_function: zero input") + assert_eq(new_function(-1), 0, "new_function: negative clamped") + +## Binary Loading Implementation + +The runtime uses LispBM's `import` statement to load binary data at runtime: + + ; Import binary files + (import "generated/display_lut.bin" 'display-lut-bin) + ; Validate headers + (defun validate-lut-header (data magic expected-version) { ... }) + ; Access display data (offset by 8-byte header) + (bufcpy pixbuf 0 display-lut-bin (+ 8 start_pos) 16) + +### Why `pixbuf` is Required + +The `pixbuf` variable is a 16-byte working buffer that is essential to the display system and **cannot be removed**: + + (let ((start_pos 0) + (pixbuf (array-create 16))) { // Temporary 16-byte buffer + ... + // Copy 16 bytes from binary file to pixbuf + (bufcpy pixbuf 0 display-lut-bin (+ 8 start_pos) 16) + + // Send pixbuf to display via I2C + (i2c-tx-rx mpu-addr pixbuf) + }) + +**Why it's needed:** + +1. The `i2c-tx-rx` function requires a buffer to send data +2. We cannot send data directly from the binary file to I2C +3. The buffer extracts the specific 16-byte frame we need from the larger binary file +4. It's a small (16 bytes) stack-allocated array with negligible overhead + +## Build Dependencies + +* Python 3.x (for build tools) +* VESC Tool (for building .vescpkg files) +* Standard Unix tools (make, grep, etc.) + +## Repository + + diff --git a/ebike/Makefile b/ebike/Makefile new file mode 100644 index 000000000..3b6dcb8e9 --- /dev/null +++ b/ebike/Makefile @@ -0,0 +1,67 @@ +VESC_TOOL ?= vesc_tool +PYTHON ?= python3 +BASH ?= bash + +GENERATED_DIR := generated +BINARY_STAMP := $(GENERATED_DIR)/binary_luts.stamp +LUT_BINARY := $(GENERATED_DIR)/display_lut.bin +DIST_FILES := README.dist.md ui.dist.qml + +# Source files that should trigger a rebuild +SOURCE_FILES := ebike.lisp ui.qml pkgdesc.qml README.md +ASSET_FILES := assets/display_lut.csv +BINARY_GENERATOR := tools/generate_lut_binary.py +VERSION_GENERATOR := tools/update_version.sh + +# Track the current git branch to detect branch changes +CURRENT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") +BRANCH_FILE := .git_branch_cache + +all: ebike.vescpkg + +$(BINARY_STAMP): $(ASSET_FILES) $(BINARY_GENERATOR) + @mkdir -p $(GENERATED_DIR) + $(PYTHON) $(BINARY_GENERATOR) + @touch $@ + +$(LUT_BINARY): $(BINARY_STAMP) + +# Update branch cache if it doesn't exist or branch has changed +$(BRANCH_FILE): FORCE + @OLD_BRANCH=$$(cat $@ 2>/dev/null || echo ""); \ + if [ "$$OLD_BRANCH" != "$(CURRENT_BRANCH)" ]; then \ + echo "$(CURRENT_BRANCH)" > $@; \ + fi + +# Generate distribution README when source files or branch changes +$(DIST_FILES) &: $(SOURCE_FILES) $(VERSION_GENERATOR) $(BRANCH_FILE) + @$(BASH) $(VERSION_GENERATOR) + +ebike.vescpkg: README.dist.md $(LUT_BINARY) + $(VESC_TOOL) --buildPkgFromDesc pkgdesc.qml --testPkgDesc 'vesc:test' + +check-whitespace: + @echo "Checking for trailing whitespace..." + @if grep -nE '[[:space:]]+$$' ebike.lisp; then \ + echo "Error: Trailing whitespace found (shown above)"; \ + exit 1; \ + else \ + echo "✓ No trailing whitespace found"; \ + fi + +smoke-tests: + @echo "Running smoke tests..." + @$(PYTHON) tests/run_tests.py + +binary: $(LUT_BINARY) + +test: check-whitespace smoke-tests + @echo "All checks passed" + +clean: + rm -f ebike.vescpkg $(DIST_FILES) $(BRANCH_FILE) + rm -rf $(GENERATED_DIR) + +.PHONY: all test clean check-whitespace smoke-tests binary FORCE + +FORCE: diff --git a/ebike/README.md b/ebike/README.md new file mode 100644 index 000000000..41136e537 --- /dev/null +++ b/ebike/README.md @@ -0,0 +1,439 @@ +# Improved Software for Dive Xtras Scooters + +![E-Bike Pkg Logo](https://raw.githubusercontent.com/vedderb/vesc_pkg/main/ebike/assets/shark_with_laser.png) + +**Version:** 1.2.1 + +## License + +This software is released under the GPL-3.0 License. See the [LICENSE](https://github.com/vedderb/vesc_pkg/blob/main/LICENSE) file for details. + +## About + +This is a comprehensive, feature-rich package for Dive Xtras scooters (Blacktip and CudaX) running VESC motor controllers. + +This package gives your DPV advanced features like Smart Cruise control, enhanced safety features, and extensive customization options. + +## Supported Hardware + +**Dive Xtras Blacktip:** + +- first generation (Flipsky 4.10 based) +- second generation (Flipsky 6.0 based) +- third generation (Flipsky 6.0 MK5 based, with Bluetooth) + +**Dive Xtras CudaX:** + +- first generation (Flipsky 6.0 based) +- second generation (Flipsky 6.0 MK5 based, with Bluetooth) + +**Scroll down for [installation instructions](#installation).** + +## Instructions + +Some videos showing the basic commands to control Smart Cruise while diving: + +- [basic commands](https://youtu.be/oFRCwuKf2qQ) +- [manually enabling and disabling Smart Cruise](https://youtu.be/riwqB_mttLM) + +--- + +## What's New in Version 1.2.1 + +- Patch fix for startup sound/battery beep overlap: When the battery is not full, the battery status beeps now wait until the startup tune has finished playing, so the two sounds no longer overlap +- Fix for a bug in the Battery Settings dialog, preventing settings from being updated when saving + +## What's New in Version 1.2.0 + +New features and critical bug fix: + +- **Startup Sound**: Added a distinctive musical theme that plays on power-up. When battery is full (>75%, 3 bars), only the startup sound plays; otherwise it's followed by the battery level beeps +- **Critical Smart Cruise Fix**: Fixed a bug where the motor could stop unexpectedly while in Smart Cruise mode with the timer bar still counting down. This occurred when tapping the trigger to reset the timer, due to a missing watchdog reset in the click-counting state + +## Features + +### Smart Cruise Control + +The headline feature of this package—Smart Cruise allows hands-free operation with configurable timeout and visual feedback. + +**Enabling Smart Cruise:** + +- **Triple click while running forward** toggles Smart Cruise on/off +- When active, display shows **"C"** to confirm engagement +- Optionally enable **auto-engage** mode to activate Smart Cruise automatically after maintaining the same speed for a configurable duration + +**Smart Cruise Timer Bar:** + +- Visual countdown indicator using 8 LEDs at the bottom of the display +- LEDs turn off left-to-right as time counts down, showing remaining time at a glance +- When nearing timeout, display changes to **"C?"**, the scooter slows down and plays a distinctive warning beep + +**Speed Adjustments During Smart Cruise:** + +- **Long hold (>0.5s), release, then tap**: Speed down +- **Long hold (>0.5s), release, then double-tap**: Speed up +- **Short tap (no hold)**: Reset timer only +- **Triple tap**: Disable Smart Cruise + +**Smart Cruise Timeout:** + +- Configurable timeout period (default 60 seconds, adjustable via app) +- Warning mode triggers at timeout showing "C?" with slowdown to 80% speed +- Additional 5-second grace period to re-engage with any trigger action +- Completely disables if no action taken after grace period + +### Triple Click Jump Speed + +- **Triple click when stopped** starts the scooter at your preset jump speed +- Default is speed 6 (overdrive), fully customizable via app +- Perfect for quickly getting to your preferred cruising speed + +### Quadruple Click Reverse Gears + +- **Quadruple click (4 clicks) when stopped** enters reverse mode +- Two reverse speeds available: + - **"Untangle"**: Slow speed for carefully freeing tangled lines + - **"Reverse"**: Faster speed for backing out of tight spaces +- Use normal speed shifting (tap/double-tap) to switch between reverse speeds +- Release trigger to stop, double-click to return to forward speeds +- Can be enabled/disabled in the app + +### Slow Speed Restart + +Prevents unexpected speed jumps after stopping in low speeds: + +- If you stop at a speed below your start speed, restart resumes at that same speed +- Ideal for sensitive environments like caves, wrecks, or around marine life +- Shifting to speeds above start speed clears this setting +- Returns to normal start speed behavior + +### Thirds Battery Display + +Professional dive planning tool for managing battery capacity: + +- Hold trigger for 10 seconds to activate (audible warble confirms) +- Display shows three bars representing thirds of remaining capacity +- Audible warning when you've used one-third of capacity +- Perfect for implementing rule-of-thirds dive planning +- Can be reset at any point during the dive to recalculate from current battery level + +### Battery Capacity Beeps + +Audio feedback for battery level when visibility is poor: + +- Enable beeps to hear battery capacity without looking at display +- Helpful in dark or murky water conditions +- Adjustable volume levels (0-5) +- Can be enabled/disabled independently + +### Startup Sound + +A distinctive musical theme plays on power-up to confirm successful initialization: + +- Plays automatically when the scooter is turned on +- When battery is full (3 bars, >75%), only the startup sound plays +- When battery is not full (<75%), the startup sound is followed by battery level beeps +- Volume matches your configured beep volume setting + +### Trigger Click Beeps + +Training and feedback feature for learning click patterns: + +- Each click pattern (single, double, triple, etc.) produces a unique tone +- Single click: 2500 Hz +- Double click: 3000 Hz +- Triple click: 3500 Hz +- Quadruple click: 4000 Hz +- Smart Cruise warning: Distinctive warble pattern +- Helps new divers learn the scooter's control system +- Can be enabled/disabled in the app + +### Speed Ramp Rate + +Customize acceleration characteristics: + +- Adjustable via app to suit your diving style +- Lower values = slower, smoother acceleration (ideal for videography) +- Higher values = faster acceleration (ideal for tech diving) +- Range: 100-10000 ERPM/s (default: 5000) + +### Safe Start + +Enhanced safety feature preventing accidental prop engagement: + +- Detects if prop is blocked during startup +- Stops motor immediately if resistance detected +- Audible beep confirms safe start activation +- Adds zero time to normal startup when prop is clear +- Can be enabled/disabled in the app +- Especially useful when children or untrained individuals might access the scooter + +### Bluetooth App Integration + +Full access to all features via the VESC mobile app on iOS or Android devices: + +- Real-time monitoring of battery, temperature, and RPM +- Configure all settings without connecting to a PC +- Visual interface for speed configuration and feature toggles +- Save settings directly to the scooter + +### Latest VESC Firmware (6.06) Compatibility + +- Always compatible with the latest VESC firmware releases +- Benefits from continuous VESC ecosystem improvements +- Ensures optimal motor control and efficiency +- Silent, smooth operation with latest FOC algorithms + +### Display Features + +- Real-time speed indicator (1-9 for forward speeds) +- Battery percentage display +- "C" indicator when Smart Cruise is active +- "C?" indicator when Smart Cruise nearing expiry +- 8-LED timer bar showing Smart Cruise countdown +- Rotating display support for different mounting orientations +- Battery level indicators (full, thirds mode, percentage) +- Error code display for diagnostics + +## Improvements Over Original Software + +This package includes substantial improvements over the original [V1.50 Dive Xtras Poseidon](https://dive-xtras.zendesk.com/hc/en-us/articles/22561137269908-Reset-Blacktip-Software) software: + +### New Features Added + +- ✅ **Smart Cruise Control** — Complete hands-free cruising system with auto-engage option +- ✅ **Smart Cruise Visual Timer Bar** — 8-LED countdown display showing remaining time +- ✅ **Smart Cruise Warning Mode** — "C?" display and beep when approaching timeout +- ✅ **Refined Speed Control Logic** — Requires long hold before tap to change speed in Smart Cruise mode +- ✅ **Intelligent Beep System** — Warning beeps for important events, silent timer resets +- ✅ **Battery Calculation Method** — Choice between voltage-based or Ah-based calculation +- ✅ **Auto-engage Smart Cruise** — Automatic activation after maintaining constant speed + +### Bug Fixes + +- ✅ **EEPROM Wear Protection** — Prevents unnecessary writes, dramatically extending EEPROM life +- ✅ **Settings Persistence** — All configuration changes properly saved and restored +- ✅ **Display Timeout Handling** — Speed number disappears after timeout but timer bar persists +- ✅ **LED Sequence Correction** — Timer bar LEDs turn off in correct order (left to right) +- ✅ **State Machine Improvements** — More reliable state transitions and click detection +- ✅ **Division-by-Zero Protection** — Guards against invalid timeout configurations +- ✅ **Memory Optimization** — Reduced stack usage and optimized variable management + +### Enhanced Functionality + +- ✅ **Better Click Detection** — Improved timing windows for reliable multi-click recognition +- ✅ **Display Caching** — Reduces I2C traffic for better performance and reliability +- ✅ **Comprehensive Debug Logging** — Easier troubleshooting and development +- ✅ **Code Documentation** — Extensively commented codebase for maintainability +- ✅ **Modular Architecture** — Cleaner separation of concerns for easier updates + +### General Improvements + +- ✅ **Runs on the latest (6.06) VESC release** — Smoother running with latest FOC algorithms, improved safety features + +--- + +## Installation + +### Requirements + +- Dive Xtras Blacktip or CudaX scooter +- the latest 'blacktip\_dpv.vescpkg' file from [GitHub](https://github.com/mikeller/vesc_pkg/releases) or the latest official Package Store in VESC Tool +- VESC Tool (PC) or VESC mobile app (iOS/Android), version 6.06 or higher from [VESC Project](https://vesc-project.com/vesc_tool) +- USB cable (for models without Bluetooth) + +### Installation Steps + +1. **Identify your hardware model:** + +**Blacktip generations:** + +- **First generation** (Flipsky 4.10 based): Has a short USB cable with type A connector at the motor end +- **Second generation** (Flipsky 6.0 based): No USB cable visible, and no Bluetooth capability\* +- **Third generation** (Flipsky 6.0 MK5 based): Supports Bluetooth connectivity + +**CudaX generations:** + +- **First generation** (Flipsky 6.0 based): No Bluetooth capability +- **Second generation** (Flipsky 6.0 MK5 based): Supports Bluetooth connectivity + +2. **Connect to Scooter:** + +**USB:** + +**Blacktip First Generation:** + +- use an 'USB extension' cable to connect the type A connector on the motor end to your PC +- install batteries to power up the scooter + +**Blacktip Second Generation:** + +- remove the four screws holding the metal plate on top of the motor assembly +- carefully lift the metal plate to expose the VESC controller mounted on the underside of it +- remove the rubber plug covering the micro USB port on the VESC +- connect a micro USB cable from the VESC to your PC +- install batteries to power up the scooter + +**CudaX First Generation:** + +- remove the rubber plug covering the micro USB port on the VESC +- connect a micro USB cable from the VESC to your PC +- install batteries to power up the scooter + +**Blacktip Third Generation and CudaX Second Generation:** + +- install batteries to power up the scooter +- use 'Scan BLE' in the first tab of the VESC Tool or the VESC mobile app to find and connect to your scooter via Bluetooth + +3. **Update the VESC firmware:** + +(only needed if the firmware version shown in the VESC Tool or VESC mobile app is below 6.06) + +- **VESC Tool (PC):** + +- Go to the "Firmware" tab +- Click the 'arrow down' button to install the firmware + +- **VESC Mobile App:** + +- Navigate to the firmware section +- Click the "arrow down" icon to install the firmware + +4. **Install Package:** + +- **VESC Tool (PC):** + +- Go to the "VESC Packages" tab, then the "Load Custom" sub-tab +- Click the "load file" icon and select "blacktip\_dpv.vescpkg" +- Click "Install" and wait for completion + +- **VESC Mobile App:** + +- Navigate to the packages section +- Click "..." +- Select "Install Package" +- Choose "blacktip\_dpv.vescpkg" +- Confirm installation + +5. **Reset to defaults:** + +- **This absolutely required after firmware updates or a fresh install.** If you miss it your scooter will not work properly. +- Make sure to check your custom settings before the reset, and then restore them +- In the VESC Tool or Mobile App go to "Settings" / "Scooter Hardware Configuration" +- Make sure the correct model and hardware version is selected +- Click "Reset Defaults" + +6. **Verify Installation:** + +- The scooter should show the startup splash screen to confirm successful installation +- Check that the custom UI appears in the app +- Configure to suit your preferences +- Test basic functionality in a safe environment + +\* this model can be upgraded with a Bluetooth module to enable Bluetooth connectivity, see [these instructions](https://github.com/vedderb/vesc_pkg/blob/main/ebike/BLUETOOTH_UPGRADE.md) + +### First-Time Configuration + +After installation, configure these essential settings: + +1. **Scooter Type:** + +- Select Blacktip or CudaX in the Hardware Configuration + +2. **Battery Settings:** + +- Set battery cell count (typically 10S for 36V systems) +- Configure battery capacity (Ah) +- Choose calculation method (voltage or Ah-based) +- Set cutoff voltages + +3. **Speed Configuration:** + +- Set your preferred start speed (default: 3) +- Configure jump speed (default: 6) +- Adjust individual speed values if needed + +4. **Feature Enable/Disable:** + +- Enable/disable Smart Cruise +- Enable/disable reverse gears +- Enable/disable safe start +- Configure beep preferences + +5. **Smart Cruise Settings:** + +- Set Smart Cruise timeout (default: 60 seconds) +- Enable/disable auto-engage +- Set auto-engage delay if enabled (default: 10 seconds) + +## Configuration + +All settings are accessible through the VESC mobile app or VESC Tool: + +### Speed Settings + +- **Start Speed:** Speed the scooter starts at (1-9) +- **Jump Speed:** Speed for triple-click start (1-9) +- **Individual Speed Values:** Fine-tune each speed's RPM +- **Speed Ramp Rate:** Acceleration rate (100-10000 ERPM/s) + +### Smart Cruise Settings + +- **Enable Smart Cruise:** Toggle the feature on/off +- **Smart Cruise Timeout:** How long before warning mode (5-255 seconds) +- **Auto-engage:** Automatically enable Smart Cruise +- **Auto-engage Delay:** Time at constant speed before auto-engage (5-255 seconds) + +### Reverse Settings + +- **Enable Reverse:** Toggle reverse gears on/off +- **Reverse Speed:** Speed for "Reverse" mode (separate from Untangle) + +### Battery Settings + +- **Battery Type:** Li-ion, LiPo, LiFePO4, etc. +- **Cell Count:** Number of cells in series (typically 10S) +- **Battery Capacity:** Total Ah rating +- **Calculation Method:** Voltage-based or Ah-based +- **Cutoff Voltages:** Start and end cutoff voltages + +### Display & Beeper Settings + +- **Battery Beeps:** Enable/disable capacity beeps +- **Beep Volume:** 0-5 volume level +- **Display Brightness:** Adjust LED brightness +- **Display Rotation:** 0°, 90°, 180°, 270° +- **Trigger Click Beeps:** Enable/disable click feedback beeps + +### Safety Settings + +- **Safe Start:** Enable/disable blocked prop detection + +### Hardware Configuration + +- **Scooter Type:** Blacktip or CudaX (affects display and pinout) + +## Support + +### Getting Help + +For issues, questions, or feature requests: + +- **GitHub Issues:** [Report bugs or request features](https://github.com/mikeller/vesc_pkg/issues) +- **Documentation:** See [DEVELOPMENT.md](https://github.com//vedderb/vesc_pkg/blob/main/ebike/DEVELOPMENT.md) for technical details + +### Before Reporting Issues + +1. Ensure you're running the latest version +2. Check that your VESC firmware is version 6.06 or higher +3. Verify your settings are properly saved +4. Try resetting to default settings to isolate the issue +5. Enable the debug log and check it in VESC Tool (LispBM Scripting tab) + +## Contributing + +Contributions are welcome! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated. + +--- + +**Dive safely and enjoy your enhanced scooter** 🦈⚡ diff --git a/ebike/assets/display_lut.csv b/ebike/assets/display_lut.csv new file mode 100644 index 000000000..5bdf58406 --- /dev/null +++ b/ebike/assets/display_lut.csv @@ -0,0 +1,125 @@ +index,name,rotation,b0,b1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11,b12,b13,b14,b15 +0,Display Battery 1 Bar,0,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,129 +1,Display Battery 1 Bar,1,0,129,0,129,0,0,0,0,0,0,0,0,0,0,0,0 +2,Display Battery 1 Bar,2,0,96,0,96,0,0,0,0,0,0,0,0,0,0,0,0 +3,Display Battery 1 Bar,3,0,0,0,0,0,0,0,0,0,0,0,0,0,96,0,96 +4,Display Battery 2 Bars,0,0,0,0,0,0,0,0,0,0,6,0,6,0,135,0,135 +5,Display Battery 2 Bars,1,0,129,0,129,0,135,0,135,0,0,0,0,0,0,0,0 +6,Display Battery 2 Bars,2,0,120,0,120,0,24,0,24,0,0,0,0,0,0,0,0 +7,Display Battery 2 Bars,3,0,0,0,0,0,0,0,0,0,120,0,120,0,96,0,96 +8,Display Battery 3 Bars,0,0,0,0,0,0,24,0,24,0,30,0,30,0,159,0,159 +9,Display Battery 3 Bars,1,0,129,0,129,0,135,0,135,0,159,0,159,0,0,0,0 +10,Display Battery 3 Bars,2,0,126,0,126,0,30,0,30,0,6,0,6,0,0,0,0 +11,Display Battery 3 Bars,3,0,0,0,0,0,126,0,126,0,120,0,120,0,96,0,96 +12,Display Battery 4 Bars,0,0,96,0,96,0,120,0,120,0,126,0,126,0,255,0,255 +13,Display Battery 4 Bars,1,0,129,0,129,0,135,0,135,0,159,0,159,0,255,0,255 +14,Display Battery 4 Bars,2,0,255,0,255,0,159,0,159,0,135,0,135,0,129,0,129 +15,Display Battery 4 Bars,3,0,255,0,255,0,126,0,126,0,120,0,120,0,96,0,96 +16,Display Reverse,0,0,31,0,63,0,51,0,31,0,15,0,27,0,51,0,51 +17,Display Reverse,1,0,0,0,255,0,255,0,108,0,110,0,251,0,177,0,0 +18,Display Reverse,2,0,51,0,51,0,54,0,60,0,62,0,51,0,63,0,62 +19,Display Reverse,3,0,0,0,99,0,247,0,157,0,141,0,255,0,255,0,0 +20,Display Untangle,0,0,51,0,51,0,51,0,51,0,51,0,51,0,63,0,30 +21,Display Untangle,1,0,0,0,127,0,255,0,129,0,129,0,255,0,127,0,0 +22,Display Untangle,2,0,30,0,63,0,51,0,51,0,51,0,51,0,51,0,51 +23,Display Untangle,3,0,0,0,191,0,255,0,96,0,96,0,255,0,191,0,0 +24,Display One,0,0,12,0,14,0,14,0,12,0,12,0,12,0,12,0,30 +25,Display One,1,0,0,0,0,0,176,0,255,0,255,0,128,0,0,0,0 +26,Display One,2,0,30,0,12,0,12,0,12,0,12,0,28,0,28,0,12 +27,Display One,3,0,0,0,0,0,64,0,255,0,255,0,67,0,0,0,0 +28,Display Two,0,0,30,0,63,0,49,0,24,0,6,0,3,0,63,0,30 +29,Display Two,1,0,0,0,51,0,231,0,229,0,233,0,249,0,49,0,0 +30,Display Two,2,0,30,0,63,0,48,0,24,0,6,0,35,0,63,0,30 +31,Display Two,3,0,0,0,35,0,231,0,229,0,233,0,249,0,51,0,0 +32,Display Three,0,0,30,0,51,0,48,0,28,0,60,0,48,0,51,0,30 +33,Display Three,1,0,0,0,33,0,225,0,204,0,204,0,255,0,55,0,0 +34,Display Three,2,0,30,0,51,0,3,0,15,0,14,0,3,0,51,0,30 +35,Display Three,3,0,0,0,59,0,255,0,204,0,204,0,225,0,33,0,0 +36,Display Four,0,0,24,0,28,0,26,0,25,0,63,0,63,0,24,0,24 +37,Display Four,1,0,0,0,14,0,22,0,38,0,255,0,255,0,6,0,0 +38,Display Four,2,0,6,0,6,0,63,0,63,0,38,0,22,0,14,0,6 +39,Display Four,3,0,0,0,24,0,255,0,255,0,25,0,26,0,28,0,0 +40,Display Five,0,0,30,0,3,0,3,0,31,0,48,0,48,0,51,0,30 +41,Display Five,1,0,0,0,57,0,249,0,200,0,200,0,207,0,7,0,0 +42,Display Five,2,0,30,0,51,0,3,0,3,0,62,0,48,0,48,0,30 +43,Display Five,3,0,0,0,56,0,252,0,196,0,196,0,231,0,39,0,0 +44,Display Six,0,0,30,0,35,0,3,0,31,0,51,0,51,0,51,0,30 +45,Display Six,1,0,0,0,63,0,255,0,200,0,200,0,207,0,39,0,0 +46,Display Six,2,0,30,0,51,0,51,0,51,0,62,0,48,0,49,0,30 +47,Display Six,3,0,0,0,57,0,252,0,196,0,196,0,255,0,63,0,0 +48,Display Seven,0,0,63,0,51,0,48,0,24,0,12,0,12,0,12,0,12 +49,Display Seven,1,0,0,0,96,0,96,0,199,0,207,0,120,0,112,0,0 +50,Display Seven,2,0,12,0,12,0,12,0,12,0,6,0,3,0,51,0,63 +51,Display Seven,3,0,0,0,131,0,135,0,252,0,248,0,129,0,129,0,0 +52,Display Eight,0,0,30,0,33,0,210,0,192,0,210,0,204,0,33,0,30 +53,Display Eight,1,0,30,0,33,0,212,0,194,0,194,0,212,0,33,0,30 +54,Display Eight,2,0,30,0,33,0,204,0,210,0,192,0,210,0,33,0,30 +55,Display Eight,3,0,30,0,33,0,202,0,208,0,208,0,202,0,33,0,30 +56,Display Off,0,0,30,0,33,0,194,0,196,0,200,0,208,0,33,0,30 +57,Display Off,1,0,30,0,33,0,194,0,196,0,200,0,208,0,33,0,30 +58,Display Off,2,0,30,0,33,0,194,0,196,0,200,0,208,0,33,0,30 +59,Display Off,3,0,30,0,33,0,208,0,200,0,196,0,194,0,33,0,30 +60,Display Startup,0,0,12,0,45,0,204,0,204,0,204,0,192,0,33,0,30 +61,Display Startup,1,0,30,0,33,0,128,0,252,0,252,0,128,0,33,0,30 +62,Display Startup,2,0,30,0,33,0,192,0,204,0,204,0,204,0,45,0,12 +63,Display Startup,3,0,30,0,33,0,64,0,207,0,207,0,64,0,33,0,30 +64,Display Custom ?,0,0,0,0,19,0,168,0,160,0,144,0,128,0,19,0,0 +65,Display Custom ?,1,0,30,0,33,0,33,0,0,0,16,0,37,0,24,0,0 +66,Display Custom ?,2,0,0,0,50,0,64,0,66,0,65,0,69,0,50,0,0 +67,Display Custom ?,3,0,0,0,6,0,41,0,2,0,0,0,33,0,33,0,30 +68,Display Custom,0,0,28,0,54,0,3,0,3,0,3,0,3,0,54,0,28 +69,Display Custom,1,0,0,0,30,0,63,0,225,0,192,0,225,0,33,0,0 +70,Display Custom,2,0,14,0,27,0,48,0,48,0,48,0,48,0,27,0,14 +71,Display Custom,3,0,0,0,33,0,225,0,192,0,225,0,63,0,30,0,0 +72,Display 3rds 1 Bar,0,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,129 +73,Display 3rds 1 Bar,1,0,129,0,129,0,0,0,0,0,0,0,0,0,0,0,0 +74,Display 3rds 1 Bar,2,0,96,0,96,0,0,0,0,0,0,0,0,0,0,0,0 +75,Display 3rds 1 Bar,3,0,0,0,0,0,0,0,0,0,0,0,0,0,96,0,96 +76,Display 3rds 2 Bars,0,0,0,0,0,0,0,0,12,0,12,0,12,0,141,0,141 +77,Display 3rds 2 Bars,1,0,129,0,129,0,0,0,143,0,143,0,0,0,0,0,0 +78,Display 3rds 2 Bars,2,0,108,0,108,0,12,0,12,0,12,0,0,0,0,0,0 +79,Display 3rds 2 Bars,3,0,0,0,0,0,0,0,124,0,124,0,0,0,96,0,96 +80,Display 3rds 3 Bars,0,0,96,0,96,0,96,0,108,0,108,0,108,0,237,0,237 +81,Display 3rds 3 Bars,1,0,129,0,129,0,0,0,143,0,143,0,0,0,255,0,255 +82,Display 3rds 3 Bars,2,0,237,0,237,0,141,0,141,0,141,0,129,0,129,0,129 +83,Display 3rds 3 Bars,3,0,255,0,255,0,0,0,124,0,124,0,0,0,96,0,96 +84,Display 10 Percent,0,0,35,0,82,0,82,0,82,0,82,0,82,0,82,0,167 +85,Display 10 Percent,1,0,128,0,192,0,255,0,128,0,0,0,63,0,192,0,63 +86,Display 10 Percent,2,0,121,0,146,0,146,0,146,0,146,0,146,0,146,0,49 +87,Display 10 Percent,3,0,63,0,192,0,63,0,0,0,64,0,255,0,192,0,64 +88,Display 20 Percent,0,0,163,0,84,0,84,0,84,0,83,0,208,0,208,0,39 +89,Display 20 Percent,1,0,67,0,196,0,196,0,184,0,0,0,63,0,192,0,63 +90,Display 20 Percent,2,0,57,0,194,0,194,0,178,0,138,0,138,0,138,0,113 +91,Display 20 Percent,3,0,63,0,192,0,63,0,0,0,71,0,200,0,200,0,176 +92,Display 30 Percent,0,0,163,0,84,0,84,0,83,0,84,0,84,0,84,0,163 +93,Display 30 Percent,1,0,192,0,200,0,200,0,55,0,0,0,63,0,192,0,63 +94,Display 30 Percent,2,0,113,0,138,0,138,0,138,0,178,0,138,0,138,0,113 +95,Display 30 Percent,3,0,63,0,192,0,63,0,0,0,59,0,196,0,196,0,192 +96,Display 40 Percent,0,0,34,0,83,0,210,0,210,0,215,0,82,0,82,0,34 +97,Display 40 Percent,1,0,28,0,36,0,255,0,4,0,0,0,63,0,192,0,63 +98,Display 40 Percent,2,0,17,0,146,0,146,0,250,0,210,0,210,0,178,0,17 +99,Display 40 Percent,3,0,63,0,192,0,63,0,0,0,8,0,255,0,9,0,14 +100,Display 50 Percent,0,0,167,0,208,0,208,0,83,0,84,0,84,0,84,0,163 +101,Display 50 Percent,1,0,240,0,200,0,200,0,71,0,0,0,63,0,192,0,63 +102,Display 50 Percent,2,0,113,0,138,0,138,0,138,0,178,0,194,0,194,0,121 +103,Display 50 Percent,3,0,63,0,192,0,63,0,0,0,184,0,196,0,196,0,195 +104,Display 60 Percent,0,0,39,0,208,0,208,0,211,0,212,0,212,0,212,0,35 +105,Display 60 Percent,1,0,63,0,200,0,200,0,71,0,0,0,63,0,192,0,63 +106,Display 60 Percent,2,0,49,0,202,0,202,0,202,0,242,0,194,0,194,0,57 +107,Display 60 Percent,3,0,63,0,192,0,63,0,0,0,184,0,196,0,196,0,63 +108,Display 70 Percent,0,0,167,0,84,0,84,0,83,0,82,0,81,0,81,0,160 +109,Display 70 Percent,1,0,192,0,75,0,76,0,112,0,0,0,63,0,192,0,63 +110,Display 70 Percent,2,0,65,0,162,0,162,0,146,0,178,0,138,0,138,0,121 +111,Display 70 Percent,3,0,63,0,192,0,63,0,0,0,131,0,140,0,180,0,192 +112,Display 80 Percent,0,0,35,0,212,0,212,0,212,0,83,0,212,0,212,0,35 +113,Display 80 Percent,1,0,59,0,196,0,196,0,59,0,0,0,63,0,192,0,63 +114,Display 80 Percent,2,0,49,0,202,0,202,0,178,0,202,0,202,0,202,0,49 +115,Display 80 Percent,3,0,63,0,192,0,63,0,0,0,55,0,200,0,200,0,55 +116,Display 90 Percent,0,0,35,0,212,0,212,0,87,0,84,0,84,0,84,0,163 +117,Display 90 Percent,1,0,176,0,200,0,200,0,63,0,0,0,63,0,192,0,63 +118,Display 90 Percent,2,0,113,0,138,0,138,0,138,0,186,0,202,0,202,0,49 +119,Display 90 Percent,3,0,63,0,192,0,63,0,0,0,63,0,196,0,196,0,67 +120,Display 100 Percent,0,0,227,0,144,0,144,0,147,0,144,0,144,0,144,0,224 +121,Display 100 Percent,1,0,255,0,72,0,72,0,0,0,0,0,63,0,192,0,192 +122,Display 100 Percent,2,0,193,0,66,0,66,0,66,0,114,0,66,0,66,0,241 +123,Display 100 Percent,3,0,192,0,192,0,63,0,0,0,0,0,132,0,132,0,255 diff --git a/ebike/ebike.lbm b/ebike/ebike.lbm new file mode 100644 index 000000000..f24aee7d9 --- /dev/null +++ b/ebike/ebike.lbm @@ -0,0 +1,1783 @@ +; ============================================================================= +; Constants +; ============================================================================= + +; Sleep intervals (seconds) - controls loop frequencies +(define SLEEP_STATE_MACHINE 0.02) ; 50Hz - button state polling +(define SLEEP_MOTOR_CONTROL 0.04) ; 25Hz - motor/GPIO polling +(define SLEEP_MOTOR_SPEED_CHANGE 0.25) ; 4Hz - motor speed transitions +(define SLEEP_UI_UPDATE 0.25) ; 4Hz - display/beeper updates +(define SLEEP_BACKGROUND_CHECK 0.5) ; 2Hz - Smart Cruise checking +(define SLEEP_BATTERY_STABILIZE 1.0) ; 1Hz - one-time battery reading delay + +; Timer durations (seconds) +(define TIMER_DISABLED 86400) ; 24 hours - effectively infinite for scooter operation +(define TIMER_CLICK_WINDOW 0.3) ; Click detection window +(define TIMER_RELEASE_WINDOW 0.5) ; Release detection window +(define TIMER_SMART_CRUISE_TIMEOUT 5) ; Smart Cruise half-enable timeout +(define TIMER_SMART_CRUISE_HOLD 0.5) ; Hold duration before Smart Cruise adjustments +(define TIMER_DISPLAY_DURATION 5) ; Display duration (used in calculations) +(define TIMER_LONG_PRESS 10) ; Long press duration for special functions + +; Thread stack sizes (in 4-byte words) for loopwhile-thd and spawn +; Reduced to minimum safe values to conserve memory +(define THREAD_STACK_GPIO 80) ; GPIO reading - minimal needs +(define THREAD_STACK_SMART_CRUISE 100) ; Smart Cruise - reduced +(define THREAD_STACK_STATE_MACHINE 80) ; State 2 (pressed) - reduced +(define THREAD_STACK_STATE_TRANSITIONS 80) ; States 0, 3 - reduced +(define THREAD_STACK_STATE_COUNTING 80) ; State 1 (counting clicks) - reduced +(define THREAD_STACK_MOTOR 150) ; Motor control - reduced but still largest +(define THREAD_STACK_DISPLAY 100) ; Display updates - reduced +(define THREAD_STACK_BATTERY 100) ; Battery display - reduced +(define THREAD_STACK_CLICK_BEEP 80) ; Click beep playback - reduced + +; State values +(define STATE_UNINITIALIZED -1) +(define STATE_OFF 0) +(define STATE_COUNTING_CLICKS 1) +(define STATE_PRESSED 2) +(define STATE_GOING_OFF 3) + +; Special speed values +(define SPEED_REVERSE_2 0) ; Reverse speed level 2 (strong reverse) +(define SPEED_UNTANGLE 1) ; Reverse speed level 1 / untangle assist +(define SPEED_OFF 99) ; Motor off indicator +(define SPEED_REVERSE_THRESHOLD 2) ; Speeds below this are reverse +(define SPEED_SOFT_START_SENTINEL 0.5) ; Sentinel value for soft start tracking + +; Click counts +(define CLICKS_SINGLE 1) +(define CLICKS_DOUBLE 2) +(define CLICKS_TRIPLE 3) +(define CLICKS_QUADRUPLE 4) +(define CLICKS_SMART_CRUISE_CHANGE 5) + +; Smart Cruise states +(define SMART_CRUISE_OFF 0) +(define SMART_CRUISE_HALF_ENABLED 1) +(define SMART_CRUISE_FULLY_ENABLED 2) +(define SMART_CRUISE_AUTO_ENGAGED 3) + +; Hardware configuration thresholds +(define HARDWARE_BLACKTIP_MAX 2) ; Hardware configs 0-2 are Blacktip + +; Scooter types (indices into hardware lists) +(define SCOOTER_BLACKTIP 0) +(define SCOOTER_CUDAX 1) + +; Motor control constants +(define MAX_ERPM_BLACKTIP 4100) +(define MAX_ERPM_CUDAX 7100) +(define MAX_CURRENT_BLACKTIP 22.8) +(define MAX_CURRENT_CUDAX 46) +(define MIN_CURRENT_BLACKTIP 1.7) +(define MIN_CURRENT_CUDAX 0.35) + +; RPM scaling +(define RPM_PERCENT_DENOMINATOR 100) + +; Safe start parameters +(define SAFE_START_DUTY 0.06) ; Initial duty cycle for soft start +(define SAFE_START_TIMEOUT 0.5) ; Timeout for safe start checks +(define SAFE_START_TIMEOUT_GRACE 0.1) ; Additional grace period before aborting (seconds) +(define SAFE_START_MIN_RPM 350) ; Minimum RPM for safe start success +(define SAFE_START_MIN_DUTY 0.05) ; Minimum duty for safe start check +(define SAFE_START_MAX_CURRENT 5) ; Maximum current during safe start spin-up +(define SAFE_START_FAIL_CURRENT 8) ; Current threshold for safe start failure +(define SAFE_START_MAX_RETRIES 3) ; Max safe start retries before shutting down +(define SAFE_START_RETRY_BACKOFF 0.2) ; Delay before retrying safe start (seconds) + +; Soft-start duration (how long to hold reduced current before restoring) +(define SOFT_START_DURATION SAFE_START_TIMEOUT) + +; Smart Cruise speed adjustment (slowdown to 80%) +(define SMART_CRUISE_SLOWDOWN_DIVISOR 125) ; Divide by 125 instead of 100 for 80% + +; Display offset (speed value to display number mapping) +(define DISPLAY_SPEED_OFFSET 4) + +; Display numbers for special screens +(define DISPLAY_OFF 14) +(define DISPLAY_SMART_CRUISE_HALF 16) +(define DISPLAY_SMART_CRUISE_FULL 17) +(define DISPLAY_SENTINEL 99) ; Sentinel for "no previous display" + +; Warbler beep parameters +(define WARBLER_FREQUENCY 450) +(define WARBLER_DURATION 0.2) + +; Display timing calculations (from State 2 repeat display) +(define DISPLAY_REPEAT_FIRST 6) ; display duration + 1 +(define DISPLAY_REPEAT_SECOND 12) ; 2 * display duration + 2 + +; Lookup table for brightness values +(define BRIGHTNESS_LUT (list 224 227 230 233 236 239)) + +; Masks for 0..8 LEDs lit (ordered per hardware bit mapping) +(define TIMER_BAR_MASKS (list 0x00 0x40 0x60 0x70 0x78 0x7C 0x7E 0x7F 0xFF)) + +; EEPROM settings buffer size +(define EEPROM_SETTINGS_COUNT 30) + +; Battery polynomial coefficients (for voltage-based calculation) +(define BATTERY_COEFF_4 4.3867) +(define BATTERY_COEFF_3 -6.7072) +(define BATTERY_COEFF_2 2.4021) +(define BATTERY_COEFF_1 1.3619) + +; Data receive handshake code +(define HANDSHAKE_CODE 255) + +; Display timer stop value +(define DISPLAY_TIMER_STOP 2) + +; Settle time after startup before allowing more beeper indications (e.g. thirds warning) +(define STARTUP_TUNE_SETTLE 1.0) + +; Display LUT binary format helpers (init-only, not moved to flash) +(defun validate_lut_header (data magic expected_version) +{ + (var file_magic (bufget-u32 data 0 'little-endian)) + (var file_version (bufget-u16 data 4 'little-endian)) + (var num_items (bufget-u16 data 6 'little-endian)) + (if (!= file_magic magic) + nil + (if (!= file_version expected_version) + nil + num_items + ) + ) +}) + +(defun load_lookup_tables () +{ + ; Import display and brightness lookup tables from binary files + ; These files are generated at build time from CSV sources + (import "generated/display_lut.bin" 'display_lut_bin) + + ; Initialize display LUT (returns number of frames or nil on error) + (var display_num_frames (validate_lut_header display_lut_bin 0x4C555444u32 1)) + + ; Verify LUTs loaded successfully - halt if validation fails + (if (not display_num_frames) + (exit-error "LUT validation failed: display")) +}) + +; EEPROM initialization (init-only, not moved to flash) +(defun eeprom_set_defaults () +{ + (if (not-eq (eeprom-read-i 127) (to-i32 1)) { + (puts "EEPROM: Initializing defaults for 1.0.0") + ; Check for current version marker (ebike release 1.0.0) + ; New settings added in version 1.0.0 + (eeprom_store_i_if_changed 25 0) ; Enable Auto-Engage Smart Cruise. 1=On 0=Off + (eeprom_store_i_if_changed 26 10) ; Auto-Engage Time in seconds (5-30 seconds) + (eeprom_store_i_if_changed 27 0) ; Enable Thirds warning on from power-up. 1=On 0=Off + (eeprom_store_i_if_changed 28 0) ; Battery calculation method: 0=Voltage-based, 1=Ampere-hour based + (eeprom_store_i_if_changed 29 0) ; Enable Debug Logging. 1=On 0=Off + + (if (not-eq (eeprom-read-i 127) (to-i32 150)) { + (puts "EEPROM: No previous version detected, setting all defaults") + ; Check for previous version marker (Dive Xtras V1.50 'Poseidon') + ; User speeds, ie 1 thru 8 are only used in the GUI, this lisp code uses speeds 0-9 with 0 & 1 being the 2 reverse speeds. + ; 99 is used as the "off" speed + (eeprom_store_i_if_changed 0 45) ; Reverse Speed 2 % + (eeprom_store_i_if_changed 1 20) ; Untangle Speed 1 % + (eeprom_store_i_if_changed 2 30) ; Speed 1 % + (eeprom_store_i_if_changed 3 38) ; Speed 2 % + (eeprom_store_i_if_changed 4 46) ; Speed 3 % + (eeprom_store_i_if_changed 5 54) ; Speed 4 % + (eeprom_store_i_if_changed 6 62) ; Speed 5 % + (eeprom_store_i_if_changed 7 70) ; Speed 6 % + (eeprom_store_i_if_changed 8 78) ; Speed 7 % + (eeprom_store_i_if_changed 9 100) ; Speed 8 % + (eeprom_store_i_if_changed 10 9) ; Maximum number of Speeds to use, must be greater or equal to start_speed (actual speed #, not user speed) + (eeprom_store_i_if_changed 11 4) ; Speed the scooter starts in. Range 2-9, must be less or equal to the max_speed_no (actual speed #, not user speed) + (eeprom_store_i_if_changed 12 7) ; Speed to jump to on triple click, (actual speed #, not user speed) + (eeprom_store_i_if_changed 13 1) ; Turn safe start on or off 1=On 0=Off + (eeprom_store_i_if_changed 14 0) ; Enable Reverse speed. 1=On 0=Off + (eeprom_store_i_if_changed 15 0) ; Enable Smart Cruise (3 clicks while running). 1=On 0=Off + (eeprom_store_i_if_changed 16 60) ; How long before Smart Cruise times out and requires reactivation in sec. + (eeprom_store_i_if_changed 17 0) ; rotation of Display, 0-3 . Each number rotates display 90 deg. + (eeprom_store_i_if_changed 18 5) ; Display Brighness 0-5 + (eeprom_store_i_if_changed 19 0) ; Hardware configuration, 0 = Blacktip HW60 + Ble, 1 = Blacktip HW60 - Ble, 2 = Blacktip HW410 - Ble, 3 = Cuda-X HW60 + Ble, 4 = Cuda-X HW60 - Ble + (eeprom_store_i_if_changed 20 0) ; Battery Beeps + (eeprom_store_i_if_changed 21 3) ; Beep Volume + (eeprom_store_i_if_changed 22 0) ; CudaX Flip Screens + (eeprom_store_i_if_changed 23 0) ; 2nd Screen rotation of Display, 0-3 . Each number rotates display 90 deg. + (eeprom_store_i_if_changed 24 0) ; Trigger Click Beeps + }) + ; Mark as initialised for 1.0.0 + (eeprom_store_i_if_changed 127 1) ; indicate that the defaults have been applied + (puts "EEPROM: Defaults initialized successfully") + }) +}) + + +; Helper function to reduce EEPROM wear by only writing when value changes +(defun eeprom_store_i_if_changed (addr new_val) +{ + (var current_val (eeprom-read-i addr)) + (if (or (eq current_val nil) (!= current_val new_val)) + (eeprom-store-i addr new_val) + ) +}) + +(move-to-flash eeprom_store_i_if_changed) + + +; ============================================================================= +; Safe Start Helpers +; ============================================================================= +; Canonical values for `safe_start_status` used throughout the codebase: +; - 'idle : not running / feature disabled +; - 'running : safe-start attempt in progress +; - 'success : safe-start completed successfully +; - 'failed : safe-start attempt failed (may retry) +; Keep checks limited to these symbols and use `soft_start_active` boolean for +; soft-start vs safe-start distinctions when needed. +; ============================================================================= + +; Helper to set safe_start_status and emit a concise debug log when it changes +(defun safe_start_set_status (new_status) +{ + (var old_status safe_start_status) + (setvar 'safe_start_status new_status) + (if (and (not-eq debug_enabled nil) (= debug_enabled 1) (not-eq old_status new_status)) { + (debug_log_format (str-merge "SafeStart: " (to-str old_status) " -> " (to-str new_status))) + }) +}) + +(move-to-flash safe_start_set_status) + +; Helper to set soft_start_active (silent - no logging to reduce noise) +(defun soft_start_set_active (val) +{ + (setvar 'soft_start_active val) +}) + +(move-to-flash soft_start_set_active) + +(defun safe_start_reset_state () +{ + (setvar 'safe_start_timer 0) + (setvar 'safe_start_attempt_speed SPEED_OFF) + (setvar 'safe_start_failures 0) + (safe_start_set_status 'idle) + (soft_start_set_active 0) +}) + +(move-to-flash safe_start_reset_state) + +(defun safe_start_begin (target_speed) +{ + ; Only perform safe-start initialization when the feature is enabled. + (if (= use_safe_start 1) { + (setvar 'safe_start_timer (systime)) + (setvar 'safe_start_attempt_speed target_speed) + (safe_start_set_status 'running) + (debug_log (str-merge "Motor: Safe start attempt " (to-str (+ safe_start_failures 1)) " targeting speed " (to-str (to-i target_speed)))) + } { + ; Feature disabled: set status to 'idle so callers treat it as not running + (safe_start_set_status 'idle) + (setvar 'safe_start_timer 0) + (setvar 'safe_start_attempt_speed SPEED_OFF) + }) +}) + +(move-to-flash safe_start_begin) + +(defun safe_start_success () +{ + (setvar 'safe_start_timer 0) + (setvar 'safe_start_attempt_speed SPEED_OFF) + (setvar 'safe_start_failures 0) + (safe_start_set_status 'success) +}) + +(move-to-flash safe_start_success) + +(defun safe_start_increment_failure (reason) +{ + (setvar 'safe_start_failures (+ safe_start_failures 1)) + (safe_start_set_status 'failed) + (if (and (not-eq debug_enabled nil) (= debug_enabled 1)) { + (debug_log (str-merge "Motor: Safe start attempt " (to-str safe_start_failures) "/" (to-str SAFE_START_MAX_RETRIES) " failed (" reason ")")) + }) +}) + +(move-to-flash safe_start_increment_failure) + +(defun safe_start_should_retry () +{ + (< safe_start_failures SAFE_START_MAX_RETRIES) +}) + +(move-to-flash safe_start_should_retry) + +(defun safe_start_abort_with_reason (reason) +{ + (safe_start_increment_failure reason) + (if (safe_start_should_retry) { + (sleep SAFE_START_RETRY_BACKOFF) + (safe_start_begin safe_start_attempt_speed) + } { + (if (and (not-eq debug_enabled nil) (= debug_enabled 1)) { + (debug_log (str-merge "Motor: Safe start retries exhausted, stopping motor (reason=" reason ")")) + }) + (set_speed_safe SPEED_OFF) + (state_transition_to STATE_COUNTING_CLICKS "safe_start_abort" THREAD_STACK_STATE_COUNTING state_handler_counting_clicks) + (foc-beep 250 0.15 5) + (safe_start_reset_state) + }) +}) + +(move-to-flash safe_start_abort_with_reason) + +(defun safe_start_value_valid (value max_abs) +{ + (and (= value value) (< (abs value) max_abs)) +}) + +(move-to-flash safe_start_value_valid) + +(defun safe_start_telemetry_valid (rpm duty current) +{ + (and (safe_start_value_valid rpm 20000) + (safe_start_value_valid duty 1.0) + (safe_start_value_valid current 200)) +}) + +(move-to-flash safe_start_telemetry_valid) + +(defun safe_start_met_success_criteria (rpm duty current) +{ + (and (> (abs rpm) SAFE_START_MIN_RPM) + (> (abs duty) SAFE_START_MIN_DUTY) + (< (abs current) SAFE_START_MAX_CURRENT)) +}) + +(move-to-flash safe_start_met_success_criteria) + +; Settings initialization (init-only, not moved to flash) +(defun update_settings_from_eeprom () +{ + (setvar 'max_speed_no (eeprom-read-i 10)) + (setvar 'start_speed (eeprom-read-i 11)) + (setvar 'jump_speed (eeprom-read-i 12)) + (setvar 'use_safe_start (eeprom-read-i 13)) + (setvar 'enable_reverse (eeprom-read-i 14)) + (setvar 'enable_smart_cruise (eeprom-read-i 15)) + (setvar 'smart_cruise_timeout (eeprom-read-i 16)) + (setvar 'rotation (eeprom-read-i 17)) + (setvar 'disp_brightness (eeprom-read-i 18)) + (setvar 'hardware_configuration (eeprom-read-i 19)) + (setvar 'enable_battery_beeps (eeprom-read-i 20)) + (setvar 'beeps_vol (eeprom-read-i 21)) + (setvar 'cudax_flip (eeprom-read-i 22)) + (setvar 'rotation2 (eeprom-read-i 23)) + (setvar 'enable_trigger_beeps (eeprom-read-i 24)) + (setvar 'enable_smart_cruise_auto_engage (eeprom-read-i 25)) + (setvar 'smart_cruise_auto_engage_time (eeprom-read-i 26)) + (setvar 'enable_thirds_warning_startup (eeprom-read-i 27)) + (setvar 'battery_calculation_method (eeprom-read-i 28)) + (setvar 'debug_enabled (eeprom-read-i 29)) + + (setvar 'speed_set (list + (eeprom-read-i 0) ; Reverse Speed 2 % + (eeprom-read-i 1) ; Untangle Speed 1 % + (eeprom-read-i 2) ; Speed 1 % + (eeprom-read-i 3) ; Speed 2 % + (eeprom-read-i 4) ; Speed 3 % + (eeprom-read-i 5) ; Speed 4 % + (eeprom-read-i 6) ; Speed 5 % + (eeprom-read-i 7) ; Speed 6 % + (eeprom-read-i 8) ; Speed 7 % + (eeprom-read-i 9) ; Speed 8 % + )) + + ; Sets scooter type, 0 = Blacktip, 1 = Cuda X + (if (<= hardware_configuration HARDWARE_BLACKTIP_MAX) + (setvar 'scooter_type SCOOTER_BLACKTIP) + (setvar 'scooter_type SCOOTER_CUDAX) + ) +}) + + +(defun log_startup () +{ + ; Log configuration on startup + (if (and (not-eq debug_enabled nil) (= debug_enabled 1)) { + (debug_log_format (str-merge "Startup, configuration:" + "\n- hardware_configuration: " (to-str (to-i hardware_configuration)) + "\n- scooter_type: " (if (= scooter_type SCOOTER_BLACKTIP) + "Blacktip" + "Cuda X" + ) + "\n- debug_enabled: " (to-str (to-i debug_enabled)) + "\n" + )) + (log_settings_1) + (log_settings_2) + (log_speeds) + (gc) + } { + (puts "Startup") + }) +}) + + +(defun log_settings_1 () +{ + (debug_log_format (str-merge "- max_speed_no: " (to-str (to-i max_speed_no)) + "\n- start_speed: " (to-str (to-i start_speed)) + "\n- jump_speed: " (to-str (to-i jump_speed)) + "\n- use_safe_start: " (to-str (to-i use_safe_start)) + "\n- enable_reverse: " (to-str (to-i enable_reverse)) + "\n- enable_smart_cruise: " (to-str (to-i enable_smart_cruise)) + "\n- smart_cruise_timeout: " (to-str (to-i smart_cruise_timeout)) + "\n- rotation: " (to-str (to-i rotation)) + "\n- disp_brightness: " (to-str (to-i disp_brightness)) + "\n- enable_battery_beeps: " (to-str (to-i enable_battery_beeps)) + "\n- beeps_vol: " (to-str (to-i beeps_vol)) + "\n- cudax_flip: " (to-str (to-i cudax_flip)) + "\n- rotation2: " (to-str (to-i rotation2)) + "\n- enable_trigger_beeps: " (to-str (to-i enable_trigger_beeps)) + )) +}) + + +(defun log_settings_2 () +{ + (debug_log_format (str-merge "- enable_smart_cruise_auto_engage: " (to-str (to-i enable_smart_cruise_auto_engage)) + "\n- smart_cruise_auto_engage_time: " (to-str (to-i smart_cruise_auto_engage_time)) + "\n- enable_thirds_warning_startup: " (to-str (to-i enable_thirds_warning_startup)) + "\n- battery_calculation_method: " (to-str (to-i battery_calculation_method)) + )) +}) + + +(defun log_speeds () +{ + (debug_log_format (str-merge "- speed (reverse): " (to-str (to-i (ix speed_set 0))) + "\n- speed (untangle): " (to-str (to-i (ix speed_set 1))) + "\n- speed (1): " (to-str (to-i (ix speed_set 2))) + "\n- speed (2): " (to-str (to-i (ix speed_set 3))) + "\n- speed (3): " (to-str (to-i (ix speed_set 4))) + "\n- speed (4): " (to-str (to-i (ix speed_set 5))) + "\n- speed (5): " (to-str (to-i (ix speed_set 6))) + "\n- speed (6): " (to-str (to-i (ix speed_set 7))) + "\n- speed (7): " (to-str (to-i (ix speed_set 8))) + "\n- speed (8): " (to-str (to-i (ix speed_set 9))) + )) +}) + + +; Debug logging helper function +(defun debug_log (msg) +{ + (if (and (not-eq debug_enabled nil) (= debug_enabled 1)) + (puts msg) + ) +}) + +(move-to-flash debug_log) + +; Lightweight macro to conditionally evaluate debug logging expressions +; Only evaluates the logging expression when debug_enabled is 1 +; This prevents expensive str-merge and to-str calls on memory-constrained targets +(define debug_log_format (macro (expr) + `(if (and (not-eq debug_enabled nil) (= debug_enabled 1)) + (puts ,expr) + ) +)) + + +(defun calculate_corrected_battery () +{ + ; Calculate corrected battery percentage from raw battery reading + (var raw_batt (get-batt)) + (+ (* BATTERY_COEFF_4 raw_batt raw_batt raw_batt raw_batt) + (* BATTERY_COEFF_3 raw_batt raw_batt raw_batt) + (* BATTERY_COEFF_2 raw_batt raw_batt) + (* BATTERY_COEFF_1 raw_batt) + ) +}) + +(move-to-flash calculate_corrected_battery) + +(defun calculate_ah_based_battery () +{ + ; Calculate battery percentage based on ampere-hours used vs total capacity + (var total-capacity (conf-get 'si-battery-ah)) + (var used-ah (get-ah)) + (var remaining_capacity (- 1.0 (/ used-ah total-capacity))) + (if (and (> total-capacity 0) (> remaining_capacity 0)) + remaining_capacity + 0.0 + ) +}) + +(move-to-flash calculate_ah_based_battery) + +(defun get_battery_level () + ; Get battery level using the configured calculation method + (if (= battery_calculation_method 1) + (calculate_ah_based_battery) + (calculate_corrected_battery) + ) +) + +(move-to-flash get_battery_level) + +(defun receive_data (data) +{ + (if (= (bufget-u8 data 0) HANDSHAKE_CODE) { ; Handshake to trigger data send if not yet received. + (var setbuf (array-create EEPROM_SETTINGS_COUNT)) ; create a temp array to store setting + (looprange i 0 EEPROM_SETTINGS_COUNT + (bufset-i8 setbuf i (or (eeprom-read-i i) 0))) + (send-data setbuf) + } { + ; For non-handshake messages, validate buffer size + (if (< (buflen data) EEPROM_SETTINGS_COUNT) { + (debug_log_format (str-merge "Error: Received data buffer too small: " (to-str (buflen data)) " < " (to-str EEPROM_SETTINGS_COUNT))) + nil ; Return early on invalid data + } { + (looprange i 0 EEPROM_SETTINGS_COUNT + (eeprom_store_i_if_changed i (bufget-u8 data i))) ; writes settings to eeprom + (update_settings_from_eeprom) ; updates actual settings in lisp + (debug_log "Settings updated") + }) + }) +}) + +(move-to-flash receive_data) + +; Setup functions (init-only, not moved to flash) +(defun setup_event_handler () +{ + (defun event_handler () + { + (loopwhile t + (recv + ((event-data-rx . (? data)) (receive_data data)) + (_ nil)) + ) + }) + + (event-register-handler (spawn event_handler)) + (event-enable 'event-data-rx) +}) + +(defun start_trigger_loop () +{ + (gpio-configure 'pin-ppm 'pin-mode-in-pd) + + (loopwhile-thd THREAD_STACK_GPIO t { + (sleep SLEEP_MOTOR_CONTROL) + (if (= 1 (gpio-read 'pin-ppm)) + (setvar 'sw_pressed 1) + (setvar 'sw_pressed 0) + ) + }) +}) + +(defun start_smart_cruise_loop () +{ + (debug_log "Smart Cruise: Starting loop") + + (var speed_setting_timer 0) ; Timer for auto-engage functionality + (var last_speed_setting SPEED_OFF) ; Track last speed setting for auto-engage + + (loopwhile-thd THREAD_STACK_SMART_CRUISE t { + (sleep SLEEP_BACKGROUND_CHECK) + (if (and (> enable_smart_cruise 0) (> enable_smart_cruise_auto_engage 0) (= sw_state STATE_PRESSED) (= smart_cruise SMART_CRUISE_OFF) (!= speed SPEED_OFF) (>= speed SPEED_REVERSE_THRESHOLD)) { + ; Check if speed setting has changed + (if (!= speed last_speed_setting) { + (setvar 'last_speed_setting speed) + (setvar 'speed_setting_timer (systime)) + } { + ; Speed setting hasn't changed, check if timer expired + (if (> (secs-since speed_setting_timer) smart_cruise_auto_engage_time) { + (debug_log "Smart Cruise: Auto-engaged") + (setvar 'smart_cruise SMART_CRUISE_AUTO_ENGAGED) + (setvar 'timer_start (systime)) + (setvar 'disp_num DISPLAY_SMART_CRUISE_FULL) + (setvar 'click_beep CLICKS_SMART_CRUISE_CHANGE) + ; re command actual speed as reverification sets it to 0.8x + (set-rpm (calculate_rpm speed RPM_PERCENT_DENOMINATOR)) + }) + }) + } { + ; Not in the right state for auto-engage, reset timer + (setvar 'speed_setting_timer (systime)) + }) + }) +}) + + +(defun state_metrics_reset () +{ + ; State machine tracking (minimal for memory conservation) + (define state_last_state STATE_UNINITIALIZED) + (define state_last_change_time 0) + (define state_last_reason "") + + (setvar 'state_last_state STATE_UNINITIALIZED) + (setvar 'state_last_change_time (systime)) + (setvar 'state_last_reason "startup") +}) + +(defun state_record_transition (from_state to_state reason) +{ + (setvar 'state_last_state to_state) + (setvar 'state_last_change_time (systime)) + (setvar 'state_last_reason reason) + (debug_log_format (str-merge "State: " (to-str from_state) "->" (to-str to_state) " " reason)) +}) + +(defun state_transition_to (new_state reason thread_stack handler) +{ + (state_record_transition + (if (= state_last_state STATE_UNINITIALIZED) STATE_UNINITIALIZED sw_state) + new_state + reason) + (setvar 'sw_state new_state) + (spawn thread_stack handler) +}) + +(move-to-flash state_metrics_reset) +(move-to-flash state_record_transition) +(move-to-flash state_transition_to) + +; ============================================================================= +; RPM Calculation Helper +; ============================================================================= + +(defun clamp (value min_val max_val) +{ + (cond + ((< value min_val) min_val) + ((> value max_val) max_val) + (t value) + ) +}) + +(move-to-flash clamp) + +(defun speed_percentage_at (speed_index) +{ + (if (= speed_index SPEED_OFF) { + 0 + } { + (var count (length speed_set)) + (if (= count 0) { + (debug_log "Speed: speed_set empty, defaulting to 0%") + 0 + } { + (var max_index (- count 1)) + (var clamped (clamp speed_index SPEED_REVERSE_2 max_index)) + (if (!= speed_index clamped) + (debug_log_format (str-merge "Speed: Index " (to-str speed_index) " clamped to " (to-str clamped) " for speed_set")) + ) + (ix speed_set clamped) + }) + }) +}) + +(move-to-flash speed_percentage_at) + +(defun calculate_rpm (speed_index divisor) +{ + (var speed_percent (speed_percentage_at speed_index)) + (var max_rpm (cond + ((= scooter_type SCOOTER_BLACKTIP) MAX_ERPM_BLACKTIP) + ((= scooter_type SCOOTER_CUDAX) MAX_ERPM_CUDAX) + (t (debug_log "Invalid scooter_type, defaulting to Blacktip") MAX_ERPM_BLACKTIP) + )) + (var base_rpm (* (/ max_rpm divisor) speed_percent)) + (if (< speed_index SPEED_REVERSE_THRESHOLD) + (- 0 base_rpm) + base_rpm + ) +}) + +(move-to-flash calculate_rpm) + +; ============================================================================= +; State Machine Design Notes: +; - Each state handler runs in a loop checking (= sw_state N) +; - When transitioning, sw_state is updated, new handler spawned, and (break) called +; - The loop condition prevents race conditions by ensuring old handler exits +; - Old thread terminates naturally when loop condition becomes false +; ============================================================================= + +; ============================================================================= +; Speed Bounds Checking +; ============================================================================= + +; Helper function to safely set speed with bounds checking +; Valid speeds: SPEED_REVERSE_2, SPEED_UNTANGLE, 2-max_speed_no (forward), SPEED_OFF +; Returns the actual speed that was set after bounds checking +(defun set_speed_safe (new_speed) +{ + (var clamped_speed new_speed) + (if (= new_speed SPEED_OFF) { + ; Speed 99 (OFF) is always valid + (setvar 'speed SPEED_OFF) + (debug_log "Speed: Set to OFF") + } { + ; Clamp to valid range + (if (< new_speed SPEED_REVERSE_2) { + (setvar 'clamped_speed SPEED_REVERSE_2) + (debug_log_format (str-merge "Speed: Clamped " (to-str (to-i new_speed)) " to " (to-str SPEED_REVERSE_2) " (underflow)")) + }) + + (if (> clamped_speed max_speed_no) { + (setvar 'clamped_speed max_speed_no) + (debug_log_format (str-merge "Speed: Clamped " (to-str new_speed) " to " (to-str (to-i max_speed_no)) " (overflow)")) + }) + + ; Check reverse enable + (if (and (< clamped_speed SPEED_REVERSE_THRESHOLD) (= enable_reverse 0)) { + (setvar 'clamped_speed SPEED_REVERSE_THRESHOLD) + (debug_log_format (str-merge "Speed: Reverse disabled, clamped " (to-str (to-i new_speed)) " to " (to-str SPEED_REVERSE_THRESHOLD))) + }) + + (setvar 'speed clamped_speed) + (debug_log_format (str-merge "Speed: Set to " (to-str (to-i clamped_speed)))) + }) + clamped_speed +}) + +(move-to-flash set_speed_safe) + +; ============================================================================= +; Smart Cruise Timeout Helper +; ============================================================================= + +; Checks if Smart Cruise should transition to warning mode (HALF_ENABLED) +; and performs the transition if needed +; Called from state_handler_going_off timer expiry +(defun check_smart_cruise_timeout () +{ + (if (or (= smart_cruise SMART_CRUISE_FULLY_ENABLED) (= smart_cruise SMART_CRUISE_AUTO_ENGAGED)) + (if (> (secs-since timer_start) smart_cruise_timeout) { + (debug_log "Smart Cruise: Timeout - entering warning slowdown") + (setvar 'smart_cruise SMART_CRUISE_HALF_ENABLED) + (setvar 'timer_start (systime)) + (setvar 'timer_duration TIMER_SMART_CRUISE_TIMEOUT) + (setvar 'disp_num DISPLAY_SMART_CRUISE_HALF) + (setvar 'click_beep CLICKS_SMART_CRUISE_CHANGE) + ; slow scooter to 80% to help people realize cruise is expiring + (set-rpm (calculate_rpm speed SMART_CRUISE_SLOWDOWN_DIVISOR)) + }) + ) +}) + +(move-to-flash check_smart_cruise_timeout) + + +(defun smart_cruise_leds_count () +{ + ; Calculate Smart Cruise timer bar LED count (0-8) + ; Returns: -1 if Smart Cruise not active, otherwise 0-8 LEDs to light + (if (or (= smart_cruise SMART_CRUISE_FULLY_ENABLED) (= smart_cruise SMART_CRUISE_AUTO_ENGAGED)) { + (var leds_lit 8) ; Default to full bar + + ; If trigger is NOT held, calculate countdown based on elapsed time + (if (!= sw_state STATE_PRESSED) { + (var elapsed (secs-since timer_start)) + (if (> smart_cruise_timeout 0) { + (var progress (/ elapsed smart_cruise_timeout)) + (if (< progress 1.0) { + (setvar 'leds_lit (clamp (to-i (+ 0.5 (* 8 (- 1.0 progress)))) 1 8)) + } { + ; At/after expiry, keep a single LED + (setvar 'leds_lit 1) + }) + } { + ; Timeout <= 0: treat as immediately expired + (setvar 'leds_lit 1) + }) + }) + + leds_lit ; Return the LED count + } { + -1 ; Smart Cruise not active, return -1 + }) +}) + +(move-to-flash smart_cruise_leds_count) + + +(defun state_handler_off () +{ + ; xxxx State "0" Off + (debug_log "State 0: Off") + (loopwhile (= sw_state STATE_OFF) { + (sleep SLEEP_STATE_MACHINE) + ; Calculate corrected batt %, only needed when scooter is off in state 0 + (setvar 'actual_batt (get_battery_level)) + + ; Pressed + (if (= sw_pressed 1) { + (debug_log "State 0->1: Button pressed") + (setvar 'batt_disp_timer_start 0) ; Stop Battery Display in case its running + (setvar 'disp_timer_start 0) ; Stop Display in case its running + (setvar 'timer_start (systime)) + (setvar 'timer_duration TIMER_CLICK_WINDOW) + (setvar 'clicks CLICKS_SINGLE) + (state_transition_to STATE_COUNTING_CLICKS "button_press" THREAD_STACK_STATE_COUNTING state_handler_counting_clicks) + (break) + }) + }) +}) + +(move-to-flash state_handler_off) + + +(defun smart_cruise_upgrade_if_needed () +{ + (if (= smart_cruise SMART_CRUISE_HALF_ENABLED) { + (debug_log "Smart Cruise: Re-enabled from warning mode") + (setvar 'smart_cruise SMART_CRUISE_FULLY_ENABLED) + (setvar 'disp_num DISPLAY_SMART_CRUISE_FULL) + (set-rpm (calculate_rpm speed RPM_PERCENT_DENOMINATOR)) + }) +}) + +(move-to-flash smart_cruise_upgrade_if_needed) + + +; Encapsulated click action handler +(defun apply_click_action (click_count) +{ + (cond + ((= click_count CLICKS_SINGLE) { + (if (!= speed SPEED_OFF) { + (if (> smart_cruise SMART_CRUISE_OFF) { + ; Smart Cruise is active + ; Only allow speed change with long hold when NOT in warning mode (timing out) + (if (and (> initial_press_time TIMER_SMART_CRUISE_HOLD) (!= smart_cruise SMART_CRUISE_HALF_ENABLED)) { + ; Long hold before click - change speed down (not allowed during timeout warning) + (debug_log "Click action: Single click after hold (Smart Cruise: speed down + timer reset)") + (setvar 'click_beep CLICKS_SINGLE) + (setvar 'timer_start (systime)) + (setvar 'speed_set_via_jump nil) ; User manually changed speed, allow remembering + ; If in warning mode, upgrade back to fully enabled + (smart_cruise_upgrade_if_needed) + ; Change speed down + (if (> speed SPEED_REVERSE_THRESHOLD) { + (set_speed_safe (- speed 1)) + }) + } { + ; Quick tap OR in warning mode - just reset timer (no speed change) + (debug_log "Click action: Single click (Smart Cruise timer reset)") + (setvar 'timer_start (systime)) + ; If in warning mode, upgrade back to fully enabled + (smart_cruise_upgrade_if_needed) + }) + } { + ; Smart Cruise not active - normal speed down + (debug_log "Click action: Single click (speed down)") + (setvar 'click_beep CLICKS_SINGLE) + (setvar 'speed_set_via_jump nil) ; User manually changed speed, allow remembering + (cond + ((> speed SPEED_REVERSE_THRESHOLD) + (set_speed_safe (- speed 1))) + ((= speed SPEED_REVERSE_2) + (set_speed_safe SPEED_UNTANGLE))) + }) + }) + }) + ((= click_count CLICKS_DOUBLE) { + (if (= speed SPEED_OFF) { + (debug_log_format (str-merge "Click action: Double click (start at speed " (to-str (to-i new_start_speed)) ")")) + (setvar 'click_beep CLICKS_DOUBLE) + (setvar 'speed_set_via_jump nil) ; Normal start, allow speed to be remembered + (set_speed_safe new_start_speed) + } { + (if (> smart_cruise SMART_CRUISE_OFF) { + ; Smart Cruise is active - only allow speed change after long hold, and not during timeout warning + (if (and (> initial_press_time TIMER_SMART_CRUISE_HOLD) (!= smart_cruise SMART_CRUISE_HALF_ENABLED)) { + ; Long hold before double tap - change speed up (not allowed during timeout warning) + (debug_log "Click action: Double click after hold (Smart Cruise: speed up + timer reset)") + (setvar 'click_beep CLICKS_DOUBLE) + (setvar 'timer_start (systime)) + (setvar 'speed_set_via_jump nil) ; User manually changed speed, allow remembering + ; If in warning mode, upgrade back to fully enabled + (smart_cruise_upgrade_if_needed) + ; Change speed up + (if (and (< speed max_speed_no) (> speed SPEED_UNTANGLE)) { + (set_speed_safe (+ speed 1)) + }) + } { + ; Quick double tap without hold OR in warning mode - just reset timer (no speed change) + (debug_log "Click action: Double click (Smart Cruise timer reset)") + (setvar 'timer_start (systime)) + ; If in warning mode, upgrade back to fully enabled + (smart_cruise_upgrade_if_needed) + }) + } { + ; Smart Cruise not active - normal speed up + (debug_log "Click action: Double click (speed up)") + (setvar 'click_beep CLICKS_DOUBLE) + (setvar 'speed_set_via_jump nil) ; User manually changed speed, allow remembering + (if (< speed max_speed_no) { + (if (> speed SPEED_UNTANGLE) + (set_speed_safe (+ speed 1)) + (set_speed_safe SPEED_REVERSE_2)) + }) + }) + }) + }) + ((= click_count CLICKS_TRIPLE) { + (if (= speed SPEED_OFF) { + ; Stopped - jump to preset speed + (debug_log_format (str-merge "Click action: Triple click (jump to speed " (to-str (to-i jump_speed)) ")")) + (setvar 'click_beep CLICKS_TRIPLE) + (setvar 'speed_set_via_jump t) ; Speed set via jump, don't remember unless changed + (set_speed_safe jump_speed) + } { + ; Running - only allow Smart Cruise toggle in forward speeds + (if (>= speed SPEED_REVERSE_THRESHOLD) { + ; Running forward - toggle Smart Cruise + (if (> smart_cruise SMART_CRUISE_OFF) { + ; Smart Cruise is active - disable it + (debug_log "Click action: Triple click (Smart Cruise disabled)") + (setvar 'click_beep CLICKS_TRIPLE) + (setvar 'smart_cruise SMART_CRUISE_OFF) + } { + ; Smart Cruise not active - enable it if feature is enabled + (if (> enable_smart_cruise 0) { + (debug_log "Click action: Triple click (Smart Cruise enabled)") + (setvar 'click_beep CLICKS_TRIPLE) + (setvar 'smart_cruise SMART_CRUISE_FULLY_ENABLED) + (setvar 'timer_start (systime)) + (setvar 'disp_num DISPLAY_SMART_CRUISE_FULL) + (set-rpm (calculate_rpm speed RPM_PERCENT_DENOMINATOR)) + } { + (debug_log "Click action: Triple click ignored (Smart Cruise disabled in settings)") + }) + }) + } { + ; Running backward - ignore + (debug_log "Click action: Triple click ignored (running backward)") + }) + }) + }) + ((= click_count CLICKS_QUADRUPLE) { + ; Quadruple click only works when stopped + (if (= speed SPEED_OFF) { + (if (= enable_reverse 1) { + (debug_log "Click action: Quadruple click (untangle)") + (setvar 'click_beep CLICKS_QUADRUPLE) + (set_speed_safe SPEED_UNTANGLE) + } { + (debug_log "Click action: Quadruple click ignored (reverse disabled in settings)") + }) + } { + ; Running - ignore quadruple click + (debug_log "Click action: Quadruple click ignored (scooter running)") + }) + }) + (t + (debug_log_format (str-merge "Click action: Unsupported count " (to-str click_count)))) + ) +}) + +(move-to-flash apply_click_action) + +; xxxx STATE 1 Counting clicks +(defun state_handler_counting_clicks () +{ + (debug_log_format (str-merge "State 1: Counting clicks=" (to-str clicks))) + (loopwhile (= sw_state STATE_COUNTING_CLICKS) { + (sleep SLEEP_STATE_MACHINE) + + ; Keep motor running while in Smart Cruise mode + (if (> smart_cruise SMART_CRUISE_OFF) + (timeout-reset) + ) + + ; Released + (if (= sw_pressed 0) { + (setvar 'disp_timer_start 0) ; Stop Display in case its running + (setvar 'timer_start (systime)) + (setvar 'timer_duration TIMER_RELEASE_WINDOW) + (state_transition_to STATE_GOING_OFF "released" THREAD_STACK_STATE_TRANSITIONS state_handler_going_off) + (break) + }) + + ; Timer Expiry + (if (> (secs-since timer_start) timer_duration) { + (debug_log_format (str-merge "State 1: Timer expired, clicks=" (to-str clicks))) + + ; Process click actions + (apply_click_action clicks) + + ; End of Click Actions + (setvar 'clicks 0) + (setvar 'timer_duration TIMER_DISABLED) + + ; Transition based on actual button state + (if (= sw_pressed 1) { + (debug_log_format (str-merge "State 1->2: Speed=" (to-str (to-i speed)))) + (state_transition_to STATE_PRESSED "click_window_expired" THREAD_STACK_STATE_MACHINE state_handler_pressed) + } { + (debug_log "State 1->3: Button released during click window") + (setvar 'timer_start (systime)) + (setvar 'timer_duration TIMER_RELEASE_WINDOW) + (state_transition_to STATE_GOING_OFF "click_window_expired" THREAD_STACK_STATE_TRANSITIONS state_handler_going_off) + }) + (break) + }) + }) +}) + +(move-to-flash state_handler_counting_clicks) + +; xxxx State 2 "Pressed" +(defun state_handler_pressed () +{ + (debug_log "State 2: Pressed") + (loopwhile (= sw_state STATE_PRESSED) { + (sleep SLEEP_STATE_MACHINE) + (timeout-reset) ; keeps motor running + + ; xxx repeat display section whilst scooter is running xxx + (if (and (> (secs-since timer_start) DISPLAY_REPEAT_FIRST) (= smart_cruise SMART_CRUISE_OFF)) ; 6 = display duration +1 + (setvar 'disp_num last_batt_disp_num) + ) + + (if (and (> (secs-since timer_start) DISPLAY_REPEAT_SECOND) (= smart_cruise SMART_CRUISE_OFF)) { ; 12= (2xdisplay duration + 2) + (setvar 'disp_num (+ speed DISPLAY_SPEED_OFFSET)) + (setvar 'timer_start (systime)) + }) + + ; xxx end repeat display section + (if (and (= smart_cruise SMART_CRUISE_HALF_ENABLED) (> (secs-since timer_start) TIMER_SMART_CRUISE_TIMEOUT)) ; time out Smart Cruise if second activation isn't received within display duration + (setvar 'smart_cruise SMART_CRUISE_OFF) + ) + + ; Extra Long Press Commands when off (10 seconds) + (if (and (> (secs-since timer_start) TIMER_LONG_PRESS) (= speed SPEED_OFF) (= thirds_warning_latched 0)) { + (debug_log "Battery: Thirds warning enabled") + (setvar 'thirds_total actual_batt) + (spawn warbler WARBLER_FREQUENCY WARBLER_DURATION 0) + (setvar 'warning_counter 0) + (setvar 'thirds_warning_latched 1) + }) + + ; Released + (if (= sw_pressed 0) { + (debug_log "State 2->3: Released") + (setvar 'thirds_warning_latched 0) + ; Record how long the button was held before first release + (setvar 'initial_press_time (secs-since timer_start)) + (setvar 'timer_start (systime)) + (setvar 'timer_duration TIMER_RELEASE_WINDOW) + (state_transition_to STATE_GOING_OFF "released" THREAD_STACK_STATE_TRANSITIONS state_handler_going_off) + (break) + }) + }) +}) + +(move-to-flash state_handler_pressed) + +; xxxx State 3 "Going Off" +(defun state_handler_going_off () +{ + (debug_log "State 3: Going Off") + (loopwhile (= sw_state STATE_GOING_OFF) { + (sleep SLEEP_STATE_MACHINE) + (if (> smart_cruise SMART_CRUISE_OFF) ; If Smart Cruise is enabled, dont shut down + (timeout-reset) + ) + + ; Pressed + (if (= sw_pressed 1) { + (timeout-reset) ; keeps motor running, vesc automatically stops if it doesn't receive this command every second + + ; Check if this is a new click sequence (timer expired) or continuation + (if (> (secs-since timer_start) timer_duration) { + ; New click sequence - reset counter and initial press time + (setvar 'clicks 1) + (setvar 'initial_press_time 0) + } { + ; Continuation of existing click sequence - increment + (if (not (eq safe_start_status 'running)) { ; block only while safe-start is in progress + (setvar 'clicks (+ clicks 1)) + }) + }) + + (setvar 'timer_start (systime)) + (setvar 'timer_duration TIMER_CLICK_WINDOW) + + (state_transition_to STATE_COUNTING_CLICKS "button_pressed" THREAD_STACK_STATE_COUNTING state_handler_counting_clicks) + (break) + }) + + ; Timer Expiry + (if (> (secs-since timer_start) timer_duration) { + ; Check if we have pending clicks to process first + (if (> clicks 0) { + (debug_log_format (str-merge "State 3: Processing pending clicks=" (to-str clicks))) + (apply_click_action clicks) + (setvar 'clicks 0) + ; Reset timer to stay in GOING_OFF state + (setvar 'timer_start (systime)) + (setvar 'timer_duration TIMER_RELEASE_WINDOW) + } { + ; No pending clicks - check if we should shut down + (if (and (!= smart_cruise SMART_CRUISE_FULLY_ENABLED) (!= smart_cruise SMART_CRUISE_AUTO_ENGAGED)) { ; If Smart Cruise is enabled, don't shut down + (debug_log "State 3->0: Timeout, shutting down") + (setvar 'timer_duration TIMER_DISABLED) + ; Only remember speed if user manually changed it (not just started via jump speed) + (if (not speed_set_via_jump) { + (cond + ((and (< speed start_speed) (> speed SPEED_UNTANGLE)) ; start at old speed if less than start speed + (setvar 'new_start_speed speed)) + ((>= speed start_speed) + (setvar 'new_start_speed start_speed)) + ) + } { + ; Speed set via jump and not changed - reset to normal start speed + (setvar 'new_start_speed start_speed) + }) + (set_speed_safe SPEED_OFF) + (setvar 'smart_cruise SMART_CRUISE_OFF) ; turn off Smart Cruise + (state_transition_to STATE_OFF "timeout_shutdown" THREAD_STACK_STATE_TRANSITIONS state_handler_off) + (break) ; SWST_OFF + }) + + ; Check if Smart Cruise needs to timeout + (check_smart_cruise_timeout) + }) + }) ; end Timer expiry + }) ; end state +}) + +(move-to-flash state_handler_going_off) + +(defun start_motor_speed_loop () +{ + (debug_log "Motor: Starting motor speed loop") + + (safe_start_reset_state) + + (var last_speed SPEED_OFF) + + (loopwhile-thd THREAD_STACK_MOTOR t { + (sleep SLEEP_MOTOR_CONTROL) + + (loopwhile (!= speed last_speed) { + ; Only log speed changes that aren't part of soft-start monitoring + (if (!= last_speed SPEED_SOFT_START_SENTINEL) + (debug_log_format (str-merge "Motor: Speed change " (to-str (to-i last_speed)) "->" (to-str (to-i speed)))) + ) + (sleep SLEEP_MOTOR_SPEED_CHANGE) + + ; turn off motor if speed is 99 + (if (= speed SPEED_OFF) { + (debug_log "Motor: Stopping motor") + (set-current 0) + (setvar 'batt_disp_timer_start (systime)) ; Start trigger for Battery Display + (setvar 'disp_num DISPLAY_OFF) ; Turn on Off display. (off display is needed to ensure restart triggers a new display number) + (safe_start_reset_state) ; unlock speed changes and disable safe start timer + (setvar 'last_speed speed) + }) + + (if (!= speed SPEED_OFF) { + ; Soft Start initiation section (only when starting from off) + (if (= last_speed SPEED_OFF) { + (debug_log "Motor: Soft start initiated") + (conf-set 'l-in-current-max (if (= scooter_type SCOOTER_BLACKTIP) MIN_CURRENT_BLACKTIP MIN_CURRENT_CUDAX)) + ; Start the soft-start timer regardless of safe-start being enabled so we can restore currents after a short period + (setvar 'soft_start_timer (systime)) + (soft_start_set_active 1) + (setvar 'safe_start_attempt_speed speed) + (if (= use_safe_start 1) + (safe_start_begin speed) + (safe_start_set_status 'idle)) ; keep safe_start_status idle when disabled + (setvar 'last_speed SPEED_SOFT_START_SENTINEL) + (if (< speed SPEED_REVERSE_THRESHOLD) + (set-duty (- 0 SAFE_START_DUTY)) + (set-duty SAFE_START_DUTY) + ) + } { + ; Speed change while already running (not from off, not during soft-start) + (if (!= last_speed SPEED_SOFT_START_SENTINEL) { + (set-rpm (calculate_rpm speed RPM_PERCENT_DENOMINATOR)) + (setvar 'disp_num (+ speed DISPLAY_SPEED_OFFSET)) + (setvar 'last_speed speed) + }) + }) + + ; Soft-start/Safe-start monitoring section (runs while sentinel is active) + (if (= last_speed SPEED_SOFT_START_SENTINEL) { + (var soft_elapsed (secs-since soft_start_timer)) + (var rpm (get-rpm)) + (var duty (get-duty)) + (var current (get-current)) + + ; For safe-start enabled: check telemetry and criteria + (if (= use_safe_start 1) { + (var elapsed (secs-since safe_start_timer)) + + ; Check for invalid telemetry + (if (not (safe_start_telemetry_valid rpm duty current)) + (safe_start_abort_with_reason "invalid telemetry") + ) + + ; Check safe-start completion + (if (safe_start_met_success_criteria rpm duty current) + { + (debug_log "Motor: Soft start completed (telemetry)") + (conf-set 'l-in-current-max (if (= scooter_type SCOOTER_BLACKTIP) MAX_CURRENT_BLACKTIP MAX_CURRENT_CUDAX)) + (set-rpm (calculate_rpm speed RPM_PERCENT_DENOMINATOR)) + (setvar 'disp_num (+ speed DISPLAY_SPEED_OFFSET)) + (safe_start_success) + (soft_start_set_active 0) + (setvar 'last_speed speed) + } { + ; Check timeout + (if (> elapsed (+ SAFE_START_TIMEOUT SAFE_START_TIMEOUT_GRACE)) + (safe_start_abort_with_reason "timeout") + ) + + ; Detect high-current stall + (if (and (> elapsed SAFE_START_TIMEOUT) (> (abs current) SAFE_START_FAIL_CURRENT) (< (abs rpm) SAFE_START_MIN_RPM)) + (safe_start_abort_with_reason "high current stall") + ) + + ; If aborted, exit sentinel + (if (not (eq safe_start_status 'running)) + (setvar 'last_speed speed) + ) + }) + } { + ; For safe-start disabled: just wait for timer + (if (> soft_elapsed SOFT_START_DURATION) + { + (debug_log "Motor: Soft start completed (timer)") + (conf-set 'l-in-current-max (if (= scooter_type SCOOTER_BLACKTIP) MAX_CURRENT_BLACKTIP MAX_CURRENT_CUDAX)) + (set-rpm (calculate_rpm speed RPM_PERCENT_DENOMINATOR)) + (setvar 'disp_num (+ speed DISPLAY_SPEED_OFFSET)) + ; Clear timers without changing safe-start status (feature disabled) + (setvar 'soft_start_timer 0) + (setvar 'safe_start_timer 0) + (soft_start_set_active 0) + (setvar 'last_speed speed) + }) + }) + }) + }) + }) + }) +}) + +(move-to-flash start_motor_speed_loop) + +; Init-only function (not moved to flash) +(defun thirds_warning_startup () +{ + (if (> enable_thirds_warning_startup 0) { + (debug_log "Battery: Thirds warning enabled at startup") + ; Wait a bit for battery reading to stabilize + (sleep SLEEP_BATTERY_STABILIZE) + ; Calculate battery % using the configured method + ; Set thirds_total to current battery level + (setvar 'thirds_total (get_battery_level)) + (debug_log (str-merge "Battery: Initial level=" (to-str thirds_total))) + (setvar 'warning_counter 0) + }) +}) + + +(defun apply_smart_cruise_timer_bar (pixbuf) +{ + ; Apply Smart Cruise timer bar to the bottom row when active + ; The display buffer is organized as 8 rows of 2 bytes each (16 bytes total) + ; b0,b1 = row 0 (top), b2,b3 = row 1, ..., b14,b15 = row 7 (bottom) + ; Within each row: b15 (odd byte) contains the 8 LED bits with the following mapping: + ; LED positions (left→right): LED1=bit7, LED2=bit0, LED3=bit1, LED4=bit2, LED5=bit3, LED6=bit4, LED7=bit5, LED8=bit6 + ; Show timer bar whenever Smart Cruise is active + ; If trigger is held, show full bar (all 8 LEDs). If released, show countdown. + ; Returns the number of LEDs that should be lit (0-8), or -1 if Smart Cruise is not active + (var leds_lit (smart_cruise_leds_count)) + + (if (!= leds_lit -1) { + ; Build the bottom row byte value - LEDs turn off left to right (LED 1→2→3→4→5→6→7→8) + ; Physical LED positions mapped to byte bits: LED1=bit7, LED2=bit0, LED3=bit1, LED4=bit2, LED5=bit3, LED6=bit4, LED7=bit5, LED8=bit6 + ; Lookup table approach for clarity + (var bottom_row_value (ix TIMER_BAR_MASKS (clamp leds_lit 0 8))) + + ; Set the bottom row (byte 15) to show the timer bar + (var current_byte (bufget-u8 pixbuf 15)) + (bufset-u8 pixbuf 15 (bitwise-or current_byte bottom_row_value)) + }) + + leds_lit ; Return the LED count or -1 +}) + +(move-to-flash apply_smart_cruise_timer_bar) + + +(defun start_display_output_loop () +{ + (var start_pos 0) ; variable used to define start position in the array of diferent display screens + (var pixbuf (array-create 16)) ; create a temp array to store display bytes in + (var display_mpu_addr 0x70) ; I2C Address for the screen + (var last_timer_bar_leds -1) ; Cache for Smart Cruise timer bar LED count to minimize I2C updates + (loopwhile-thd THREAD_STACK_DISPLAY t { + (sleep SLEEP_UI_UPDATE) + ; Normal display timeout logic - clear display content but keep timer bar visible + (if (and + (> disp_timer_start 1) + (> (secs-since disp_timer_start) TIMER_DISPLAY_DURATION)) { + ; Clear the display number so 'C' or speed won't re-render with timer bar updates + ; Only do this if Smart Cruise is active to avoid blank flash + (if (or (= smart_cruise SMART_CRUISE_FULLY_ENABLED) (= smart_cruise SMART_CRUISE_AUTO_ENGAGED)) { + (setvar 'disp_num DISPLAY_SENTINEL) + } { + ; Smart Cruise not active - turn off display normally + (setvar 'disp_num DISPLAY_SENTINEL) + ; For Blacktip, turn off the display + (if (= scooter_type SCOOTER_BLACKTIP) + (i2c-tx-rx 0x70 (list 0x80)) + ) + ; Prevent the off→on flip in the same loop iteration + (setvar 'last_disp_num DISPLAY_SENTINEL) + }) + ; For Cuda X make sure it doesn't get stuck on displaying B1 or B2 error, so switch back to last battery. + (if (and (= scooter_type SCOOTER_CUDAX) (> last_disp_num 20)) + (setvar 'disp_num last_batt_disp_num) + ) + + (setvar 'disp_timer_start 0) + }) + + ; Check if we need to update display (either disp_num changed OR Smart Cruise timer bar needs updating) + (var should_update_display (!= disp_num last_disp_num)) + + ; Check if Smart Cruise timer bar LED count has changed + (var current_leds (smart_cruise_leds_count)) + (if (!= current_leds -1) { + ; Smart Cruise active - check if LED count changed + (if (!= current_leds last_timer_bar_leds) { + (setvar 'should_update_display 1) + (setvar 'last_timer_bar_leds current_leds) + }) + } { + ; Smart Cruise not active - reset cache if it was previously set + (if (!= last_timer_bar_leds -1) { + (setvar 'last_timer_bar_leds -1) + (setvar 'should_update_display 1) ; Force update to clear timer bar + }) + }) + + (if should_update_display { + (setvar 'display_mpu_addr 0x70) + (if (= scooter_type 1) { ; For cuda X second screen + (if (or (= disp_num 0) (= disp_num 1) (= disp_num 2) (= disp_num 3) (> disp_num 17)) + (if (= cudax_flip 1) + (setvar 'display_mpu_addr 0x71) + ) + (if (!= cudax_flip 1) + (setvar 'display_mpu_addr 0x71) + ) + ) + }) + ; Only update disp_timer_start if disp_num actually changed + (if (!= disp_num last_disp_num) { + (setvar 'disp_timer_start (systime)) + }) + (if (= display_mpu_addr 0x70) + (setvar 'start_pos (+(* 64 disp_num) (* 16 rotation))) ; define the correct start position in the array for the display + (setvar 'start_pos (+(* 64 disp_num) (* 16 rotation2))) + ) + (bufclear pixbuf) + ; Copy display data from binary LUT only if not sentinel (allows timer bar only display) + (if (!= disp_num DISPLAY_SENTINEL) + (bufcpy pixbuf 0 display_lut_bin (+ 8 start_pos) 16) ; copy the required display from binary LUT to "pixbuf" + ) + ; Apply Smart Cruise timer bar overlay to bottom row if active + (apply_smart_cruise_timer_bar pixbuf) + (i2c-tx-rx display_mpu_addr pixbuf) ; send display characters + (i2c-tx-rx display_mpu_addr (list 0x81)) ; Turn on display + (setvar 'last_disp_num disp_num) + }) + }) +}) + +(move-to-flash start_display_output_loop) + +; Returns true once the startup tune has finished AND 1 second has elapsed, +; giving a clear gap between the tune and the battery status beeps. +(defun tune-settled () + (and (> startup_tune_done_time 0) (> (secs-since startup_tune_done_time) STARTUP_TUNE_SETTLE)) +) + +(move-to-flash tune-settled) + +; **** Program that triggers the display to show battery status **** +(defun start_display_battery_loop () +{ + (var batt_disp_state 0) + (loopwhile-thd THREAD_STACK_BATTERY t { + (sleep SLEEP_UI_UPDATE) + + (if (or (= batt_disp_timer_start 0) (= batt_disp_state 0)) { + (setvar 'batt_disp_state 0)}) + + + (if (and (> batt_disp_timer_start 1) (> (secs-since batt_disp_timer_start) 6) (= batt_disp_state 0)) { ; waits Display Duration + 1 second after scooter is turned off to stabilize battery readings + + ; xxxx Section for normal 4 bar battery capacity display + + (if (= thirds_total 0) + (cond + ((> actual_batt 0.75) { + (setvar 'disp_num 3) + (if (tune-settled) (spawn beeper 4) (setvar 'batt_beeps_pending 4)) + }) + ((> actual_batt 0.5) { + (setvar 'disp_num 2) + (if (tune-settled) (spawn beeper 3) (setvar 'batt_beeps_pending 3)) + }) + ((> actual_batt 0.25) { + (setvar 'disp_num 1) + (if (tune-settled) (spawn beeper 2) (setvar 'batt_beeps_pending 2)) + }) + (t { + (setvar 'disp_num 0) + (if (tune-settled) (spawn beeper 1) (setvar 'batt_beeps_pending 1)) + }) + ) + + ; Section for 1/3rds display + (cond + ((and (> actual_batt (* thirds_total 0.66)) (= warning_counter 0)) { + (debug_log "Battery: 2/3rds warning triggered") + (setvar 'disp_num 20) + }) + ((and (> actual_batt (* thirds_total 0.33)) (< warning_counter 3)) { + (debug_log "Battery: 1/3rd warning triggered") + (setvar 'disp_num 19) + (if (< warning_counter 2) { + (spawn warbler 350 0.5 0.5) + (setvar 'warning_counter (+ warning_counter 1)) + }) + }) + (t { + (debug_log "Battery: Critical warning triggered") + (setvar 'disp_num 18) + (if (< warning_counter 4) { + (spawn warbler 350 0.5 0.5) + (setvar 'warning_counter (+ warning_counter 1)) + }) + }) + ) + ) + + (setvar 'batt_disp_state 1) + (setvar 'last_batt_disp_num disp_num) + }) + + (if (and (> batt_disp_timer_start 1) (> (secs-since batt_disp_timer_start) 12) (= batt_disp_state 1) (> thirds_total 0)) { + + (cond + ((> actual_batt 0.95) + (setvar 'disp_num 30)) + ((> actual_batt 0.90) + (setvar 'disp_num 29)) ; 90% + ((> actual_batt 0.80) + (setvar 'disp_num 28)) ; 80% + ((> actual_batt 0.70) + (setvar 'disp_num 27)) ; 70% + ((> actual_batt 0.60) + (setvar 'disp_num 26)) ; 60% + ((> actual_batt 0.50) + (setvar 'disp_num 25)) ; 50% + ((> actual_batt 0.40) + (setvar 'disp_num 24)) ; 40% + ((> actual_batt 0.30) + (setvar 'disp_num 23)) ; 30% + ((> actual_batt 0.20) + (setvar 'disp_num 22)) ; 20% + (t + (setvar 'disp_num 21)) + ) + (setvar 'batt_disp_state 0) + (setvar 'batt_disp_timer_start 0) + }) + + ; Play battery beeps that were deferred while the startup tune was playing + (if (and (> batt_beeps_pending 0) (tune-settled)) { + (setvar 'disp_num last_batt_disp_num) ; Re-show battery level while deferred beeps play + (spawn beeper batt_beeps_pending) + (setvar 'batt_beeps_pending 0) + }) + }) +}) + +(move-to-flash start_display_battery_loop) + +(defun beeper (beeps) +(loopwhile (and (= enable_battery_beeps 1) (> batt_disp_timer_start 0) (> beeps 0)) { + (sleep SLEEP_UI_UPDATE) + (foc-beep 350 0.5 beeps_vol) + (setvar 'beeps (- beeps 1)) + })) + +(move-to-flash beeper) + +; xxxx warbler Program xxxx" +(defun warbler (Tone Time Delay) +{ + (sleep Delay) + (foc-beep Tone Time beeps_vol) + (foc-beep (- Tone 200) Time beeps_vol) + (foc-beep Tone Time beeps_vol) + (foc-beep (- Tone 200) Time beeps_vol) +}) + +(move-to-flash warbler) + +; ***** Imperial March Theme ***** +; Plays the first ~9 bars of the Imperial March on startup +; Note frequencies in Hz (approximations for beeper): +; G4=392, Eb4=311, Bb4=466, D5=587, Gb4=370 +(defun play_imperial_march () +{ + (if (> beeps_vol 0) { ; Only play if volume is not zero + ; Bar 1-2: G G G Eb-Bb G + (foc-beep 392 0.35 beeps_vol) ; G quarter + (sleep 0.37) + (foc-beep 392 0.35 beeps_vol) ; G quarter + (sleep 0.37) + (foc-beep 392 0.35 beeps_vol) ; G quarter + (sleep 0.37) + (foc-beep 311 0.25 beeps_vol) ; Eb short + (sleep 0.27) + (foc-beep 466 0.12 beeps_vol) ; Bb very short + (sleep 0.14) + (foc-beep 392 0.35 beeps_vol) ; G quarter + (sleep 0.37) + + ; Bar 3-4: Eb-Bb G (hold) + (foc-beep 311 0.25 beeps_vol) ; Eb short + (sleep 0.27) + (foc-beep 466 0.12 beeps_vol) ; Bb very short + (sleep 0.14) + (foc-beep 392 0.7 beeps_vol) ; G half note + (sleep 0.74) + + ; Bar 5-6: D D D Eb-Bb Gb + (foc-beep 587 0.35 beeps_vol) ; D quarter + (sleep 0.37) + (foc-beep 587 0.35 beeps_vol) ; D quarter + (sleep 0.37) + (foc-beep 587 0.35 beeps_vol) ; D quarter + (sleep 0.37) + (foc-beep 311 0.25 beeps_vol) ; Eb short + (sleep 0.27) + (foc-beep 466 0.12 beeps_vol) ; Bb very short + (sleep 0.14) + (foc-beep 370 0.35 beeps_vol) ; Gb quarter + (sleep 0.37) + + ; Bar 7-8: Eb-Bb G (hold) + (foc-beep 311 0.25 beeps_vol) ; Eb short + (sleep 0.27) + (foc-beep 466 0.12 beeps_vol) ; Bb very short + (sleep 0.14) + (foc-beep 392 0.7 beeps_vol) ; G half note + (sleep 0.74) + }) + (setvar 'startup_tune_done_time (systime)) ; Record when startup tune finished +}) + +(move-to-flash play_imperial_march) + +; ***** Program that beeps trigger clicks +(defun start_beeper_loop () +{ + (var click_beep_timer 0) + (loopwhile-thd THREAD_STACK_CLICK_BEEP t { + (sleep SLEEP_UI_UPDATE) + + (if (and (> (secs-since click_beep_timer) SLEEP_UI_UPDATE) (!= click_beep_timer 0)) { + (foc-play-stop) + (setvar 'click_beep_timer 0) + }) + + (if (> click_beep 0) { + (cond + ((= click_beep CLICKS_SMART_CRUISE_CHANGE) + (foc-play-tone 1 1500 beeps_vol)) + ((= enable_trigger_beeps 1) { + (cond + ((= click_beep CLICKS_SINGLE) (foc-play-tone 1 2500 beeps_vol)) + ((= click_beep CLICKS_DOUBLE) (foc-play-tone 1 3000 beeps_vol)) + ((= click_beep CLICKS_TRIPLE) (foc-play-tone 1 3500 beeps_vol)) + ((= click_beep CLICKS_QUADRUPLE) (foc-play-tone 1 4000 beeps_vol)) + ) + }) + ) + + (setvar 'click_beep_timer (systime)) + (setvar 'click_beep 0) + }) + }) +}) + +(move-to-flash start_beeper_loop) + +; Init-only function (not moved to flash) +(defun peripherals_setup () +{ + (if (or (= 0 hardware_configuration) (= 3 hardware_configuration)) ; turn on i2c for the screen based on wiring. 0 = Blacktip with Bluetooth, 3 = CudaX with Bluetooth + (i2c-start 'rate-400k 'pin-swdio 'pin-swclk) ; Works HW 60 with screen on SWD Connector. Screen SDA pin to Vesc SWDIO (2), Screen SCL pin to Vesc SWCLK (4) + (if (or (= 1 hardware_configuration) ( = 4 hardware_configuration)) ; 1 = Blacktip without Bluetooth, 4 = CudaX without Bluetooth + (i2c-start 'rate-400k 'pin-rx 'pin-tx) ; Works HW 60 with screen on Comm Connector. Screen SDA pin to Vesc RX/SDA (5), Screen SCL pin to Vesc TX/SCL (6) + (if ( = 2 hardware_configuration) + (i2c-start 'rate-400k 'pin-tx 'pin-rx) ; tested on HW 410 Tested: SN 189, SN 1691 + (nil)))) + + (i2c-tx-rx 0x70 (list 0x21)) ; start the oscillator + ; Set brightness using BRIGHTNESS_LUT (six discrete levels, indices 0..5) + (i2c-tx-rx 0x70 (list (ix BRIGHTNESS_LUT (clamp disp_brightness 0 (- (length BRIGHTNESS_LUT) 1))))) ; set brightness safely + + (if (= scooter_type 1) { ; For cuda X setup second screen + (i2c-tx-rx 0x71 (list 0x21)) ; start the oscillator + (i2c-tx-rx 0x71 (list (ix BRIGHTNESS_LUT (clamp disp_brightness 0 (- (length BRIGHTNESS_LUT) 1))))) ; set brightness safely + }) +}) + +(defun init () +{ + (load_lookup_tables) + + (eeprom_set_defaults) + + (gc) ; Initialisation is done, clean up + + (debug_log "Initialisation done") +}) + +(defun main () +{ + (update_settings_from_eeprom) + + (log_startup) + + (define thirds_total 0) + (define warning_counter 0) ; Count how many times the 3rds warnings have been triggered. + (define thirds_warning_latched 0) ; Prevents repeated long-press activation + + (thirds_warning_startup) + + (setup_event_handler) + + (define sw_state 0) + (define timer_start 0) + (define timer_duration 0) + (define initial_press_time 0) + (define clicks 0) + (define actual_batt 0) + (define new_start_speed start_speed) + (define speed_set_via_jump nil) ; Track if current speed was set via jump (triple-click) and not manually changed + (define state_last_state STATE_UNINITIALIZED) + (define state_last_change_time 0) + (define state_last_reason "") + + (state_metrics_reset) + + (define speed SPEED_OFF) + (define safe_start_timer 0) + (define soft_start_timer 0) + (define soft_start_active 0) + (define safe_start_attempt_speed SPEED_OFF) + (define safe_start_failures 0) + (define safe_start_status 'idle) + + (start_motor_speed_loop) + + (define click_beep 0) + + (start_beeper_loop) + + (define disp_timer_start 0) ; Timer for display duration + + (peripherals_setup) + + (define disp_num 1) ; variable used to define the display screen you are accesing 0-X + (define last_disp_num 1) ; variable used to track last display screen show + + (start_display_output_loop) + + (define batt_disp_timer_start 0) ; Timer to see if Battery display has been triggered + (define last_batt_disp_num 3) ; variable used to track last display screen show + (define startup_tune_done_time 0) ; Timestamp when startup tune finished (0 = not done yet) + (define batt_beeps_pending 0) ; Battery beep count deferred until startup tune settle delay + + (start_display_battery_loop) + + (define smart_cruise SMART_CRUISE_OFF) + + (start_smart_cruise_loop) + + (define sw_pressed 0) + + (start_trigger_loop) + + (state_transition_to STATE_OFF "startup" THREAD_STACK_STATE_TRANSITIONS state_handler_off) ; ***Start state machine running for first time + + (setvar 'disp_num 15) ; display startup screen, change bytes if you want a different one + + ; Play Imperial March on startup in background thread to avoid blocking + (spawn THREAD_STACK_CLICK_BEEP play_imperial_march) + + ; Check battery level and only play battery indication if not full (3 bars) + ; Battery is considered "full" at > 0.75 (matching the 3-bar threshold) + ; Allow 6+ seconds for battery to stabilize and Imperial March to finish before beeps + (if (> (get_battery_level) 0.75) + (setvar 'batt_disp_timer_start 0) ; Skip battery beeps if battery is full + (setvar 'batt_disp_timer_start (systime)) ; Battery beeps after stabilization period + ) + + (puts "Startup complete") +}) + +; Configuration settings +(define max_speed_no 0) +(define start_speed 0) +(define jump_speed 0) +(define use_safe_start 0) +(define enable_reverse 0) +(define enable_smart_cruise 0) +(define smart_cruise_timeout 0) +(define rotation 0) +(define disp_brightness 0) +(define hardware_configuration 0) +(define enable_battery_beeps 0) +(define beeps_vol 0) +(define cudax_flip 0) +(define rotation2 0) +(define enable_trigger_beeps 0) +(define enable_smart_cruise_auto_engage 0) +(define smart_cruise_auto_engage_time 0) +(define enable_thirds_warning_startup 0) +(define battery_calculation_method 0) +(define debug_enabled 0) +(define speed_set 0) +(define scooter_type 0) + +(init) + +(image-save) + +(main) diff --git a/ebike/pkgdesc.qml b/ebike/pkgdesc.qml new file mode 100644 index 000000000..360759aff --- /dev/null +++ b/ebike/pkgdesc.qml @@ -0,0 +1,45 @@ +import QtQuick 2.15 + +Item { + property string pkgName: "E-Bike Pkg" + property string pkgDescriptionMd: "README.dist.md" + property string pkgLisp: "ebike.lbm" + property string pkgQml: "ui.dist.qml" + property bool pkgQmlIsFullscreen: false + property string pkgOutput: "ebike.vescpkg" + + // This function should return true when this package is compatible + // with the connected vesc-based device + function isCompatible (fwRxParams) { + var hwName = fwRxParams.hw.toLowerCase(); + var fwName = fwRxParams.fwName.toLowerCase(); + + // vesc, vesc bms or custom module + // Note that VBMS32 is a custom module + var hwType = fwRxParams.hwTypeStr().toLowerCase(); + + var major = fwRxParams.major; + var minor = fwRxParams.minor; + + //console.log("HW Name: " + hwName) + //console.log("FW Name: " + fwName) + //console.log("HW Type: " + hwType) + //console.log("Major: " + major) + //console.log("Minor: " + minor) + + // Prevent installing on VBMS + if (hwType != "vesc") { + return false + } + + if (hwName != "410" && hwName != "60" && hwName != "60_mk5") { + return false + } + + if (major != 6 || minor != 6) { + return false + } + + return true + } +} diff --git a/ebike/src_lbm/disp_bf_uart.lbm b/ebike/src_lbm/disp_bf_uart.lbm new file mode 100644 index 000000000..b915c86b7 --- /dev/null +++ b/ebike/src_lbm/disp_bf_uart.lbm @@ -0,0 +1,274 @@ +(def arr-rx (bufcreate 32)) +(def disp-pas-set 0) +(def disp-pas-set-raw 0x06) +(def disp-pas-mode -1) ; -1=9lvl, 0=5lvl eco, 1=5lvl sport +(def disp-wheel-rpm 0.0) +(def disp-update-time (systime)) +(def disp-type -1) +(def disp-nolimit-rx 0) +(def disp-light 0) +(def disp-walkmode -1) +(def disp-thr-follow -1) +(def disp-speed-rpm 0) +(def disp-error-code 0x01) + +@const-start + +; assist mapping function +(defunret pas-apply () { + (var pas-new + (match disp-pas-mode + (0 (match disp-pas-set-raw ; 5-level ECO + (0x06 -1) (0x00 0) (0x0B 1) (0x0D 2) (0x15 3) (0x17 4) (0x03 5))) + (1 (match disp-pas-set-raw ; 5-level SPORT + (0x06 -1) (0x00 0) (0x0B 6) (0x0D 7) (0x15 8) (0x17 9) (0x03 10))) + (_ { + (match disp-pas-set-raw ; 9-level + (0x06 -1) (0x00 0) (0x01 2) (0x0B 3) (0x0C 4) (0x0D 5) + (0x02 6) (0x15 7) (0x16 8) (0x17 9) (0x03 10))}))) + (if (!= pas-new disp-pas-set) { + ;(print (str-from-n disp-pas-set-raw "0x%02X")) + ;(print (str-merge (str-from-n disp-pas-set) " " (str-from-n pas-new) " " (str-from-n disp-pas-mode))) + (if (or (and (= disp-pas-set 5) (= pas-new 1) (= disp-pas-mode 0)) ; filters if pas-new arrives before disp-pas-mode + (and (= disp-pas-set 6) (= pas-new 10) (= disp-pas-mode 1))) (return t)) + (setq disp-pas-set pas-new) + (setq assist-scale + (match disp-pas-set + (-1 (read-setting 'assist-w)) + (0 0) + (_ (read-setting (str2sym (str-from-n (trunc disp-pas-set 1 10) "assist-%d"))))))})}) + +(defun disp-bafang-uart () { + (uart-start 1200) + (var arr-rx (bufcreate 32)) + + (loopwhile t { + (uart-read-bytes arr-rx 1 0) + (var byte (bufget-u8 arr-rx 0)) + + (cond + ((= byte 0x16) { ; CMD Write + (uart-read-bytes arr-rx 1 0) + (var cmd (bufget-u8 arr-rx 0)) + (cond + + ((= cmd 0x00) { ; Bigstone M210CTL - Display automatic shutoff + (print "Display turned off (timeout)") + }) + + ((= cmd 0x0C) { ; Mode eco(0x02)/sport(0x040) - tested OK + (uart-read-bytes arr-rx 2 0) + (var mode (bufget-u8 arr-rx 0)) + (var csum (bufget-u8 arr-rx 1)) + (var csum-calc (mod (+ byte cmd mode) 0xff)) + (if (= csum csum-calc) { + (setq disp-pas-mode (if (= mode 0x02) 0 1)) + (pas-apply) + }) + }) + + ((= cmd 0x0B) { ; PAS Level + (uart-read-bytes arr-rx 2 0) + (var pas (bufget-u8 arr-rx 0)) + (var csum (bufget-u8 arr-rx 1)) + (var csum-calc (mod (+ byte cmd pas) 0xff)) + (if (= csum csum-calc) { + (setq disp-pas-set-raw pas) + (pas-apply) + }) + }) + + ((= cmd 0x1A) { ; Light - tested OK + (uart-read-bytes arr-rx 1 0) + (var light (bufget-u8 arr-rx 0)) + (setq disp-light (- light 240)) + }) + + ((= cmd 0x1F) { ; Speedlimit (rpm) set in display - tested OK + (uart-read-bytes arr-rx 3 0) + (var spd-h (bufget-u8 arr-rx 0)) + (var spd-l (bufget-u8 arr-rx 1)) + (var csum (bufget-u8 arr-rx 2)) + (var csum-calc (bitwise-and (+ byte cmd spd-h spd-l) 0xff)) + (if (= csum csum-calc) { + (setq disp-speed-rpm (+ (shl spd-h 8) spd-l)) + }) + }) + + ;((= cmd 0x16) { ; From Velofox DM03 + ;}) + + ;((= cmd 0x0D) { ; From Velofox DM09 + ;}) + + ((= cmd 0x9A) { ; Bigstone M210CTL - Battery current + (uart-read-bytes arr-rx 2 0) + (var curr1 (* (bufget-u8 arr-rx 0) 0.5)) + (var curr2 (* (bufget-u8 arr-rx 1) 0.5)) + (if (and (= curr1 curr2) (or (not-eq curr1 in-current-max) (not-eq curr2 in-current-max))) { + (setq in-current-max curr1) + (conf-set 'l-in-current-max in-current-max) + }) + }) + + ((= cmd 0x9B) { ; Bigstone M210CTL - Throttle follow PAS on/off + (uart-read-bytes arr-rx 1 0) + (var thr-fl (bufget-u8 arr-rx 0)) + (setq disp-thr-follow (- thr-fl 240)) + }) + + ((= cmd 0x9C) { ; Bigstone M210CTL - Walkmode on/off + (uart-read-bytes arr-rx 1 0) + (var walk (bufget-u8 arr-rx 0)) + (setq disp-walkmode (- walk 240)) + }) + + (t (print (str-merge "Unknown: 0x16 0x" (str-from-n cmd "%02X")))) + ) + }) + + ((= byte 0x11) { ; CMD Read + (uart-read-bytes arr-rx 1 0) + (var cmd (bufget-u8 arr-rx 0)) + (cond + + ;((= cmd 0x90) { ; Read protocol version + ; (var arr-tx (bufcreate 3)) + ; (bufset-u8 arr-tx 0 0x90) + ; (bufset-u8 arr-tx 1 0x40) + ; (bufset-u8 arr-tx 1 0xD0) + ; (uart-write arr-tx) + ; (print "Display read protocol version") + ;}) + + ; untested, implemented according to BF-UART-protocol, maybe outdated/unused + ;((= cmd 0x51) { ; Switch from 1200kbps to 9600kbps + ; (print "Baud change requested") + ; (uart-read-bytes arr-rx 3 0) + ; (var baud-h (bufget-u8 arr-rx 0)) + ; (var baud-l (bufget-u8 arr-rx 1)) + ; (var csum (bufget-u8 arr-rx 2)) + ; (var csum-calc (mod (+ byte cmd baud-h baud-l) 0xff)) + ; (if (= csum csum-calc) { + ; (uart-stop) + ; (uart-start 9600) + ; (var arr-tx (bufcreate 3)) + ; (bufset-u8 arr-tx 0 0x90) + ; (bufset-u8 arr-tx 1 0x40) + ; (bufset-u8 arr-tx 1 0xD0) + ; (uart-write arr-tx) + ; (print "Switched to 9600kbps") + ; }) + ;}) + + ((= cmd 0x0A) { ; Battery Half Amps + (var arr-tx (bufcreate 2)) + (bufset-u8 arr-tx 0 (* (get-current-in) 2)) + (bufset-u8 arr-tx 1 (+ (bufget-u8 arr-tx 0))) + (uart-write arr-tx) + }) + + ((= cmd 0x08) { ; Read status + (var arr-tx (bufcreate 1)) + (bufset-u8 arr-tx 0 disp-error-code) + (uart-write arr-tx) + }) + + ((= cmd 0x11) { ; Battery SOC + (var arr-tx (bufcreate 2)) + (bufset-u8 arr-tx 0 (* (get-batt) 100.0)) + (bufset-u8 arr-tx 1 (+ (bufget-u8 arr-tx 0))) + (uart-write arr-tx) + }) + + ((= cmd 0x12) { ; Battery voltage + (var arr-tx (bufcreate 3)) + (bufset-u16 arr-tx 0 (* (get-vin) 100)) + (bufset-u8 arr-tx 2 (+ (bufget-u8 arr-tx 0) (bufget-u8 arr-tx 1) cmd)) + (uart-write arr-tx) + }) + + ((= cmd 0x20) { ; Wheel RPM + (var arr-tx (bufcreate 3)) + (bufset-u16 arr-tx 0 disp-wheel-rpm) + (bufset-u8 arr-tx 2 (+ (bufget-u8 arr-tx 0) (bufget-u8 arr-tx 1) cmd)) + (uart-write arr-tx) + (setq disp-update-time (systime)) + }) + + ;((= cmd 0x31) { ; Working status + ; ; 31 31 = 1) Current 2) RPM 3) Speed 4) Power assist pulse/torque signal + ; ; 30 30 = none if none is active + ;}) + + (t (print (str-merge "Unknown: 0x11 0x" (str-from-n cmd "%02X")))) + ) + + }) + + ((= byte 0xFF) { ; Display turned on + }) + + ((or (= byte 0x6E) (= unlock-mode 0)) { ; Remove Speedlimit + (if (eq disp-nolimit-rx 0) { + (setq disp-nolimit-rx 1) + (print "Speedlimit removed") + }) + }) + + ((= byte 0x66) { ; Display type is Topology SW102 + (if (= disp-type -1) { + (setq disp-nolimit-rx 0) + (setq disp-type 102) + (print "disp-type 102 (Topology SW102)") + }) + }) + + ((= byte 0x67) { ; Display type is Velofox DM03 + (if (= disp-type -1) { + (setq disp-nolimit-rx 0) + (setq disp-type 103) + (print "disp-type 103 (Velofox DM03)") + }) + }) + + ((= byte 0x70) { ; Display type is Bigstone M210CTL + (if (= disp-type -1) { + (setq disp-nolimit-rx 0) + (setq disp-type 104) + (print "disp-type 104 (Bigstone M210CTL)") + }) + }) + + (t (print (str-merge "Unknown: 0x" (str-from-n byte "%02X")))) + ) + }) +}) + +(defun bafang-fault-thread () { + (loopwhile t { + (var fault (get-fault)) + + (if (< (get-temp-fet) -15) { + (setq fault 30) + }) + + (if (< (get-temp-mot) -15) { + (setq fault 31) + }) + + (setq disp-error-code + (match fault + (1 0x07) ; FAULT_CODE_OVER_VOLTAGE - tested OK + (2 0x06) ; FAULT_CODE_UNDER_VOLTAGE (not shown by display) + (5 0x14) ; FAULT_CODE_OVER_TEMP_FET + (6 0x10) ; FAULT_CODE_OVER_TEMP_MOTOR + (30 0x10) ; T Fet fault + (31 0x11) ; T Mot fault - tested OK + (_ 0x01) ; FAULT_CODE_NONE + ) + ) + + (sleep 0.04) + }) +}) \ No newline at end of file diff --git a/ebike/src_lbm/eeprom.lbm b/ebike/src_lbm/eeprom.lbm new file mode 100644 index 000000000..fd6aa3147 --- /dev/null +++ b/ebike/src_lbm/eeprom.lbm @@ -0,0 +1,172 @@ +; -- tables +(def eeprom-addrs '( (ver-code . (0 i)) (assist-w . (1 f)) + (assist-1 . (2 f)) (assist-2 . (3 f)) (assist-3 . (4 f)) (assist-4 . (5 f)) (assist-5 . (6 f)) + (assist-6 . (7 f)) (assist-7 . (8 f)) (assist-8 . (9 f)) (assist-9 . (10 f)) (assist-10 . (11 f)) + (assist-type . (12 i)) (brake-mode . (13 b)) (pdl-trq-offset . (14 f)) (pdl-trq-const . (15 f)) + (unlock-mode . (16 i)) (spd-lmt-pdl . (17 f)) (spd-lmt-thr . (18 f)) (spd-snr-detect . (19 b)) (time-pwr-off . (20 f)) (time-unlock . (21 f)) +)) + +; -- vars +(def settings-version 41i32) +(def conf-auto-store t) + +; -- helpers +(defun eep-spec (name) (assoc eeprom-addrs name)) + +; -- read eeprom/conf +(defun read-setting (name) + (let ((spec (eep-spec name))) + (if spec + (let ((addr (first spec)) + (type (second spec))) + (cond + ((eq type 'i) (eeprom-read-i addr)) + ((eq type 'f) (eeprom-read-f addr)) + ((eq type 'b) + (let ((x (eeprom-read-i addr))) + (if x (!= x 0) nil))) + (t nil))) + (conf-get name)))) + +; -- write eeprom/conf +(defun write-setting (name val) + (let ((spec (eep-spec name))) + (if spec + (let ((addr (first spec)) + (type (second spec))) + (cond + ((eq type 'i) (eeprom-store-i addr val)) + ((eq type 'f) (eeprom-store-f addr val)) + ((eq type 'b) (eeprom-store-i addr (if val 1 0))) + (t nil))) { + (conf-set name val) ; (if (or (eq val t) (eq val 'true)) 1 (if (eq val nil) 0 val) + (if conf-auto-store (conf-store))}))) ; persist, stops motor + +; -- send config to QML +(defun send-settings () + (let* ((assist-idx (trunc disp-pas-set 1 10)) + (assist-sym (str2sym (str-from-n assist-idx "assist-%d"))) + (assist-val (read-setting assist-sym))) + (send-data + (str-merge + "settings " + ; what level and its value + (str-from-n assist-idx "%d ") ; current PAS index [1..10] + (str-from-n assist-val "%.3f ") ; current assist value + + ; core eeprom/conf (order stable for QML) + (str-from-n (read-setting 'assist-w) "%.3f ") + (str-from-n (read-setting 'assist-type) "%d ") + (b->s (read-setting 'brake-mode)) + (str-from-n (read-setting 'pdl-trq-offset) "%.3f ") + (str-from-n (read-setting 'pdl-trq-const) "%.3f ") + (str-from-n (read-setting 'unlock-mode) "%d ") + (str-from-n (read-setting 'spd-lmt-pdl) "%.2f ") + (str-from-n (read-setting 'spd-lmt-thr) "%.2f ") + (b->s (read-setting 'spd-snr-detect)) + (str-from-n (read-setting 'time-pwr-off) "%.2f ") + (str-from-n (read-setting 'time-unlock) "%d ") + + ; VESC conf-backed items (read-setting falls back to conf-get) + ;(str-from-n (read-setting 'm-motor-temp-sens-type) "%d ") + ;(str-from-n (read-setting 'si-wheel-diameter) "%.3f ") + ;(str-from-n (read-setting 'si-battery-cells) "%d ") + ;(str-from-n (read-setting 'si-battery-ah) "%.2f ") + ;(str-from-n (read-setting 'l-battery-cut-start) "%.2f ") + ;(str-from-n (read-setting 'l-battery-cut-end) "%.2f ") + ;(str-from-n (read-setting 'l-in-current-max) "%.2f ") + ;(str-from-n (read-setting 'l-current-max) "%.2f ") + ;(str-from-n (read-setting 'l-watt-max) "%d ") + )))) + +; -- ouput all settings +(defun print-settings () + (loopforeach it eeprom-addrs + (print (list (first it) (read-setting (first it)))))) + +; -- write all settings +(defun save-settings ( ver-code assist-w + assist-1 assist-2 assist-3 assist-4 assist-5 + assist-6 assist-7 assist-8 assist-9 assist-10 + assist-type brake-mode pdl-trq-offset pdl-trq-const + unlock-mode spd-lmt-pdl spd-lmt-thr spd-snr-detect time-pwr-off time-unlock) { + ;temp-snr-mot wheel-diam battery-cells battery-ah battery-cut-start battery-cut-end + ;in-current-max current-max watt-max ) { + (write-setting 'assist-1 assist-w) + (write-setting 'assist-1 assist-1) + (write-setting 'assist-2 assist-2) + (write-setting 'assist-3 assist-3) + (write-setting 'assist-4 assist-4) + (write-setting 'assist-5 assist-5) + (write-setting 'assist-6 assist-6) + (write-setting 'assist-7 assist-7) + (write-setting 'assist-8 assist-8) + (write-setting 'assist-9 assist-9) + (write-setting 'assist-10 assist-10) + (write-setting 'assist-type assist-type) + (write-setting 'brake-mode brake-mode) + (write-setting 'pdl-trq-offset pdl-trq-offset) + (write-setting 'pdl-trq-const pdl-trq-const) + (write-setting 'unlock-mode unlock-mode) + (write-setting 'spd-lmt-pdl spd-lmt-pdl) + (write-setting 'spd-lmt-thr spd-lmt-thr) + (write-setting 'spd-snr-detect spd-snr-detect) + (write-setting 'time-pwr-off time-pwr-off) + (write-setting 'time-unlock time-unlock) + ;(setq conf-auto-store nil) + ;(write-setting 'm-motor-temp-sens-type temp-snr-mot) + ;;(write-setting 'si-gear-ratio gear-ratio) + ;(write-setting 'si-wheel-diameter wheel-diam) + ;(write-setting 'si-battery-cells battery-cells) + ;(write-setting 'si-battery-ah battery-ah) + ;(write-setting 'l-battery-cut-start battery-cut-start) + ;(write-setting 'l-battery-cut-end battery-cut-end) + ;(write-setting 'l-in-current-max in-current-max) + ;(write-setting 'l-current-max current-max) + ;(setq conf-auto-store t) + ;(write-setting 'l-watt-max watt-max) + (print "Settings Saved!") +}) + +; -- restore default settings values +(defun restore-settings () { + (write-setting 'ver-code settings-version) + (write-setting 'assist-w 0.05) ; scale for walkmode, 0=disabled + (write-setting 'assist-1 0.1) + (write-setting 'assist-2 0.2) + (write-setting 'assist-3 0.3) + (write-setting 'assist-4 0.4) + (write-setting 'assist-5 0.5) + (write-setting 'assist-6 0.6) + (write-setting 'assist-7 0.7) + (write-setting 'assist-8 0.8) + (write-setting 'assist-9 0.9) + (write-setting 'assist-10 1.0) + (write-setting 'assist-type 0) ; 0=9lvl [1-2-3-4-5-6-7-8-9], 1=5lvl [3-5-7-9-10], 2=5+5lvl [3-5-7-9-10-3-5-7-9-10] + (write-setting 'brake-mode nil) ; 0=NO (closer), 1=NC (opener) + (write-setting 'pdl-trq-offset 0.8) ; TODO: implement calibration (measure, median, subtract & apply) + (write-setting 'pdl-trq-const 32.0) ; Nm/V ; TB33=472Nm/V 07.9=320Nm/V ; 1800Nm/V + (write-setting 'unlock-mode 1) ; 0=unlocked, 1=0x6E, (TODO: 2=walk-mode, 3=assist-combo) + (write-setting 'spd-lmt-pdl 26.0) ; speed limit for pedal + (write-setting 'spd-lmt-thr 6.0) ; speed limit for throttle + (write-setting 'spd-snr-detect nil) ; detect missing speedsensor + (write-setting 'time-pwr-off 2.0) ; time to release pwr-hold (-1=disabled, controller stays on) + (write-setting 'time-unlock 10) ; time to unlock after boot (only unlock-mode 2+3) + ;(setq conf-auto-store nil) + ;(write-setting 'm-motor-temp-sens-type 2) ; Motor temp sensor type + ;;(write-setting 'si-gear-ratio 3.0) ; relying on external speed sensor instead + ;(write-setting 'si-wheel-diameter 0.748) ; 0.748=circumference of 2350mm + ;(write-setting 'si-battery-cells 14) ; 14=52V, 13=48V, 12=43V, .. + ;(write-setting 'si-battery-ah 17.5) ; battery capacity in Ah + ;(write-setting 'l-battery-cut-start 10) ; start cutting out in A + ;(write-setting 'l-battery-cut-end 8) ; end cutting out in A + ;(write-setting 'l-in-current-max 45) ; max battery current in A + ;(write-setting 'l-current-max 90) ; max motor current in A + ;(setq conf-auto-store t) + ;(write-setting 'l-watt-max 3000) ; max wattage in A + (print "Settings Restored!") +}) + +; Restore settings if version number does not match +; as that probably means something else is in eeprom +(if (not-eq (read-setting 'ver-code) settings-version) (restore-settings)) \ No newline at end of file diff --git a/ebike/src_lbm/logger.lbm b/ebike/src_lbm/logger.lbm new file mode 100644 index 000000000..5837423bf --- /dev/null +++ b/ebike/src_lbm/logger.lbm @@ -0,0 +1,130 @@ +(defun t-bms (sensor) + (if (< sensor (get-bms-val 'bms-temp-adc-num)) + (get-bms-val 'bms-temps-adc sensor) + -1 + ) +) + +; State +(def log-running false) +(def last-can-id -1) + +; Variables +(def esp-id 124) ; C3-Supermini CAN-ID +(def fw-cap 1048576) ; 1 MiB +(def fw-ofs 0) ; current write offset on ESP +(def buf (bufcreate 448)) ; batching buffer for efficiency +(def blen 0) ; buffered payload length + +; Format +; (optKey optName optUnit optPrecision optIsRel optIsTime value-function) +; +; All entries except value-function are optional and +; default values will be used if they are left out. +(def loglist-local '( + ("Input Voltage" "V" (get-vin)) + ("Current" "A" (get-current)) + ("Current In" "A" (get-current-in)) + ("Duty" (get-duty)) + ("RPM" (get-rpm)) + ("Temp Fet" "degC" 1 (get-temp-fet)) + ("Temp Motor" "degC" 1 (get-temp-mot)) + ("Batt" "%" (* (get-batt) 100)) + ("fault" (get-fault)) + ("trip_vesc" "m" (get-dist)) + ("trip_vesc_abs" "m" (get-dist-abs)) + ("cnt_ah" "Ah" "Amp Hours" (get-ah)) + ("cnt_wh" "Wh" "Watt Hours" (get-wh)) + ("cnt_ah_chg" "Ah" "Ah Chg" (get-ah-chg)) + ("cnt_wh_chg" "Wh" "Wh Chg" (get-wh-chg)) + ("ADC1" "V" (get-adc 0)) + ("ADC2" "V" (get-adc 1)) + ("Pedal Torque" "Nm" (* 1 pdl-trq)) + ("Pedal Out" (* 1 trq-out)) + ("Pedal RPM" (* 1 pdl-rpm)) + ("Wheel RPM" (* 1 wheel-rpm)) + ("Wheel kmh" (* 1 wheel-kmh)) + ("PAS Set" (* 1 disp-pas-set)) + ;("BMS T Mos" "degC" (t-bms 0)) + ;("BMS T2" "degC" (t-bms 1)) + ;("BMS T3" "degC" (t-bms 2)) + ;("BMS Current" "A" (get-bms-val 'bms-i-in-ic)) + ("Current limit" (conf-get 'l-current-max-scale)) +)) + +(defun loglist-parse (id lst res-fun) + (looprange row 0 (length lst) + (let ( + (field (ix lst row)) + (get-field + (fn (type default) + (let ((f (first field))) + (if (eq (type-of f) type) + (progn + (setvar 'field (rest field)) + f + ) + default + )))) + (key (get-field type-array (str-from-n row "Field %d"))) + (unit (get-field type-array "")) + (name (get-field type-array key)) + (precision (get-field type-i 2)) + (is-rel (get-field type-symbol false)) + (is-time (get-field type-symbol false)) + ) + (res-fun + id ; CAN id + row ; Field + key ; Key + name ; Name + unit ; Unit + precision ; Precision + is-rel ; Is relative + is-time ; Is timestamp + ) +))) + +; Confiure all log fields based on loglist lst +(defun log-configure (id lst) (loglist-parse id lst 'log-config-field)) + +(defun log-thd (id rate lst) + (loopwhile log-running { + (log-send-f32 id 0 + (map + (fn (x) (eval (ix x -1))) + lst + ) + ) + (sleep (/ 1.0 rate))})) + +(defun start-log (id append-gnss log-local log-can log-bms rate) { + ;(setq id esp-id) + + (def last-can-id id) + (stop-log id) + + (def loglist loglist-local) + + (log-configure id loglist) + + (log-start + id ; CAN id + (length loglist) ; Field num + rate ; Rate Hz + true ; Append time + append-gnss ; Append gnss + ) + + (def log-running true) + (def log-thd-id (spawn log-thd id rate loglist)) + (send-data "Log Started")}) + +(defun stop-log (id) { + (log-stop id) + (if log-running { + (def log-running false) + (wait log-thd-id) + (send-data "Log stopped")})}) + +(defun send-settings () nil) \ No newline at end of file diff --git a/ebike/src_lbm/main.lbm b/ebike/src_lbm/main.lbm new file mode 100644 index 000000000..89b2151d9 --- /dev/null +++ b/ebike/src_lbm/main.lbm @@ -0,0 +1,283 @@ +(import "eeprom.lbm" 'code-eeprom) +(read-eval-program code-eeprom) +(import "utils.lbm" 'code-utils) +(read-eval-program code-utils) + +; Variables (EEPROM) +(def assist-type (read-setting 'assist-type)) +(def assist-scale 0.0) +(def brake-mode (read-setting 'brake-mode)) +(def pdl-trq-offset (read-setting 'pdl-trq-offset)) +(def pdl-trq-const (read-setting 'pdl-trq-const)) +(def unlock-mode (read-setting 'unlock-mode)) +(def spd-lmt-pdl (read-setting 'spd-lmt-pdl)) +(def spd-lmt-thr (read-setting 'spd-lmt-thr)) +(def spd-snr-detect (read-setting 'spd-snr-detect)) +(def time-pwr-off (read-setting 'time-pwr-off)) +(def time-unlock (read-setting 'time-unlock)) +(def temp-snr-mot (read-setting 'm-motor-temp-sens-type)) +;(def gear-ratio (read-setting 'si-gear-ratio)) ; relying on external speed sensor instead +(def wheel-diam (read-setting 'si-wheel-diameter)) +(def battery-cells (read-setting 'si-battery-cells)) +(def battery-ah (read-setting 'si-battery-ah)) +(def in-current-max (read-setting 'l-in-current-max)) + + +; Variables (temp) +(def pas-pulses 0) ; PAS pulse counter +(def pas-ppr 32) ; PAS-pulses per revolution +(def pas-last-pulse-time 0) ; Last time a PAS-pulse was received +(def pdl-rpm 0.0) ; Calculated pedal RPM +(def pdl-trq 0.0) +(def trq-out 0.0) +(def trq-filter 0.0) +(def trq-sample-len 8) ; Number of torque samples to average over +(def trq-samples (map (fn (x) 0) (range trq-sample-len))) +(def trq-sample-ind 0) +(def thr-zero-time (systime)) +(def boot-zero-time (systime)) +(def wheel-kmh 0.0) +(def wheel-rpm 0.0) +(def speed-lim-now 20.0) + +(def assist-mode-duty 0.1) +(def assist-duty-rate 0.05) ; Duty ramp rate, 0.0 to 1.0, lower is slower +(def assist-duty-state 0.0) + +(def pwr-out 1.0) +(def pwr-out-thr 1.0) + +@const-start + +(import "disp_bf_uart.lbm" 'code-disp) +(read-eval-program code-disp) +;(import "logger.lbm" 'code-logger) +;(read-eval-program code-logger) + +(defun proc-icu (period) { ; Called when PAS-pulses are received. ; The timer counts at 10000 Hz + (var rpm (/ (/ 600000.0 pas-ppr) period)) + (if (> rpm 120.0) (setq rpm 120.0)) + (setq pdl-rpm (lpf pdl-rpm rpm 0.5)) + (setq pas-pulses (+ pas-pulses 1)) + (setq pas-last-pulse-time (systime)) + + (setix trq-samples trq-sample-ind pdl-trq) + (setq trq-sample-ind (mod (+ trq-sample-ind 1) (length trq-samples))) +}) + +(defun update-vt () { + (def vt-disp-pas-mode disp-pas-mode) + (def vt-disp-pas-set disp-pas-set) + (def vt-disp-type disp-type) + (def vt-disp-nolimit-rx disp-nolimit-rx) + (def vt-unlock-mode unlock-mode) + (def vt-spd-lmt-pdl spd-lmt-pdl) + (def vt-assist-scale assist-scale) + (def vt-disp-light disp-light) + (def vt-disp-walkmode disp-walkmode) + (def vt-disp-thr-follow disp-thr-follow) + (def vt-battery-curr in-current-max) + + (def vt-pdl-rpm pdl-rpm) + (def vt-wheel-rpm wheel-rpm) + (def vt-trq-out trq-out) + (def vt-trq-pedal pdl-trq) +}) + +(defun event-handler () + (loopwhile t + (recv + ((event-icu-period . ((? width) . (? period))) (proc-icu period)) + ((event-data-rx . (? data)) (eval (read data))) + (event-shutdown (stop-log last-can-id)) + (_ nil) +))) + + +(defun main () { + (icu-start 10000 1) + (event-register-handler (spawn event-handler)) + (event-enable 'event-icu-period) + (event-enable 'event-data-rx) + (event-enable 'event-shutdown) + + (var pas-last disp-pas-set) + (loopwhile-thd ("worker" 120) t { + ; Update PAS RPM while no new pulses are coming. From the last + ; pulse we can always calculate an upper bound on the RPM. + (var elapsed (secs-since pas-last-pulse-time)) + (if (> elapsed 0.1) { + (var rpm (/ 60.0 pas-ppr elapsed)) + (if (< rpm pdl-rpm) (setq pdl-rpm rpm)) + }) + (sleep 0.02) + }) + + (loopwhile-thd ("SpeedTorque" 150) t { + (var wheel-age (speed-age)) + (var wheel-last (speed-last-time)) + + (if (and (> wheel-last 0.0) (> wheel-age 0.0)) { + (var rpm (/ 60.0 wheel-last)) + (var rpm-max (/ 60.0 wheel-age)) + (var wheel-rpm (if (> rpm rpm-max) rpm-max rpm)) + (if (> wheel-rpm 10) + (setq disp-wheel-rpm wheel-rpm) + (setq disp-wheel-rpm 0.0) + ) + (setq wheel-kmh (* 3.6 wheel-diam 3.14159 (/ wheel-rpm 60.0))) + (override-speed 1 (/ wheel-kmh 3.6)) + }) + + ; Read trq from ADC + (var trq (* pdl-trq-const (- (get-adc 1) pdl-trq-offset))) + (setq pdl-trq (if (> trq 0.0) trq 0.0)) + (def vt-trq-raw (get-adc 1)) + + ; Lights + (if (= disp-light 1) + { (set-aux 1 1) (set-aux 2 1) } + { (set-aux 1 0) (set-aux 2 0) } + ) + + ; Unlock if mode=2, walk-mode visited and within time-unlock + ;(if (and (> time-unlock (secs-since boot-zero-time)) (= ;disp-pas-set -1) (= disp-nolimit-rx 0) (= unlock-mode 2)) { + ; (setq disp-nolimit-rx 1) + ; (print "Speedlimit removed") + ;}) + + ; Shut off after display stops responding + ;(if (and (> (secs-since disp-update-time) time-pwr-off) (!= time-pwr-off -1)) (pwr-hold 0)) + + (sleep 0.02) + }) + + (loopwhile-thd 120 t { ; Update speed limits for throttle and PAS + (if (< wheel-kmh spd-lmt-pdl) + (setq pwr-out (lpf pwr-out 1.0 0.1)) + (setq pwr-out (lpf pwr-out 0.0 0.1)) + ) + + (if (< wheel-kmh spd-lmt-thr) + (setq pwr-out-thr (lpf pwr-out-thr 1.0 0.1)) + (setq pwr-out-thr (lpf pwr-out-thr 0.0 0.1)) + ) + + (sleep 0.04) + }) + + (loopwhile-thd 120 t { ; PAS and brake-ramp thread + (if (or (if brake-mode (not (> (read-brake) 0)) (> (read-brake) 0)) (= disp-pas-set 0)) + { ; Brake is pressed + (app-disable-output -1) + + (set-current-rel 0.0) + + (setq trq-out 0.0) + (setq trq-filter 0.0) + (setq disp-error-code 0x3) ; info for disp, that braking + + ; Prevent current spike when releasing the brake + (app-adc-detach 1 1) + (app-adc-override 0 (conf-get 'adc-v1-start)) + } + { ; No brake is pressed + (app-adc-detach 0 0) + + (setq trq-filter (lpf trq-filter pdl-trq 0.1)) + ;(setq trq-filter pdl-trq) + + (if (< pdl-rpm 6) + (setq trq-out 0.0) + (if (< pdl-rpm 25) + { + (setq trq-out trq-filter) + (map (fn (x) (setix trq-samples x trq-out)) (range (length trq-samples))) + } + { + (setq trq-out (/ (apply + trq-samples) (length trq-samples))) + } + )) + + (var trq01 (trunc (/ trq-out 60.0) 0.0 1.0)) + + (if (and + (< wheel-kmh 10.0) + (= disp-pas-set -1) + (< (secs-since disp-update-time) 2) + ) + { ; walk-mode + (setq assist-duty-state (lpf assist-duty-state assist-mode-duty assist-duty-rate)) + (app-disable-output -1) + (set-duty assist-duty-state) + } + { + (setq assist-duty-state 0.0) + (if (> (+ trq-out 0.02) (get-adc-decoded 0)) ; torque vs throttle + { + (if (= disp-nolimit-rx 1) + (conf-set 'l-current-max-scale pwr-out) + (conf-set 'l-current-max-scale 1.0) + ) + + (app-disable-output -1) + (set-current-rel (* trq01 assist-scale)) + (app-adc-override 0 (conf-get 'adc-v1-start)) + } + { + (if (= disp-nolimit-rx 1) + (conf-set 'l-current-max-scale pwr-out-thr) + (conf-set 'l-current-max-scale 1.0) + ) + + ; Avoid glitches when holding full throttle + (if (< (conf-get 'l-current-max-scale) 0.01) + { + (app-disable-output -1) + (set-current-rel 0.0) + (app-adc-override 0 (conf-get 'adc-v1-start)) + } + { + (app-disable-output 0) + } + ) + } + ) + } + ) + } + ) + + (sleep 0.02) + }) + + (loopwhile-thd ("BafangUart" 200) t { ; Thread for communication to Bafang UART displays + (print "Starting BafangUart-thread") + + (match (trap (disp-bafang-uart)) + ((exit-ok (? a)) (print "BafangUart-thread exit")) + (_ (print "BafangUart-thread crashed")) + ) + + (sleep 5.0) + }) + + (loopwhile-thd ("BafangFault" 200) t { ; Mapping VESC and Bafang error codes + (print "Starting BafangFault-thread") + + (match (trap (bafang-fault-thread)) + ((exit-ok (? a)) (print "BafangFault-thread exit")) + (_ (print "BafangFault-thread crashed")) + ) + + (sleep 5.0) + }) + + (loopwhile-thd 120 t { ; Thread to update VT-* variables (for debugging) + (update-vt) + (sleep 0.1) + }) +}) + +(image-save) +(main) diff --git a/ebike/src_lbm/utils.lbm b/ebike/src_lbm/utils.lbm new file mode 100644 index 000000000..5d599bbf5 --- /dev/null +++ b/ebike/src_lbm/utils.lbm @@ -0,0 +1,17 @@ +; +; various helper functions +; + +(defun lpf (val sample fconst) + (- val (* fconst (- val sample))) +) + +(defun trunc (val min max) + (cond ((< val min) min) ((> val max) max) (t val)) +) + +(defun max (a b) (if (> a b) a b)) + +(defun send-msg (text) ; emit status message in QML + (send-data (str-merge "msg " text)) +) \ No newline at end of file diff --git a/ebike/tests/run_tests.py b/ebike/tests/run_tests.py new file mode 100755 index 000000000..1823c50a4 --- /dev/null +++ b/ebike/tests/run_tests.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +E-Bike Pkg Smoke Tests +Tests pure functions to catch regressions before flashing hardware +""" + +import sys + +# Test counters +test_passes = 0 +test_failures = 0 + +def assert_eq(actual, expected, test_name): + """Assert that actual equals expected""" + global test_passes, test_failures + if actual == expected: + test_passes += 1 + print(f"✓ {test_name}") + else: + test_failures += 1 + print(f"✗ {test_name}") + print(f" Expected: {expected}") + print(f" Actual: {actual}") + +def assert_near(actual, expected, tolerance, test_name): + """Assert that actual is within tolerance of expected""" + global test_passes, test_failures + if abs(actual - expected) <= tolerance: + test_passes += 1 + print(f"✓ {test_name}") + else: + test_failures += 1 + print(f"✗ {test_name}") + print(f" Expected: {expected} ± {tolerance}") + print(f" Actual: {actual}") + +# ============================================================================= +# Pure Function Implementations (mirroring ebike.lbm) +# ============================================================================= + +SPEED_REVERSE_THRESHOLD = 5 + +def clamp(value, min_val, max_val): + """Clamp value between min and max""" + if value < min_val: + return min_val + elif value > max_val: + return max_val + else: + return value + +def validate_boolean(value): + """Convert to boolean (0 or 1)""" + # In LispBM, (> value 0) returns true for positive numbers + # -1 is NOT > 0, so it returns 0 + return 1 if value > 0 else 0 + +def state_name_for(state): + """Map state number to name""" + STATE_OFF = 0 + STATE_COUNTING_CLICKS = 1 + STATE_PRESSED = 2 + STATE_GOING_OFF = 3 + STATE_UNINITIALIZED = 4 + + mapping = { + STATE_OFF: "Off", + STATE_COUNTING_CLICKS: "CountingClicks", + STATE_PRESSED: "Pressed", + STATE_GOING_OFF: "GoingOff", + STATE_UNINITIALIZED: "Init" + } + return mapping.get(state, "Unknown") + +def speed_percentage_at(speed_index): + """Get speed percentage for given index""" + speed_set = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100] + max_index = len(speed_set) - 1 + clamped = clamp(speed_index, 0, max_index) + return speed_set[clamped] + +def calculate_rpm(speed_index, divisor, max_erpm=50000): + """Calculate RPM for given speed index""" + speed_percent = speed_percentage_at(speed_index) + # Formula: (* (/ max_erpm divisor) speed_percent) + # speed_percent is 0-100, not 0-1 + base_rpm = (max_erpm / divisor) * (speed_percent / 100.0) + + if speed_index < SPEED_REVERSE_THRESHOLD: + return -base_rpm + else: + return base_rpm + +# ============================================================================= +# Test Suites +# ============================================================================= + +def test_clamp(): + """Test clamp function""" + print("\n=== Testing clamp ===") + + # Within range + assert_eq(clamp(5, 0, 10), 5, "clamp: value within range") + + # Below minimum + assert_eq(clamp(-5, 0, 10), 0, "clamp: value below min") + + # Above maximum + assert_eq(clamp(15, 0, 10), 10, "clamp: value above max") + + # At boundaries + assert_eq(clamp(0, 0, 10), 0, "clamp: value at min boundary") + assert_eq(clamp(10, 0, 10), 10, "clamp: value at max boundary") + + # Negative range + assert_eq(clamp(-5, -10, -1), -5, "clamp: value in negative range") + assert_eq(clamp(-15, -10, -1), -10, "clamp: value below negative min") + +def test_validate_boolean(): + """Test validate_boolean function""" + print("\n=== Testing validate_boolean ===") + + # Valid boolean values + assert_eq(validate_boolean(0), 0, "validate_boolean: 0 -> 0") + assert_eq(validate_boolean(1), 1, "validate_boolean: 1 -> 1") + + # Non-standard truthy values + assert_eq(validate_boolean(5), 1, "validate_boolean: 5 -> 1") + assert_eq(validate_boolean(100), 1, "validate_boolean: 100 -> 1") + + # Negative values (> operator: -1 is NOT > 0, so returns 0) + assert_eq(validate_boolean(-1), 0, "validate_boolean: -1 -> 0") + +def test_state_name_for(): + """Test state_name_for function""" + print("\n=== Testing state_name_for ===") + + # Valid states + assert_eq(state_name_for(0), "Off", "state_name_for: STATE_OFF") + assert_eq(state_name_for(1), "CountingClicks", "state_name_for: STATE_COUNTING_CLICKS") + assert_eq(state_name_for(2), "Pressed", "state_name_for: STATE_PRESSED") + assert_eq(state_name_for(3), "GoingOff", "state_name_for: STATE_GOING_OFF") + assert_eq(state_name_for(4), "Init", "state_name_for: STATE_UNINITIALIZED") + + # Invalid state + assert_eq(state_name_for(99), "Unknown", "state_name_for: invalid state") + +def test_speed_percentage_at(): + """Test speed_percentage_at function""" + print("\n=== Testing speed_percentage_at ===") + + # Valid indices + assert_eq(speed_percentage_at(0), 0, "speed_percentage_at: index 0") + assert_eq(speed_percentage_at(5), 50, "speed_percentage_at: index 5") + assert_eq(speed_percentage_at(10), 100, "speed_percentage_at: index 10") + + # Out of bounds (should clamp) + assert_eq(speed_percentage_at(-1), 0, "speed_percentage_at: negative index clamped to 0") + assert_eq(speed_percentage_at(99), 100, "speed_percentage_at: large index clamped to max") + +def test_calculate_rpm(): + """Test calculate_rpm function""" + print("\n=== Testing calculate_rpm ===") + + # Forward speeds (above SPEED_REVERSE_THRESHOLD) + assert_near(calculate_rpm(5, 1), 25000, 0.1, "calculate_rpm: speed 5, divisor 1") + assert_near(calculate_rpm(10, 1), 50000, 0.1, "calculate_rpm: speed 10 (max), divisor 1") + assert_near(calculate_rpm(5, 2), 12500, 0.1, "calculate_rpm: speed 5, divisor 2") + + # Reverse speeds (below SPEED_REVERSE_THRESHOLD) + assert_near(calculate_rpm(0, 1), 0, 0.1, "calculate_rpm: speed 0 (reverse range, 0 RPM)") + assert_near(calculate_rpm(2, 1), -10000, 0.1, "calculate_rpm: speed 2 (reverse)") + + # Edge case at threshold + assert_near(calculate_rpm(4, 1), -20000, 0.1, "calculate_rpm: speed 4 (just below threshold)") + assert_near(calculate_rpm(5, 1), 25000, 0.1, "calculate_rpm: speed 5 (at threshold, forward)") + +# ============================================================================= +# Test Runner +# ============================================================================= + +def run_all_tests(): + """Run all test suites""" + print("\n╔══════════════════════════════════════════╗") + print("║ E-Bike Pkg Smoke Tests ║") + print("╚══════════════════════════════════════════╝") + + test_clamp() + test_validate_boolean() + test_state_name_for() + test_speed_percentage_at() + test_calculate_rpm() + + print("\n╔══════════════════════════════════════════╗") + print(f"║ Results: {test_passes} passed, {test_failures} failed") + print("╚══════════════════════════════════════════╝\n") + + if test_failures > 0: + print("FAILED: Some tests did not pass") + return 1 + else: + print("SUCCESS: All tests passed!") + return 0 + +if __name__ == "__main__": + sys.exit(run_all_tests()) diff --git a/ebike/tools/generate_lut_binary.py b/ebike/tools/generate_lut_binary.py new file mode 100755 index 000000000..2716ba0b8 --- /dev/null +++ b/ebike/tools/generate_lut_binary.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Generate binary file from display lookup table for efficient import. + +The binary format is optimized for direct memory access without parsing. +It can be imported using the `import` statement and then loaded as a byte array. + +Format: + Header (8 bytes): + magic: 0x4C555444 (ASCII "LUTD") + version: u16 (1) + num_frames: u16 + + Frame data (num_frames * 16 bytes): + Each frame is 16 bytes (8 column pairs of low/high bytes) + +Usage:: + python tools/generate_lut_binary.py +""" +from __future__ import annotations + +import csv +import struct +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +ASSET_DIR = REPO_ROOT / "assets" +GENERATED_DIR = REPO_ROOT / "generated" +DISPLAY_CSV = ASSET_DIR / "display_lut.csv" +DISPLAY_BIN = GENERATED_DIR / "display_lut.bin" + +# Magic number for display LUT binary file +MAGIC_DISPLAY = 0x4C555444 # ASCII "LUTD" +VERSION = 1 + + +def generate_display_binary() -> None: + """Generate binary file from display LUT CSV.""" + # Read all frames from CSV + frames: list[tuple[int, list[int]]] = [] + with DISPLAY_CSV.open(newline="") as f: + reader = csv.DictReader(f) + for row in reader: + # Extract 16 bytes per frame + index = int(row["index"]) + frame_bytes = [int(row[f"b{i}"]) for i in range(16)] + frames.append((index, frame_bytes)) + + # Sort by index to ensure correct order + # (CSV should already be sorted, but let's be explicit) + frames.sort(key=lambda item: item[0]) + + # Write binary file + GENERATED_DIR.mkdir(exist_ok=True) + with DISPLAY_BIN.open('wb') as f: + # Write header + f.write(struct.pack(' None: + """Generate both binary files.""" + generate_display_binary() + + +if __name__ == "__main__": + main() diff --git a/ebike/tools/preview_display.py b/ebike/tools/preview_display.py new file mode 100755 index 000000000..649d5ccac --- /dev/null +++ b/ebike/tools/preview_display.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Preview or export display frames from ``assets/display_lut.csv``. + +Examples +-------- +List the available display names:: + + python tools/preview_display.py --list + +Preview the first frame (index 0) in the terminal:: + + python tools/preview_display.py --index 0 + +Export the "Display Battery" frame with rotation 2 to ``battery.pgm``:: + + python tools/preview_display.py --name "Display Battery" --rotation 2 --output battery.pgm + +The exported file uses the simple ASCII Portable Gray Map (PGM) format so it can +be opened by most image viewers or further processed in scripts without extra +Python dependencies. +""" +from __future__ import annotations + +import argparse +import csv +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, List + +REPO_ROOT = Path(__file__).resolve().parents[1] +ASSET_PATH = REPO_ROOT / "assets" / "display_lut.csv" + + +@dataclass +class DisplayFrame: + index: int + name: str + rotation: int + bytes: List[int] + + @classmethod + def from_row(cls, row: dict[str, str]) -> "DisplayFrame": + data = [int(row[f"b{i}"], 10) for i in range(16)] + return cls(index=int(row["index"], 10), + name=row["name"].strip(), + rotation=int(row["rotation"], 10), + bytes=data) + + def columns(self) -> List[int]: + """Return the high-byte of each column (odd-indexed entries). + + The firmware stores display data as interleaved low/high bytes. Only the + high byte carries the 8 pixel rows we need for visualization, so we read + positions 1, 3, ..., 15 as columns. + """ + return [self.bytes[i] for i in range(1, 16, 2)] + + def render_rows(self) -> List[str]: + """Render display as ASCII art with 90° clockwise rotation. + + The hardware stores 8 column bytes (high bytes at odd indices). + Each column byte has 8 bits representing pixels vertically (MSB=top). + + To rotate 90° clockwise with correct mapping: + - Column N (left to right) becomes row N (top to bottom) + - Within each column: position 0 checks bit 7 (MSB) + - Positions 1-7 check bits 0-6 respectively + """ + cols = self.columns() # 8 column bytes, left to right + rows: List[str] = [] + + # Read columns from left to right (0→7) to form rows top to bottom + # Position 0 uses bit 7, positions 1-7 use bits 0-6 + for col in cols: + rows.append(''.join( + '#' if (col >> pos) & 1 else '.' + for pos in [7, *range(7)] # bit 7 first, then bits 0-6 + )) + + return rows + + +def load_frames(path: Path) -> List[DisplayFrame]: + frames: List[DisplayFrame] = [] + with path.open(newline="") as f: + reader = csv.DictReader(f) + required = {"index", "name", "rotation"} | {f"b{i}" for i in range(16)} + missing = required - set(reader.fieldnames or []) + if missing: + raise ValueError(f"CSV file is missing columns: {sorted(missing)}") + for row in reader: + frames.append(DisplayFrame.from_row(row)) + return frames + + +def list_names(frames: Iterable[DisplayFrame]) -> None: + by_name: dict[str, set[int]] = {} + for frame in frames: + by_name.setdefault(frame.name, set()).add(frame.rotation) + for name in sorted(by_name): + rotations = ', '.join(str(r) for r in sorted(by_name[name])) + print(f"{name} (rotations: {rotations})") + + +def select_frame(frames: Iterable[DisplayFrame], *, index: int | None, name: str | None, + rotation: int | None) -> DisplayFrame: + if index is not None: + for frame in frames: + if frame.index == index: + return frame + raise SystemExit(f"No frame with index {index} found") + if name is None: + raise SystemExit("Either --index or --name must be provided") + candidates = [frame for frame in frames if frame.name.lower() == name.lower()] + if not candidates: + raise SystemExit(f"No frame found with name '{name}'") + if rotation is None: + if len({frame.rotation for frame in candidates}) > 1: + raise SystemExit("Multiple rotations available. Please specify --rotation.") + return candidates[0] + for frame in candidates: + if frame.rotation == rotation: + return frame + available = ', '.join(str(frame.rotation) for frame in candidates) + raise SystemExit(f"No rotation {rotation} for '{name}'. Available: {available}") + + +def export_pgm(rows: List[str], path: Path) -> None: + width = len(rows[0]) if rows else 0 + height = len(rows) + with path.open('w') as f: + f.write("P2\n") + f.write(f"{width} {height}\n") + f.write("1\n") + for row in rows: + f.write(' '.join('1' if ch == '#' else '0' for ch in row)) + f.write('\n') + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--index', type=int, help='Select frame by absolute index') + parser.add_argument('--name', help='Select frame by label (case-insensitive)') + parser.add_argument('--rotation', type=int, help='Rotation number when selecting by name') + parser.add_argument('--output', type=Path, help='Optional PGM output path') + parser.add_argument('--list', action='store_true', help='List available display names and rotations') + parser.add_argument('--show-all-rotation', type=int, metavar='ROT', + help='Show all frames for the specified rotation number') + args = parser.parse_args() + + frames = load_frames(ASSET_PATH) + + if args.list: + list_names(frames) + return + + if args.show_all_rotation is not None: + rotation_frames = [f for f in frames if f.rotation == args.show_all_rotation] + if not rotation_frames: + raise SystemExit(f"No frames found with rotation {args.show_all_rotation}") + print(f"=== All frames for rotation {args.show_all_rotation} ===\n") + for frame in rotation_frames: + print(f"[{frame.index}] {frame.name}") + for row in frame.render_rows(): + print(row) + print() + return + + frame = select_frame(frames, index=args.index, name=args.name, rotation=args.rotation) + rows = frame.render_rows() + + print(f"Frame index: {frame.index}") + print(f"Name: {frame.name}") + print(f"Rotation: {frame.rotation}") + print() + for row in rows: + print(row) + + if args.output: + export_pgm(rows, args.output) + print(f"\nSaved preview to {args.output}") + + +if __name__ == '__main__': # pragma: no cover - CLI entry point + main() diff --git a/ebike/tools/update_version.sh b/ebike/tools/update_version.sh new file mode 100755 index 000000000..9082542c3 --- /dev/null +++ b/ebike/tools/update_version.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Generate distribution README with dynamic version information + +set -e + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +README_FILE="$PROJECT_DIR/README.md" +DIST_README_FILE="$PROJECT_DIR/README.dist.md" +UI_FILE="$PROJECT_DIR/ui.qml" +DIST_UI_FILE="$PROJECT_DIR/ui.dist.qml" + +# Check if README exists +if [ ! -f "$README_FILE" ]; then + echo "Error: README.md not found" + exit 1 +fi + +# Extract base version from README.md (looks for line like "**Version:** 1.0.0") +BASE_VERSION=$(grep -E '^\*\*Version:\*\*' "$README_FILE" | sed -E 's/^\*\*Version:\*\* //' | tr -d '\n\r') + +if [ -z "$BASE_VERSION" ]; then + echo "Error: Could not extract version from README.md" + echo "Please ensure README.md contains a line like: **Version:** 1.0.0" + exit 1 +fi + +# Get git information +GIT_HASH=$(git -C "$PROJECT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown") +BRANCH_RAW="$(git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")" +# Normalize: lowercase, replace / and _ with -, strip illegal chars, collapse dashes, trim edges +GIT_BRANCH="$(printf '%s' "$BRANCH_RAW" \ + | tr '[:upper:]' '[:lower:]' \ + | sed -E 's@[/_]+@-@g; s/[^a-z0-9.-]//g; s/-{2,}/-/g; s/^-+//; s/-+$//')" +# Fallback for detached HEAD or empty after sanitization +if [ -z "$GIT_BRANCH" ] || [ "$GIT_BRANCH" = "head" ]; then + GIT_BRANCH="unknown" +fi +BUILD_DATE=$(date +%Y%m%d) +BUILD_TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') + +# Build version string based on branch +if [ "$GIT_BRANCH" = "main" ]; then + FULL_VERSION="${BASE_VERSION}-${BUILD_DATE}-${GIT_HASH}" +else + FULL_VERSION="${BASE_VERSION}-${GIT_BRANCH}-${GIT_HASH}" +fi + +# Create distribution README by copying source and replacing version line +cp "$README_FILE" "$DIST_README_FILE" + +# Replace version and add build timestamp +sed "s|^\*\*Version:\*\* .*$|**Version:** \`${FULL_VERSION}\`|" "$DIST_README_FILE" > "$DIST_README_FILE.tmp" +sed "/^\*\*Version:\*\*/a\\ +\\ +**Built:** ${BUILD_TIMESTAMP}" "$DIST_README_FILE.tmp" > "$DIST_README_FILE" +rm -f "$DIST_README_FILE.tmp" + +echo "✓ Generated $DIST_README_FILE with version: $FULL_VERSION" + +# Create distribution ui by copying source and replacing version line +cp "$UI_FILE" "$DIST_UI_FILE" + +# Replace version and add build timestamp +sed "s/\(readonly property string const_ebike_VERSION: \"\)\(\"\)/\1${FULL_VERSION}\2/" "$DIST_UI_FILE" > "$DIST_UI_FILE.tmp" +sed "s/\(readonly property string const_ebike_RELEASE_DATE: \"\)\(\"\)/\1${BUILD_TIMESTAMP}\2/" "$DIST_UI_FILE.tmp" > "$DIST_UI_FILE" +rm -f "$DIST_UI_FILE.tmp" + +echo "✓ Generated $DIST_UI_FILE with version: $FULL_VERSION" diff --git a/ebike/ui.qml b/ebike/ui.qml new file mode 100644 index 000000000..946e1cb05 --- /dev/null +++ b/ebike/ui.qml @@ -0,0 +1,1756 @@ +import Vedder.vesc.vescinterface 1.0 +import "qrc:/mobile" + +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.2 + +import Vedder.vesc.utility 1.0 +import Vedder.vesc.commands 1.0 +import Vedder.vesc.configparams 1.0 + +Item { + id: dxrtData + + anchors.fill: parent + anchors.margins: 10 + + readonly property string const_ebike_VERSION: "" + readonly property string const_ebike_RELEASE_DATE: "" + + readonly property var const_SCOOTER_MODELS: [ + "Blacktip series 2, with Bluetooth", + "Blacktip series 2, no Bluetooth", + "Blacktip series 1, no Bluetooth", + "CudaX, with Bluetooth", + "CudaX, no Bluetooth", + ] + + readonly property int const_RELOAD_DELAY_MS: 1000 + + + property Commands mCommands: VescIf.commands() + property ConfigParams mMcConf: VescIf.mcConfig() + property ConfigParams mInfoConf: VescIf.infoConfig() + property ConfigParams mAppConf: VescIf.appConfig() + + property bool readSettingsDone: false + + // Callback holder for delay timer + property var _delayCb: null + + property int gaugeSize: big.width * 0.8 + property int gaugeSize2: big.width * 0.45 + + property bool loading_values: false + + property string firmwareVersion: "<unknown$gt;" + + property string detectedHardwareModel: "" + property string possibleScooterModels: "" + + Component.onCompleted: { + mCommands.emitEmptySetupValues() + updateFwText() + } + + ColumnLayout { + anchors.fill: parent + + TabBar { + id: tabBar + currentIndex: swipeView.currentIndex + Layout.fillWidth: true + implicitWidth: 0 + clip: true + + property int buttons: 3 + property int buttonWidth: 120 + + TabButton { + id: tab + text: qsTr("Home") + width: Math.max(tabBar.buttonWidth, tabBar.width / tabBar.buttons) + } + TabButton { + text: qsTr("Settings") + width: Math.max(tabBar.buttonWidth, tabBar.width / tabBar.buttons) + } + TabButton { + text: qsTr("Speeds") + width: Math.max(tabBar.buttonWidth, tabBar.width / tabBar.buttons) + } + } + + SwipeView { + id: swipeView + currentIndex: tabBar.currentIndex + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + + // Home page settings. + Page { + ScrollView { + id: homeScroll + anchors.fill: parent + clip: true + contentWidth: availableWidth + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + id: big + Layout.fillWidth: true + Layout.fillHeight: true + color: "transparent" + + CustomGauge { + id: speedGauge + width: gaugeSize + height:big.width + anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenterOffset: -big.width / 10 + anchors.verticalCenter: big.top + anchors.verticalCenterOffset: big.width / 1.9 + tab.height + minimumValue: -400 + maximumValue: 1100 + minAngle: -250 + maxAngle: 12 + labelStep: 200 + value: 0 + typeText: "RPM" + + Image { + anchors.centerIn: parent + antialiasing: true + height: big.width * 0.2 + fillMode: Image.PreserveAspectFit + source : "https://raw.githubusercontent.com/vedderb/vesc_pkg/main/ebike/assets/shark_with_laser.png" + + anchors.horizontalCenterOffset: -(big.width)/3.7 + anchors.verticalCenterOffset: -(big.width)/1.9 + } + + Text { + color: Utility.getAppHexColor("lightText") + text: + "E-Bike Pkg:
" + + "Configured scooter model:
" + + "- " + (hardware_configuration.currentIndex >= 0 ? const_SCOOTER_MODELS[hardware_configuration.currentIndex] : "<unknown>") + "
" + + "Runtime version:
" + + "- " + const_ebike_VERSION + "
" + + "Runtime build timestamp:
" + + "- " + const_ebike_RELEASE_DATE + "
" + + "VESC firmware version:
" + + "- " + firmwareVersion + font.pixelSize: big.width/22.0 + verticalAlignment: Text.AlignVCenter + anchors.centerIn: parent + anchors.verticalCenterOffset: 1.05 * big.width + anchors.horizontalCenterOffset: 0.05 * big.width + font.family: "Roboto" + } + + CustomGauge { + id: batteryGauge + width: gaugeSize2*1.2 + height: gaugeSize2*1.2 + anchors.centerIn: parent + anchors.horizontalCenterOffset: 0.4 * gaugeSize + anchors.verticalCenterOffset: -0.4 * gaugeSize + minAngle: 10 + maxAngle: 350 + minimumValue: 0 + maximumValue: 100 + value: 0 + centerTextVisible: false + property color greenColor: "green" + property color orangeColor: Utility.getAppHexColor("orange") + property color redColor: "red" + nibColor: value > 50 ? greenColor : value > 20 ? orangeColor : redColor + + Text { + id: batteryLabel + color: Utility.getAppHexColor("lightText") + text: "BATTERY" + font.pixelSize: gaugeSize2/18.0 + verticalAlignment: Text.AlignVCenter + anchors.centerIn: parent + anchors.verticalCenterOffset: - gaugeSize2*0.12 + anchors.margins: 10 + font.family: "Roboto" + } + + Text { + id: battValLabel + color: Utility.getAppHexColor("lightText") + text: parseFloat(batteryGauge.value).toFixed(0) +"%" + font.pixelSize: gaugeSize2/6.0 + verticalAlignment: Text.AlignVCenter + anchors.centerIn: parent + anchors.verticalCenterOffset: gaugeSize2*0.015 + anchors.margins: 10 + font.family: "Roboto" + } + + Behavior on nibColor { + ColorAnimation { + duration: 1000; + easing.type: Easing.InOutSine + easing.overshoot: 3 + } + } + } + } + + CustomGauge { + id: escTempGauge + width:gaugeSize2 + height:gaugeSize2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenterOffset: -0.25 *big.width + anchors.verticalCenter: big.top + anchors.verticalCenterOffset: (1.05 * big.width) + tab.height + + minimumValue: 0 + maximumValue: 100 + value: 0 + labelStep: 20 + property real throttleStartValue: 70 + property color blueColor: Utility.getAppHexColor("tertiary2") + property color orangeColor: Utility.getAppHexColor("orange") + property color redColor: "red" + nibColor: value > throttleStartValue ? redColor : (value > 40 ? orangeColor: blueColor) + Behavior on nibColor { + ColorAnimation { + duration: 1000; + easing.type: Easing.InOutSine + easing.overshoot: 3 + } + } + unitText: "°C" + typeText: "TEMP\nESC" + minAngle: -160 + maxAngle: 160 + } + + CustomGauge { + id: motTempGauge + width: gaugeSize2 + height: gaugeSize2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenterOffset: 0.25 *big.width + anchors.verticalCenter: big.top + anchors.verticalCenterOffset: (1.05 * big.width) + tab.height + maximumValue: 200 + minimumValue: 0 + minAngle: -160 + maxAngle: 160 + labelStep: 20 + value: 0 + unitText: "°C" + typeText: "TEMP\nMOTOR" + property real throttleStartValue: 70 + property color blueColor: Utility.getAppHexColor("tertiary2") + property color orangeColor: Utility.getAppHexColor("orange") + property color redColor: "red" + nibColor: value > throttleStartValue ? redColor : (value > 40 ? orangeColor: blueColor) + Behavior on nibColor { + ColorAnimation { + duration: 1000; + easing.type: Easing.InOutSine + easing.overshoot: 3 + } + } + } + } + } + } + } + + /// Settings Page + Page { + background: Rectangle { + opacity: 0.0 + } + + ScrollView { + id: settingsScroll + anchors.fill: parent + clip: true + contentWidth: availableWidth + + property bool has_changes: false + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + DoubleSpinBox { + id: no_speeds + Layout.fillWidth: true + decimals: 0 + prefix: "No. Speeds: " + realFrom: 1 + realTo: 8 + realValue: 8 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + settingsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: start_speed + Layout.fillWidth: true + decimals: 0 + prefix: "Start Speed: " + realFrom: 1 + realTo: no_speeds.realValue + realValue: 3 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + settingsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: jump_speed + Layout.fillWidth: true + decimals: 0 + prefix: "Jump Speed (3 Clicks while stopped): " + realFrom: 1 + realTo: no_speeds.realValue + realValue: 6 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + settingsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: ramp_rate + Layout.fillWidth: true + decimals: 0 + prefix: "Speed Ramp Rate: " + realFrom: 600 + realTo: 8000 + realValue: 5000 + realStepSize: 200 + onRealValueChanged: { + if (!loading_values) { + settingsScroll.has_changes = true + } + } + } + + CheckBox { + id: safe_start + Layout.fillWidth: true + text: "Enable Safe Start" + checked: false + onClicked: { + if (!loading_values) { + settingsScroll.has_changes = true + } + } + } + + RowLayout { + spacing: 10 // Space between the buttons + + Button { + Layout.fillWidth: true + text: "Undo Changes" + enabled: settingsScroll.has_changes + onClicked: { + read_settings() + + settingsScroll.has_changes = false + } + } + + Button { + Layout.fillWidth: true + text: "Save" + enabled: settingsScroll.has_changes + onClicked: { + mMcConf.updateParamDouble("s_pid_ramp_erpms_s", ramp_rate.realValue, null) + mCommands.setMcconf(false) + + settingsScroll.has_changes = false + + delay(2000, function() { + write_settings() + }) + } + } + } + + Button { + Layout.fillWidth: true + text: "Enable Smart Cruise (3 clicks while running)" + onClicked: { + smartCruiseDialog.open() + } + } + + Button { + Layout.fillWidth: true + text: "Enable Untangle && Reverse (4 Clicks while stopped)" + onClicked: { + reverseDialog.open() + } + } + + Button { + Layout.fillWidth: true + text: "Battery Configuration" + onClicked: { + batteryDialog.open() + } + } + + Button { + Layout.fillWidth: true + text: "Beeper && Display Configuration" + onClicked: { + beeperDisplayDialog.open() + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + Button { + Layout.fillWidth: true + text: "Scooter Hardware Configuration" + onClicked: { + hardwareDialog.open() + } + } + } + } + } + + // Speeds Page + Page { + background: Rectangle { + opacity: 0.0 + } + + ScrollView { + id: speedsScroll + anchors.fill: parent + clip: true + contentWidth: availableWidth + + property bool has_changes: false + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + DoubleSpinBox { + id: reverse_speed + Layout.fillWidth: true + visible: enable_reverse.checked + decimals: 0 + prefix: "Reverse Speed: " + suffix: " %" + realFrom: 20 + realTo: 50 + realValue: 45 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: untangle_speed + Layout.fillWidth: true + visible: enable_reverse.checked + decimals: 0 + prefix: "Untangle Speed: " + suffix: " %" + realFrom: (hardware_configuration.currentIndex < 2) ? 20 : 10 + realTo: 30 + realValue: 20 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + + DoubleSpinBox { + id: one_speed + Layout.fillWidth: true + decimals: 0 + prefix: "Speed 1: " + suffix: " %" + realFrom: 20 + realTo: 100 + realValue: 30 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: two_speed + Layout.fillWidth: true + visible: no_speeds.realValue > 1 + decimals: 0 + prefix: "Speed 2: " + suffix: " %" + realFrom: 20 + realTo: 100 + realValue: 38 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: three_speed + Layout.fillWidth: true + visible: no_speeds.realValue > 2 + decimals: 0 + prefix: "Speed 3: " + suffix: " %" + realFrom: 20 + realTo: 100 + realValue: 46 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: four_speed + Layout.fillWidth: true + visible: no_speeds.realValue > 3 + decimals: 0 + prefix: "Speed 4: " + suffix: " %" + realFrom: 20 + realTo: 100 + realValue: 54 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: five_speed + Layout.fillWidth: true + visible: no_speeds.realValue > 4 + decimals: 0 + prefix: "Speed 5: " + suffix: " %" + realFrom: 20 + realTo: 100 + realValue: 62 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: six_speed + Layout.fillWidth: true + visible: no_speeds.realValue > 5 + decimals: 0 + prefix: "Speed 6: " + suffix: " %" + realFrom: 20 + realTo: 100 + realValue: 70 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: seven_speed + Layout.fillWidth: true + visible: no_speeds.realValue > 6 + decimals: 0 + prefix: "Speed 7: " + suffix: " %" + realFrom: 20 + realTo: 100 + realValue: 78 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + DoubleSpinBox { + id: eight_speed + Layout.fillWidth: true + visible: no_speeds.realValue > 7 + decimals: 0 + prefix: "Speed 8: " + suffix: " %" + realFrom: 20 + realTo: 100 + realValue: 100 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + speedsScroll.has_changes = true + } + } + } + + RowLayout { + spacing: 10 // Space between the buttons + + Button { + Layout.fillWidth: true + text: "Undo Changes" + enabled: speedsScroll.has_changes + onClicked: { + read_settings() + + speedsScroll.has_changes = false + } + } + + Button { + Layout.fillWidth: true + text: "Save" + enabled: speedsScroll.has_changes + onClicked: { + write_settings() + + speedsScroll.has_changes = false + } + } + } + } + } + } + } + } + + // handshake timmer to initiate first transfer of values from lisp + Timer { + repeat: true + interval: const_RELOAD_DELAY_MS + running: true + + onTriggered: { + + if (readSettingsDone) { + return + } + var buffer = new ArrayBuffer(1) + var da = new DataView(buffer) + da.setUint8(0, 255) // sends 255 as a handshake that data has not yet been recieved, + mCommands.sendCustomAppData(buffer) + console.log("Sent values request" ) + } + } + + // get live values for RT data when on RT page + Timer { + id: rtTimer + interval: 50 + running: true + repeat: true + + onTriggered: { + if (swipeView.currentIndex == 0) { + mCommands.getValues() + mCommands.getValuesSetup() + } + } + } + + // get HW and firmware values + function updateFwText() { + var params = VescIf.getLastFwRxParams() + + var testFwStr = ""; + var fwNameStr = ""; + + if (params.isTestFw > 0) { + testFwStr = " BETA " + params.isTestFw + } + + if (params.fwName !== "") { + fwNameStr = " (" + params.fwName + ")" + } + + if (params.major >= 0) { + firmwareVersion = params.major + "." + (1e5 + params.minor + '').slice(-2) + fwNameStr + testFwStr + } + + detectedHardwareModel = params.hw + + if (detectedHardwareModel == "410") { + possibleScooterModels = "\n- " + const_SCOOTER_MODELS[2] + } else if (detectedHardwareModel == "60") { + possibleScooterModels = "\n- " + const_SCOOTER_MODELS[1] + "\n- " + const_SCOOTER_MODELS[4] + "\nIf upgraded with Bluetooth adaptor:\n- " + const_SCOOTER_MODELS[0] + "\n- " + const_SCOOTER_MODELS[3] + } else if (detectedHardwareModel == "60_MK5") { + possibleScooterModels = "\n- " + const_SCOOTER_MODELS[0] + "\n- " + const_SCOOTER_MODELS[3] + } else { + possibleScooterModels = "" + } + } + + // delay timer + Timer { + id: timer + onTriggered: { + if (_delayCb) { + var callback = _delayCb + _delayCb = null + callback() + } + } + } + + function delay(delayTime, cb) { + timer.stop(); + timer.interval = Math.max(0, Math.floor(delayTime)); + timer.repeat = false; + dxrtData._delayCb = (typeof cb === "function") ? cb : null; + timer.start(); + } + + function doReboot(delayTime) { + delay(0, function () { + rebootDialog.open() + + delay(delayTime, function () { + console.log("Rebooting..." ) + + mCommands.reboot() + + delay(const_RELOAD_DELAY_MS, function () { + read_settings() + }) + }) + }) + } + + function read_settings() { + readSettingsDone = false + } + + function write_settings () { + if (!readSettingsDone) { + return + } + + var buffer = new ArrayBuffer(30) + var da = new DataView(buffer) + + da.setUint8(0, reverse_speed.realValue) + da.setUint8(1, untangle_speed.realValue) + da.setUint8(2, one_speed.realValue) + da.setUint8(3, two_speed.realValue) + da.setUint8(4, three_speed.realValue) + da.setUint8(5, four_speed.realValue) + da.setUint8(6, five_speed.realValue) + da.setUint8(7, six_speed.realValue) + da.setUint8(8, seven_speed.realValue) + da.setUint8(9, eight_speed.realValue) + da.setUint8(10, no_speeds.realValue + 1) // "+ 1" convert user speed values to actuall speed values + da.setUint8(11, start_speed.realValue + 1) + da.setUint8(12, jump_speed.realValue + 1) + da.setUint8(13, safe_start.checked ? 1 : 0) + da.setUint8(14, enable_reverse.checked ? 1 : 0) + da.setUint8(15, enable_smart_cruise.checked ? 1 : 0) + da.setUint8(16, smart_cruise_timeout.realValue) + da.setUint8(17, (display_rotation.realValue == 0) ? 0 : Math.round(display_rotation.realValue / 90)) + da.setUint8(18, (display_brightness.realValue == 0) ? 0 : Math.round(display_brightness.realValue / 20)) + da.setUint8(19, hardware_configuration.currentIndex) + da.setUint8(20, enable_beeps.checked ? 1 : 0) + da.setUint8(21, beeps_volume.realValue) + da.setUint8(22, cudaX_Flip.checked ? 1 : 0) + da.setUint8(23, (display_rotation2.realValue == 0) ? 0 : Math.round(display_rotation2.realValue / 90)) + da.setUint8(24, enable_tbeeps.checked ? 1 : 0) + da.setUint8(25, enable_smart_cruise_auto_engage.checked ? 1 : 0) + da.setUint8(26, smart_cruise_auto_engage_delay.realValue) + da.setUint8(27, enable_thirds_warning_startup.checked ? 1 : 0) + da.setUint8(28, use_ah_battery_calculation.checked ? 1 : 0) + da.setUint8(29, debug_enabled.checked ? 1 : 0) + mCommands.sendCustomAppData(buffer) + + console.log("Sent values") + } + + function reset_defaults_blacktip() { + var buffer1 = new ArrayBuffer(30) + var da1 = new DataView(buffer1) + da1.setUint8(0, 45) + da1.setUint8(1, 20) + da1.setUint8(2, 30) + da1.setUint8(3, 38) + da1.setUint8(4, 46) + da1.setUint8(5, 54) + da1.setUint8(6, 62) + da1.setUint8(7, 70) + da1.setUint8(8, 78) + da1.setUint8(9, 100) + da1.setUint8(10, 9) + da1.setUint8(11, 4) + da1.setUint8(12, 7) + da1.setUint8(13, 1) + da1.setUint8(14, 0) + da1.setUint8(15, 0) + da1.setUint8(16, 60) + da1.setUint8(17, 0) + da1.setUint8(18, 5) + da1.setUint8(19, 0) + da1.setUint8(20, 0) + da1.setUint8(21, 3) + da1.setUint8(22, 0) + da1.setUint8(23, 0) + da1.setUint8(24, 0) + da1.setUint8(25, 0) // Enable Auto-Engage default: off + da1.setUint8(26, 10) // Auto-Engage Delay default: 10 seconds + da1.setUint8(27, 0) // Enable Thirds Warning Startup default: off + da1.setUint8(28, 0) // Battery calculation method default: voltage-based + da1.setUint8(29, 0) // Debug enabled default: off + mCommands.sendCustomAppData(buffer1) + + // All available settings here https://github.com/vedderb/bldc/blob/master/datatypes.h + + mMcConf.updateParamInt("si_motor_poles", 5, null) + mMcConf.updateParamDouble("l_erpm_start", 0.9, null) + mMcConf.updateParamDouble("l_in_current_map_start", 1, null) //6.05 only + mMcConf.updateParamDouble("l_in_current_map_filter", 0.005, null) //6.05 only + mMcConf.updateParamDouble("l_min_erpm", -6000, null) + mMcConf.updateParamDouble("l_max_erpm", 6000, null) + + //Motor Temp Settings + mMcConf.updateParamInt("l_temp_motor_start", 160, null) + mMcConf.updateParamInt("l_temp_motor_end", 180, null) + mMcConf.updateParamDouble("l_temp_accel_dec", 0, null) + mMcConf.updateParamEnum("m_motor_temp_sens_type", 0, null) + mMcConf.updateParamDouble("m_ntc_motor_beta", 3950, null) + mMcConf.updateParamBool("foc_temp_comp", 1, null) + mMcConf.updateParamDouble("foc_temp_comp_base_temp", 67.3, null) + + //FOC Blacktip motor settings + mMcConf.updateParamDouble("foc_motor_r", 0.1225, null) + mMcConf.updateParamDouble("foc_motor_l", 0.00023008, null) //245.97 + mMcConf.updateParamDouble("foc_motor_ld_lq_diff", 0.00005968, null) + mMcConf.updateParamDouble("foc_motor_flux_linkage", 0.016608, null) + mMcConf.updateParamDouble("foc_current_kp", 0.2301, null) + mMcConf.updateParamDouble("foc_current_ki", 122.48, null) + mMcConf.updateParamDouble("foc_observer_gain", 3.63e+06, null) + mMcConf.updateParamDouble("foc_openloop_rpm", 500, null) + + mMcConf.updateParamDouble("foc_sl_openloop_hyst", 0.1, null) + mMcConf.updateParamDouble("foc_sl_openloop_time_lock", 0, null) + mMcConf.updateParamDouble("foc_sl_openloop_time_ramp", 0.1, null) + mMcConf.updateParamDouble("foc_sl_openloop_time", 0.05, null) + mMcConf.updateParamDouble("foc_sl_openloop_boost_q", 20, null) + mMcConf.updateParamDouble("foc_sl_openloop_max_q", 30, null) + + //PID Settings + mMcConf.updateParamDouble("s_pid_min_erpm", 5, null) + mMcConf.updateParamBool("s_pid_allow_braking", 0, null) + mMcConf.updateParamDouble("s_pid_ramp_erpms_s", 5000, null) + + //Battery Settings + mMcConf.updateParamEnum("si_battery_type", 0, null) + mMcConf.updateParamInt("si_battery_cells", 10, null) + mMcConf.updateParamDouble("si_battery_ah", 9, null) + + //Current & Voltage Settings + mMcConf.updateParamDouble("l_current_max", 45, null) + mMcConf.updateParamDouble("l_current_min", -45, null) + mMcConf.updateParamDouble("l_in_current_max", 23, null) + mMcConf.updateParamDouble("l_in_current_min", -23, null) + mMcConf.updateParamDouble("l_abs_current_max", 75, null) + mMcConf.updateParamDouble("l_min_vin", 29, null) + mMcConf.updateParamDouble("l_battery_cut_start", 32, null) + mMcConf.updateParamDouble("l_battery_cut_end", 30, null) + + // App settings for UART/Bluetooth + mAppConf.updateParamEnum("app_to_use", 3) + mAppConf.updateParamInt("app_uart_baudrate", 115200) + mAppConf.updateParamEnum("shutdown_mode", 9) + + mCommands.setMcconf(false) // Write Motor settings immediatly + + delay(2000, function() { + mCommands.setAppConf() // Write App settings 2 seconds later + + doReboot(2000) + }) + + console.log("Defaults Reset for Blacktip") + } + + function reset_defaults_cudax() { + var buffer1 = new ArrayBuffer(30) + var da1 = new DataView(buffer1) + da1.setUint8(0, 30) + da1.setUint8(1, 10) + da1.setUint8(2, 20) + da1.setUint8(3, 30) + da1.setUint8(4, 39) + da1.setUint8(5, 49) + da1.setUint8(6, 59) + da1.setUint8(7, 68) + da1.setUint8(8, 78) + da1.setUint8(9, 100) + da1.setUint8(10, 9) + da1.setUint8(11, 4) + da1.setUint8(12, 7) + da1.setUint8(13, 1) + da1.setUint8(14, 0) + da1.setUint8(15, 0) + da1.setUint8(16, 60) + da1.setUint8(17, 2) + da1.setUint8(18, 5) + da1.setUint8(19, 3) + da1.setUint8(20, 0) + da1.setUint8(21, 3) + da1.setUint8(22, 0) + da1.setUint8(23, 2) + da1.setUint8(24, 0) + da1.setUint8(25, 0) // Enable Auto-Engage default: off + da1.setUint8(26, 10) // Auto-Engage Delay default: 10 seconds + da1.setUint8(27, 0) // Enable Thirds Warning Startup default: off + da1.setUint8(28, 0) // Battery calculation method default: voltage-based + da1.setUint8(29, 0) // Debug enabled default: off + mCommands.sendCustomAppData(buffer1) + + // All available settings here https://github.com/vedderb/bldc/blob/f6b06bc9f8d02d2ba262166127c3f2ffaedbb17e/datatypes.h#L369 + + mMcConf.updateParamInt("si_motor_poles", 7, null) + mMcConf.updateParamDouble("l_erpm_start", 0.9, null) + mMcConf.updateParamDouble("l_in_current_map_start", 1, null) //6.05 only + mMcConf.updateParamDouble("l_in_current_map_filter", 0.005, null) //6.05 only + mMcConf.updateParamDouble("l_min_erpm", -9000, null) + mMcConf.updateParamDouble("l_max_erpm", 9000, null) + + + //Motor Temp Settings + mMcConf.updateParamInt("l_temp_motor_start", 160, null) + mMcConf.updateParamInt("l_temp_motor_end", 180, null) + mMcConf.updateParamDouble("l_temp_accel_dec", 0, null) + mMcConf.updateParamEnum("m_motor_temp_sens_type", 0, null) + mMcConf.updateParamDouble("m_ntc_motor_beta", 3950, null) + mMcConf.updateParamBool("foc_temp_comp", 1, null) + mMcConf.updateParamDouble("foc_temp_comp_base_temp", 67.7, null) + + //FOC CudaX motor settings + mMcConf.updateParamDouble("foc_motor_r", 0.0253, null) + mMcConf.updateParamDouble("foc_motor_l", 0.00013034, null) + mMcConf.updateParamDouble("foc_motor_ld_lq_diff", 0.00001821, null) + mMcConf.updateParamDouble("foc_motor_flux_linkage", 0.014246, null) + mMcConf.updateParamDouble("foc_current_kp", 0.1303, null) + mMcConf.updateParamDouble("foc_current_ki", 25.25, null) + mMcConf.updateParamDouble("foc_observer_gain", 4.93e+06, null) + mMcConf.updateParamDouble("foc_openloop_rpm", 500, null) + + mMcConf.updateParamDouble("foc_sl_openloop_hyst", 0.1, null) + mMcConf.updateParamDouble("foc_sl_openloop_time_lock", 0, null) + mMcConf.updateParamDouble("foc_sl_openloop_time_ramp", 0.1, null) + mMcConf.updateParamDouble("foc_sl_openloop_time", 0.05, null) + mMcConf.updateParamDouble("foc_sl_openloop_boost_q", 20, null) + mMcConf.updateParamDouble("foc_sl_openloop_max_q", 30, null) + + //PID Settings + mMcConf.updateParamDouble("s_pid_min_erpm", 5, null) + mMcConf.updateParamBool("s_pid_allow_braking", 0, null) + mMcConf.updateParamDouble("s_pid_ramp_erpms_s", 5000, null) + + //Battery Settings + mMcConf.updateParamEnum("si_battery_type", 0, null) + mMcConf.updateParamInt("si_battery_cells", 10, null) + mMcConf.updateParamDouble("si_battery_ah", 9, null) + + //Current & Voltage Settings + mMcConf.updateParamDouble("l_current_max", 100, null) + mMcConf.updateParamDouble("l_current_min", -100, null) + mMcConf.updateParamDouble("l_in_current_max", 46, null) + mMcConf.updateParamDouble("l_in_current_min", -46, null) + mMcConf.updateParamDouble("l_abs_current_max", 150, null) + mMcConf.updateParamDouble("l_min_vin", 29, null) + mMcConf.updateParamDouble("l_battery_cut_start", 32, null) + mMcConf.updateParamDouble("l_battery_cut_end", 30, null) + + // App settings for UART/Bluetooth + mAppConf.updateParamEnum("app_to_use", 3) + mAppConf.updateParamInt("app_uart_baudrate", 115200) + mAppConf.updateParamEnum("shutdown_mode", 9) + + mCommands.setMcconf(false) // Write Motor settings immediatly + + delay(2000, function() { + mCommands.setAppConf() // Write App settings 2 seconds later + + doReboot(2000) + }) + + console.log("Defaults Reset for CudaX") + } + + function isBlacktip(hardware_type) { + return hardware_type < 3 + } + + Connections { + target: mCommands + + function onCustomAppDataReceived (data) { + var dv = new DataView(data) + loading_values = true; + + hardware_configuration.currentIndex = dv.getUint8(19) // set first so U spinbox range is opened up for cuda x + + reverse_speed.realValue = dv.getUint8(0) + untangle_speed.realValue = dv.getUint8(1) + one_speed.realValue = dv.getUint8(2) + two_speed.realValue = dv.getUint8(3) + three_speed.realValue = dv.getUint8(4) + four_speed.realValue = dv.getUint8(5) + five_speed.realValue = dv.getUint8(6) + six_speed.realValue = dv.getUint8(7) + seven_speed.realValue = dv.getUint8(8) + eight_speed.realValue = dv.getUint8(9) + no_speeds.realValue = dv.getUint8(10) -1 + start_speed.realValue = dv.getUint8(11) -1 + jump_speed.realValue = dv.getUint8(12) -1 + safe_start.checked = dv.getUint8(13) == 1 + enable_reverse.checked = dv.getUint8(14) == 1 + enable_smart_cruise.checked = dv.getUint8(15) == 1 + smart_cruise_timeout.realValue = dv.getUint8(16) + display_rotation.realValue = (dv.getUint8(17) == 0) ? 0 : dv.getUint8(17) * 90 + display_brightness.realValue = (dv.getUint8(18) == 0) ? 0 : dv.getUint8(18) * 20 + enable_beeps.checked = dv.getUint8(20) == 1 + beeps_volume.realValue = dv.getUint8(21) + cudaX_Flip.checked = dv.getUint8(22) == 1 + display_rotation2.realValue = (dv.getUint8(23) == 0) ? 0 : dv.getUint8(23) * 90 + enable_tbeeps.checked = dv.getUint8(24) == 1 + enable_smart_cruise_auto_engage.checked = dv.getUint8(25) == 1 + smart_cruise_auto_engage_delay.realValue = dv.getUint8(26) + enable_thirds_warning_startup.checked = dv.getUint8(27) == 1 + use_ah_battery_calculation.checked = dv.getUint8(28) == 1 + debug_enabled.checked = dv.getUint8(29) == 1 + + ramp_rate.realValue = mMcConf.getParamDouble("s_pid_ramp_erpms_s") + battery_ah.realValue = mMcConf.getParamDouble("si_battery_ah") + + loading_values = false + readSettingsDone = true + + rebootDialog.close() + + console.log("Values received") + } + } + + Connections { + id: commandsUpdate + target: mCommands + + function onValuesSetupReceived(values, mask) { + + var soc = Math.max(0, Math.min(1, values.battery_level)) + var pct = 100 * ( + 4.3867 * Math.pow(soc, 4) + - 6.7072 * Math.pow(soc, 3) + + 2.4021 * Math.pow(soc, 2) + + 1.3619 * soc + ) + batteryGauge.value = Math.max(0, Math.min(100, pct)) + + speedGauge.value = values.rpm / mMcConf.getParamInt("si_motor_poles") + escTempGauge.value = values.temp_mos + escTempGauge.maximumValue = Math.ceil(mMcConf.getParamDouble("l_temp_fet_end") / 5) * 5 + escTempGauge.throttleStartValue = Math.ceil(mMcConf.getParamDouble("l_temp_fet_start") / 5) * 5 + escTempGauge.labelStep = Math.ceil(escTempGauge.maximumValue/ 50) * 5 + motTempGauge.value = values.temp_motor + motTempGauge.labelStep = Math.ceil(motTempGauge.maximumValue/ 50) * 5 + motTempGauge.maximumValue = Math.ceil(mMcConf.getParamDouble("l_temp_motor_end") / 5) * 5 + motTempGauge.throttleStartValue = Math.ceil(mMcConf.getParamDouble("l_temp_motor_start") / 5) * 5 + } + } + + Dialog { + id: reverseDialog + standardButtons: Dialog.Save | Dialog.Cancel + modal: true + focus: true + width: big.width - 20 + closePolicy: Popup.CloseOnEscape + title: "Untangle & Reverse" + + property bool has_changes: false + + onOpened: { + standardButton(Dialog.Save).enabled = false + } + + onAccepted: { + if (has_changes) { + write_settings() + + has_changes = false + } + } + + onRejected: { + if (has_changes) { + read_settings() + + has_changes = false + } + } + + function valuesChanged() { + has_changes = true + standardButton(Dialog.Save).enabled = true + } + + ScrollView { + id: reverseScroll + anchors.fill: parent + clip: true + contentWidth: availableWidth + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + + text: "This adds two reverse gears, untangle and reverse. You can access these gears via a quadruple (4) click on the trigger." + } + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + topPadding:15 + text: " This feature can be dangerous and requires training. By enabling this feature you acknowledge you fully understand how to use it safely. " + } + + CheckBox { + id: enable_reverse + Layout.fillWidth: true + text: "Enable Untangle & Reverse" + checked: false + onClicked: { + reverseDialog.valuesChanged() + } + } + } + } + } + + Dialog { + id: smartCruiseDialog + standardButtons: Dialog.Save | Dialog.Cancel + modal: true + focus: true + width: big.width - 20 + closePolicy: Popup.CloseOnEscape + title: "Smart Cruise" + + property bool has_changes: false + + onOpened: { + standardButton(Dialog.Save).enabled = false + } + + onAccepted: { + if (has_changes) { + write_settings() + + has_changes = false + } + } + + onRejected: { + if (has_changes) { + read_settings() + + has_changes = false + } + } + + function valuesChanged() { + has_changes = true + standardButton(Dialog.Save).enabled = true + } + + ScrollView { + id: customScroll + anchors.fill: parent + clip: true + contentWidth: availableWidth + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + + text: "This gives you the option of Smart Cruise. While running, do a triple (3) click and the display will show \"C\" and Smart Cruise will be engaged." + } + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + topPadding:10 + text: "While Smart Cruise is active: short trigger taps reset the timeout timer. To adjust speed, hold the trigger for >0.5 second, release, then do 1 click (speed down) or 2 clicks (speed up). To disable Smart Cruise, do another triple (3) click after a long hold." + } + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + topPadding:10 + text: "Smart Cruise also times out after the duration set below. At the set time the display will show “C?” and reduce your rpm’s slightly. Another triple click will re-engage Smart Cruise, otherwise the scooter will stop." + } + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + topPadding:15 + text: " By enabling this feature you acknowledge you fully understand how to use it safely." + } + + CheckBox { + id: enable_smart_cruise + Layout.fillWidth: true + text: "Enable Smart Cruise" + checked: false + onClicked: { + smartCruiseDialog.valuesChanged() + } + } + + DoubleSpinBox { + id: smart_cruise_timeout + Layout.fillWidth: true + visible: enable_smart_cruise.checked + decimals: 0 + prefix: "Smart Cruise Timeout: " + suffix: " sec." + realFrom: 10 + realTo: 240 + realValue: 60 + realStepSize: 10.0 + onRealValueChanged: { + if (!loading_values) { + smartCruiseDialog.valuesChanged() + } + } + } + + CheckBox { + id: enable_smart_cruise_auto_engage + visible: enable_smart_cruise.checked + Layout.fillWidth: true + text: "Enable Auto-Engage Smart Cruise" + checked: false + onClicked: { + smartCruiseDialog.valuesChanged() + } + } + + DoubleSpinBox { + id: smart_cruise_auto_engage_delay + Layout.fillWidth: true + visible: enable_smart_cruise.checked && enable_smart_cruise_auto_engage.checked + decimals: 0 + prefix: "Auto-Engage Delay: " + suffix: " sec." + realFrom: 5 + realTo: 30 + realValue: 10 + realStepSize: 1.0 + onRealValueChanged: { + if (!loading_values) { + smartCruiseDialog.valuesChanged() + } + } + } + } + } + } + + Dialog { + id: batteryDialog + standardButtons: Dialog.Save | Dialog.Cancel + modal: true + focus: true + width: big.width - 20 + closePolicy: Popup.CloseOnEscape + title: "Battery Configuration" + + property bool has_changes: false + + onOpened: { + standardButton(Dialog.Save).enabled = false + } + + onAccepted: { + if (has_changes) { + mMcConf.updateParamDouble("si_battery_ah", battery_ah.realValue, null) + mCommands.setMcconf(false) + + has_changes = false + + delay(2000, function() { + write_settings() + }) + } + } + + onRejected: { + if (has_changes) { + read_settings() + + has_changes = false + } + } + + function valuesChanged() { + has_changes = true + standardButton(Dialog.Save).enabled = true + } + + ScrollView { + anchors.fill: parent + clip: true + contentWidth: availableWidth + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + DoubleSpinBox { + id: battery_ah + Layout.fillWidth: true + decimals: 1 + prefix: "Battery capacity: " + realFrom: 0.5 + realTo: 20 + realValue: 9 + realStepSize: 0.5 + onRealValueChanged: { + if (!loading_values) { + batteryDialog.valuesChanged() + } + } + } + + CheckBox { + id: enable_thirds_warning_startup + Layout.fillWidth: true + text: "Thirds warning on from power-up" + checked: false + onClicked: { + batteryDialog.valuesChanged() + } + } + + CheckBox { + id: use_ah_battery_calculation + Layout.fillWidth: true + text: "Use ampere-hour based battery calculation" + checked: false + onClicked: { + batteryDialog.valuesChanged() + } + } + } + } + } + + Dialog { + id: beeperDisplayDialog + standardButtons: Dialog.Save | Dialog.Cancel + modal: true + focus: true + width: big.width - 20 + closePolicy: Popup.CloseOnEscape + title: "Beeper & Display Configuration" + + property bool has_changes: false + property bool reboot_required: false + + onOpened: { + standardButton(Dialog.Save).enabled = false + } + + onAccepted: { + if (has_changes) { + write_settings() + + has_changes = false + + if (reboot_required) { + reboot_required = false + + doReboot(2000) + } + } + } + + onRejected: { + if (has_changes) { + read_settings() + + has_changes = false + } + } + + function valuesChanged() { + has_changes = true + standardButton(Dialog.Save).enabled = true + } + + ScrollView { + anchors.fill: parent + clip: true + contentWidth: availableWidth + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + CheckBox { + id: enable_beeps + Layout.fillWidth: true + text: "Enable Battery Capacity Beeps" + checked: false + onClicked: { + beeperDisplayDialog.valuesChanged() + } + } + + CheckBox { + id: enable_tbeeps + Layout.fillWidth: true + text: "Enable Trigger Beeps" + checked: false + onClicked: { + beeperDisplayDialog.valuesChanged() + } + } + + DoubleSpinBox { + id: beeps_volume + Layout.fillWidth: true + decimals: 0 + prefix: "Beep Volume: " + realFrom: 1 + realTo:10 + realValue: 1 + realStepSize: 1 + onRealValueChanged: { + if (!loading_values) { + beeperDisplayDialog.valuesChanged() + } + } + } + + CheckBox { + id: cudaX_Flip + visible: !isBlacktip(hardware_configuration.currentIndex) + Layout.fillWidth: true + text: "Flip Screens on CudaX" + checked: false + onClicked: { + beeperDisplayDialog.valuesChanged() + } + } + + DoubleSpinBox { + id: display_rotation + Layout.fillWidth: true + decimals: 0 + prefix: "Display 1 Rotation: " + suffix: " Deg." + realFrom: 0 + realTo: 270 + realValue: 90 + realStepSize: 90 + onRealValueChanged: { + if (!loading_values) { + beeperDisplayDialog.valuesChanged() + } + } + } + + DoubleSpinBox { + id: display_rotation2 + Layout.fillWidth: true + visible: !isBlacktip(hardware_configuration.currentIndex) + decimals: 0 + prefix: "Display 2 Rotation: " + suffix: " Deg." + realFrom: 0 + realTo: 270 + realValue: 90 + realStepSize: 90 + onRealValueChanged: { + if (!loading_values) { + beeperDisplayDialog.valuesChanged() + } + } + } + + DoubleSpinBox { + id: display_brightness + Layout.fillWidth: true + decimals: 0 + prefix: "Display Brightness*: " + suffix: " %" + realFrom: 0 + realTo: 100 + realValue: 100 + realStepSize: 20 + onRealValueChanged: { + if (!loading_values) { + beeperDisplayDialog.valuesChanged() + beeperDisplayDialog.reboot_required = true + } + } + } + + Text { + topPadding:5 + font.pixelSize: Qt.application.font.pixelSize * 0.8 + color: Utility.getAppHexColor("lightText") + text: "* Will trigger a scooter reboot" + } + + CheckBox { + id: debug_enabled + Layout.fillWidth: true + text: "Enable Debug Logging" + checked: false + onClicked: { + beeperDisplayDialog.valuesChanged() + } + } + } + } + } + + Dialog { + id: hardwareDialog + standardButtons: Dialog.Save | Dialog.Cancel + modal: true + focus: true + width: big.width - 20 + closePolicy: Popup.CloseOnEscape + title: "Scooter Hardware Configuration" + + property int original_hardware_configuration + + onOpened: { + original_hardware_configuration = hardware_configuration.currentIndex + standardButton(Dialog.Save).enabled = false + } + + onAccepted: { + if (original_hardware_configuration != hardware_configuration.currentIndex) { + if (isBlacktip(hardware_configuration.currentIndex) && !isBlacktip(original_hardware_configuration)) { + reset_defaults_blacktip() + } else if (!isBlacktip(hardware_configuration.currentIndex) && isBlacktip(original_hardware_configuration)) { + reset_defaults_cudax() + } else { + write_settings() + + doReboot(2000) + } + } + } + + onRejected: { + if (original_hardware_configuration != hardware_configuration.currentIndex) { + read_settings() + } + } + + ScrollView { + id: hardwareScroll + anchors.fill: parent + clip: true + contentWidth: availableWidth + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + topPadding:15 + text: " Warning: If you are connected via Bluetooth and select a scooter without Bluetooth you will loose your connection." + } + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + topPadding:15 + text: "If you loose Bluetooth you will need to use the PC based VESC Tool and connect via USB to select the correct scooter." + } + + Text { + Layout.fillWidth: true + font.pixelSize: Qt.application.font.pixelSize + topPadding:15 + color: Utility.getAppHexColor("lightText") + text: "Detected motor controller model: " + detectedHardwareModel + "\nPossible scooter models for this hardware:" + possibleScooterModels + } + + Text { + Layout.fillWidth: true + font.pixelSize: Qt.application.font.pixelSize + topPadding:15 + color: Utility.getAppHexColor("lightText") + text: "Select your model and hardware version:*" + } + + ComboBox { + id: hardware_configuration + Layout.fillWidth: true + currentIndex: -1 + model: const_SCOOTER_MODELS + + onCurrentIndexChanged: { + if (hardwareDialog.original_hardware_configuration != hardware_configuration.currentIndex) { + hardwareDialog.standardButton(Dialog.Save).enabled = true + } else { + hardwareDialog.standardButton(Dialog.Save).enabled = false + } + } + } + + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true + color : "transparent" + } + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + topPadding:15 + text: "Use reset button to reset ALL the settings for the scooter. Must be used after a firmware update." + } + + Button { + Layout.fillWidth: true + text: "Reset Defaults*" + enabled: hardwareDialog.original_hardware_configuration == hardware_configuration.currentIndex + onClicked: { + if (isBlacktip(hardware_configuration.currentIndex)) { + reset_defaults_blacktip() + } else { + reset_defaults_cudax() + } + + hardwareDialog.close() + } + } + + Text { + id: text3 + topPadding:5 + font.pixelSize: Qt.application.font.pixelSize * 0.8 + color: Utility.getAppHexColor("lightText") + text: "* Will trigger a scooter reboot and potentially a reset to defaults" + } + } + } + } + + Dialog { + id: rebootDialog + standardButtons: Dialog.Cancel + modal: true + focus: true + width: big.width - 20 + closePolicy: Popup.CloseOnEscape + title: "Rebooting..." + + ScrollView { + anchors.fill: parent + clip: true + contentWidth: availableWidth + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Text { + color: "#ffffff" + Layout.fillWidth: true + wrapMode: Text.WordWrap + + text: "Please wait while the scooter is rebooting..." + } + } + } + } +} diff --git a/res_all.qrc b/res_all.qrc index cd966e366..a801fd034 100644 --- a/res_all.qrc +++ b/res_all.qrc @@ -5,6 +5,7 @@ refloat/refloat.vescpkg logui/logui.vescpkg tnt/tnt.vescpkg + x1_unlocker/x1_unlocker.vescpkg vbms32/vbms32.vescpkg vbms32_micro/vbms32_micro.vescpkg lib_ws2812/ws2812.vescpkg @@ -20,6 +21,7 @@ vdisp/vdisp_esc.vescpkg vbms_harmony32/vbms_harmony32.vescpkg blacktip_dpv/blacktip_dpv.vescpkg + ebike/ebike.vescpkg vbms_harmony16/vbms_harmony16.vescpkg dash35b/dash35b.vescpkg dash35b/dash35b_esc.vescpkg diff --git a/x1_unlocker/Makefile b/x1_unlocker/Makefile new file mode 100644 index 000000000..b18784970 --- /dev/null +++ b/x1_unlocker/Makefile @@ -0,0 +1,11 @@ +VESC_TOOL ?= vesc_tool + +all: x1_unlocker.vescpkg + +x1_unlocker.vescpkg: + $(VESC_TOOL) --buildPkg "x1_unlocker.vescpkg:script.lisp::0:README.md:X1-Unlocker" + +clean: + rm -f x1_unlocker.vescpkg + +.PHONY: all clean diff --git a/x1_unlocker/README.md b/x1_unlocker/README.md new file mode 100644 index 000000000..a53ec7066 --- /dev/null +++ b/x1_unlocker/README.md @@ -0,0 +1,51 @@ +# X1-Unlocker + +Automatically sends a "magic" message, to unlock the CAN-port of INNOTRACE X1 controllers. + +INNOTRACE was a brand that sold aftermarket controllers for chinese Bafang Ultra M620/G510 mid-drive motors with integrated torque-sensor. Their X1-Controller and X1-Tool were basically a commercialized copy of the VESC-Project. It was tied to a subscription service and overpriced USB-FTDI-cable, which connected via UART to the motor, like the stock Bafang-Controller does. With this package a magic CAN-message is sent, which makes the X1-Controller show up as VESC-CAN-device and accessible. + +

+ +--- +## Disclaimer +This is experimental and mostlikely not intended by Innotrace! Functions are limited and you should be very careful with changing unknown parameters! It works with X1-FW 2.4.x.x and 2.5.x.x. Things you can do with this: + +* Rotor-Calibration +* Max Current +* Max Wattage + +

+ +--- +## Usage +The package is made to be used with VESC-Express, but can run it on any VESC that supports lispBM. The script automatically runs one time at boot, therefore please exactly follow these steps, to prepare your setup correctly: + +1. Download the latest VESC-Tool beta from https://vesc-project.com/vesc_tool +2. Connect the CAN-H and CAN-L of the VESC-Express with your X1 motor. (see pinout below) +3. Turn on the battery. +4. Turn on the display. (**Important! Do not skip!**) +5. Plug the VESC-Express into your computer. +6. Connect via VESC-Tool beta. +7. Wait until LED changes from RED to BLUE. +8. Now click "Scan CAN" and click on "X1". +9. There you go! + +

+ +--- +## Locate Connector +With Bafang this connector is normally for optional battery communication via UART. On X1, this connector is repurposed and has CAN-Bus accessible from the outside, without opening the motor: [plug_location.png](https://github.com/Tomblarom/vesc_pkg/blob/main/x1_unlocker/plug_location.png)\ +[image source: [https://www.greenbikekit.com](https://www.greenbikekit.com)] +

+ +--- +## Pinout Connector +Copied from [@dedo](https://forums.electricbikereview.com/threads/archon-x1-programming-thread-questions-and-experiences.40034/page-16#post-627685). Thanks for providing! [plug_pinout.png](https://github.com/Tomblarom/vesc_pkg/blob/main/x1_unlocker/plug_pinout.png)\ +Female: 04R-JWPF-VSLE-S\ +Male: 04T-JWPF-VSLE-S + +

+ +--- +### Version +- X1-Unlocker v1.0 \ No newline at end of file diff --git a/x1_unlocker/plug_location.png b/x1_unlocker/plug_location.png new file mode 100644 index 000000000..af0feb176 Binary files /dev/null and b/x1_unlocker/plug_location.png differ diff --git a/x1_unlocker/plug_pinout.png b/x1_unlocker/plug_pinout.png new file mode 100644 index 000000000..c279fecd8 Binary files /dev/null and b/x1_unlocker/plug_pinout.png differ diff --git a/x1_unlocker/script.lisp b/x1_unlocker/script.lisp new file mode 100644 index 000000000..173eda0a7 --- /dev/null +++ b/x1_unlocker/script.lisp @@ -0,0 +1,30 @@ +(gpio-configure 3 'pin-mode-out) ; LED Blue +(gpio-configure 2 'pin-mode-out) ; LED Red + +(print "scanning..") +(gpio-write 3 0) +(gpio-write 2 1) + +(defun avl nil + (progn + (print "X1 available") + (gpio-write 3 1) + (gpio-write 2 0) + ) +) + +(conf-set 'can-baud-rate 1) +(can-send-eid 0x00D431FF (list 0x14 0x78 0x00 0x00 0x00 0x00 0x00 0x00)) +(if (can-scan) (avl) + (progn + (conf-set 'can-baud-rate 2) + (can-send-eid 0x00D431FF (list 0x14 0x78 0x00 0x00 0x00 0x00 0x00 0x00)) + (if (can-scan) (avl) + (progn + (print "not found") + ) + ) + ) +) + +