diff --git a/.github/workflows/flasher.yml b/.github/workflows/flasher.yml new file mode 100644 index 0000000..6bb4938 --- /dev/null +++ b/.github/workflows/flasher.yml @@ -0,0 +1,286 @@ +name: flasher + +on: + push: + branches: [master, MillibytePlatforms] + tags: ['v*'] + paths: + - "ESP32/flasher/millibyte-flasher/**" + - "ESP32/platformio.ini" + - "ESP32/src/**" + - "ESP32/data/**" + - "ESP32/dataEdit/**" + - "ESP32/boards/**" + - ".github/workflows/flasher.yml" + pull_request: + paths: + - "ESP32/flasher/millibyte-flasher/**" + - ".github/workflows/flasher.yml" + workflow_dispatch: + release: + types: [published] + +defaults: + run: + shell: bash + +jobs: + firmware: + name: Build firmware (${{ matrix.board }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: [sr6_pcb, ssr1_pcb] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install platformio + run: | + python -m pip install --upgrade pip + pip install platformio + + - name: Build firmware + filesystem + working-directory: ESP32 + run: | + set -euxo pipefail + mkdir -p cache + echo "::group::data/ contents" + find data -type f -print -exec ls -la {} \; + echo "::endgroup::" + stash=$(mktemp -d) + # Step 1: build the filesystem image and stash it immediately, since + # a later `pio run` invocation has been observed to wipe other + # artifacts out of .pio/build// on some pioarduino releases. + pio run -t buildfs -e ${{ matrix.board }} + for f in littlefs.bin spiffs.bin; do + if [ -f .pio/build/${{ matrix.board }}/$f ]; then + cp .pio/build/${{ matrix.board }}/$f "$stash/" + fi + done + # Step 2: build firmware/bootloader/partitions. + pio run -e ${{ matrix.board }} + # Step 3: restore the FS image so staging can pick it up. + cp -n "$stash"/*.bin .pio/build/${{ matrix.board }}/ 2>/dev/null || true + ls -la .pio/build/${{ matrix.board }}/ + + - name: Stage firmware bundle + working-directory: ESP32 + run: | + set -euxo pipefail + dest=flasher/millibyte-flasher/firmware/${{ matrix.board }} + src=.pio/build/${{ matrix.board }} + mkdir -p "$dest" + cp "$src/bootloader.bin" "$dest/" + cp "$src/partitions.bin" "$dest/" + cp "$src/firmware.bin" "$dest/" + if [ -f "$src/littlefs.bin" ]; then + cp "$src/littlefs.bin" "$dest/" + elif [ -f "$src/spiffs.bin" ]; then + cp "$src/spiffs.bin" "$dest/littlefs.bin" + else + echo "::error::No filesystem image (littlefs.bin / spiffs.bin) was produced for ${{ matrix.board }}" + ls -la "$src" + exit 1 + fi + # boot_app0.bin location varies by arduino-espressif32 install layout. + boot_app0="" + for cand in \ + ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \ + ~/.platformio/packages/framework-arduinoespressif32@*/tools/partitions/boot_app0.bin; do + if [ -f "$cand" ]; then boot_app0="$cand"; break; fi + done + if [ -z "$boot_app0" ]; then + boot_app0=$(find ~/.platformio/packages -name boot_app0.bin -type f 2>/dev/null | head -n1 || true) + fi + if [ -z "$boot_app0" ]; then + echo "::error::could not locate boot_app0.bin" + ls -la ~/.platformio/packages/ || true + exit 1 + fi + cp "$boot_app0" "$dest/boot_app0.bin" + ls -la "$dest" + + - uses: actions/upload-artifact@v4 + with: + name: firmware-${{ matrix.board }} + path: ESP32/flasher/millibyte-flasher/firmware/${{ matrix.board }}/ + if-no-files-found: error + + # Raw-bin release archive: same files, but renamed and zipped so users + # who already have esptool can flash without downloading the full + # MillibyteFlasher bundle. + - name: Build merged release.bin (esptool) + working-directory: ESP32 + run: | + pip install esptool + src=flasher/millibyte-flasher/firmware/${{ matrix.board }} + python - <<'PY' + import json, os, subprocess, sys + src = os.environ["SRC"] + m = json.load(open(f"{src}/manifest.json")) + chip = m["chip"] + mode = m.get("flash_mode", "dio") + freq = str(m.get("flash_freq", "40m")).lower().replace("mhz", "m") + size = m.get("flash_size", "4MB") + pairs = [] + for f in m["files"]: + name = f.get("name") or f.get("path") + pairs += [f["offset"], f"{src}/{name}"] + cmd = [ + "esptool.py", "--chip", chip, "merge_bin", + "-o", f"{src}/release.bin", + "--flash_mode", mode, "--flash_freq", freq, "--flash_size", size, + *pairs, + ] + print("+", " ".join(cmd)) + subprocess.check_call(cmd) + PY + env: + SRC: flasher/millibyte-flasher/firmware/${{ matrix.board }} + + - name: Stage raw-bin archive + working-directory: ESP32/flasher/millibyte-flasher + run: | + board=${{ matrix.board }} + src=firmware/$board + name=MillibyteFirmware-$board + stage=dist-firmware/$name + rm -rf "$stage" + mkdir -p "$stage" + cp "$src/bootloader.bin" "$src/partitions.bin" "$src/firmware.bin" \ + "$src/boot_app0.bin" "$src/manifest.json" "$src/release.bin" "$stage/" + if [ -f "$src/littlefs.bin" ]; then cp "$src/littlefs.bin" "$stage/"; fi + (cd dist-firmware && zip -r9 "$name.zip" "$name") + ls -la dist-firmware/ + + - uses: actions/upload-artifact@v4 + with: + name: firmware-archive-${{ matrix.board }} + path: ESP32/flasher/millibyte-flasher/dist-firmware/MillibyteFirmware-${{ matrix.board }}.zip + if-no-files-found: error + + package: + name: Package flasher (${{ matrix.os }}) + needs: firmware + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + artifact: MillibyteFlasher-linux-x86_64 + ext: "" + - os: windows-latest + artifact: MillibyteFlasher-windows-x86_64 + ext: ".exe" + - os: macos-latest + artifact: MillibyteFlasher-macos-universal + ext: "" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + workspaces: ESP32/flasher/millibyte-flasher + + - name: Linux GUI deps + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev libgtk-3-dev libxkbcommon-dev libwayland-dev + + - name: Download sr6_pcb firmware + uses: actions/download-artifact@v4 + with: + name: firmware-sr6_pcb + path: ESP32/flasher/millibyte-flasher/firmware/sr6_pcb + + - name: Download ssr1_pcb firmware + uses: actions/download-artifact@v4 + with: + name: firmware-ssr1_pcb + path: ESP32/flasher/millibyte-flasher/firmware/ssr1_pcb + + - name: Build (macOS universal) + if: runner.os == 'macOS' + working-directory: ESP32/flasher/millibyte-flasher + run: | + rustup target add aarch64-apple-darwin x86_64-apple-darwin + cargo build --release --target aarch64-apple-darwin + cargo build --release --target x86_64-apple-darwin + mkdir -p target/release + lipo -create \ + target/aarch64-apple-darwin/release/millibyte-flasher \ + target/x86_64-apple-darwin/release/millibyte-flasher \ + -output target/release/millibyte-flasher + + - name: Build (Linux/Windows) + if: runner.os != 'macOS' + working-directory: ESP32/flasher/millibyte-flasher + run: cargo build --release + + - name: Stage distribution + working-directory: ESP32/flasher/millibyte-flasher + run: | + stage="dist/${{ matrix.artifact }}" + rm -rf "$stage" + mkdir -p "$stage" + cp "target/release/millibyte-flasher${{ matrix.ext }}" "$stage/" + cp README.md "$stage/" || true + mkdir -p "$stage/firmware" + cp -R firmware/sr6_pcb "$stage/firmware/" + cp -R firmware/ssr1_pcb "$stage/firmware/" + ls -laR "$stage" + + - name: Package (Windows) + if: runner.os == 'Windows' + working-directory: ESP32/flasher/millibyte-flasher/dist + run: 7z a "${{ matrix.artifact }}.zip" "${{ matrix.artifact }}" + + - name: Package (Linux) + if: runner.os == 'Linux' + working-directory: ESP32/flasher/millibyte-flasher/dist + run: tar -czvf "${{ matrix.artifact }}.tar.gz" "${{ matrix.artifact }}" + + - name: Package (macOS) + if: runner.os == 'macOS' + working-directory: ESP32/flasher/millibyte-flasher/dist + run: zip -r9 "${{ matrix.artifact }}.zip" "${{ matrix.artifact }}" + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact }} + path: | + ESP32/flasher/millibyte-flasher/dist/${{ matrix.artifact }}.zip + ESP32/flasher/millibyte-flasher/dist/${{ matrix.artifact }}.tar.gz + if-no-files-found: error + + release: + name: Attach to release + needs: package + if: github.event_name == 'release' || startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + - name: Flatten download tree + run: | + mkdir -p release + find dist -type f \( -name '*.zip' -o -name '*.tar.gz' \) -exec cp {} release/ \; + ls -la release/ + - uses: softprops/action-gh-release@v2 + with: + files: release/* + fail_on_unmatched_files: true diff --git a/ESP32/boards/sr6pcb.json b/ESP32/boards/sr6pcb.json new file mode 100644 index 0000000..57dbfac --- /dev/null +++ b/ESP32/boards/sr6pcb.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_ESP32S3_DEV", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DARDUINO_USB_CDC_ON_BOOT=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "sr6pcb" + }, + "connectivity": [ + "wifi", + "bluetooth" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "platforms": [ + "espressif32" + ], + "name": "SR6PCB", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://millibyte.store", + "vendor": "Millibyte LLC" +} \ No newline at end of file diff --git a/ESP32/boards/ssr1pcb.json b/ESP32/boards/ssr1pcb.json new file mode 100644 index 0000000..26cd748 --- /dev/null +++ b/ESP32/boards/ssr1pcb.json @@ -0,0 +1,37 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "ssr1pcb" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "SSR1PCB", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://millibyte.store", + "vendor": "Millibyte LLC" +} \ No newline at end of file diff --git a/ESP32/boards/variants/sr6pcb/pins_arduino.h b/ESP32/boards/variants/sr6pcb/pins_arduino.h new file mode 100644 index 0000000..94ead36 --- /dev/null +++ b/ESP32/boards/variants/sr6pcb/pins_arduino.h @@ -0,0 +1,57 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#include "soc/soc_caps.h" + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 2; +static const uint8_t SCL = 1; + +static const uint8_t SS = 10; +static const uint8_t MOSI = 11; +static const uint8_t MISO = 13; +static const uint8_t SCK = 12; + +static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + 48; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/ESP32/boards/variants/ssr1pcb/pins_arduino.h b/ESP32/boards/variants/ssr1pcb/pins_arduino.h new file mode 100644 index 0000000..3663de7 --- /dev/null +++ b/ESP32/boards/variants/ssr1pcb/pins_arduino.h @@ -0,0 +1,48 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 22; + +static const uint8_t SS = 5; +static const uint8_t MOSI = 23; +static const uint8_t MISO = 19; +static const uint8_t SCK = 18; + +static const uint8_t A0 = 36; +static const uint8_t A3 = 39; +static const uint8_t A4 = 32; +static const uint8_t A5 = 33; +static const uint8_t A6 = 34; +static const uint8_t A7 = 35; +static const uint8_t A10 = 4; +static const uint8_t A11 = 0; +static const uint8_t A12 = 2; +static const uint8_t A13 = 15; +static const uint8_t A14 = 13; +static const uint8_t A15 = 12; +static const uint8_t A16 = 14; +static const uint8_t A17 = 27; +static const uint8_t A18 = 25; +static const uint8_t A19 = 26; + +static const uint8_t T0 = 4; +static const uint8_t T1 = 0; +static const uint8_t T2 = 2; +static const uint8_t T3 = 15; +static const uint8_t T4 = 13; +static const uint8_t T5 = 12; +static const uint8_t T6 = 14; +static const uint8_t T7 = 27; +static const uint8_t T8 = 33; +static const uint8_t T9 = 32; + +static const uint8_t DAC1 = 25; +static const uint8_t DAC2 = 26; + +#endif /* Pins_Arduino_h */ diff --git a/ESP32/configure_wifi_and_reset.bat b/ESP32/configure_wifi_and_reset.bat new file mode 100644 index 0000000..3cf77d6 --- /dev/null +++ b/ESP32/configure_wifi_and_reset.bat @@ -0,0 +1,25 @@ +@echo off +setlocal + +if "%~1"=="" ( + echo Usage: %~nx0 ^ ^ [COMx] + echo Example: %~nx0 MyWifi MyPass123 COM7 + exit /b 1 +) + +set "SSID=%~1" +set "PASSWORD=%~2" +set "PORT=%~3" + +if "%PASSWORD%"=="" ( + echo Usage: %~nx0 ^ ^ [COMx] + exit /b 1 +) + +if "%PORT%"=="" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0configure_wifi_and_reset.ps1" -Ssid "%SSID%" -Password "%PASSWORD%" +) else ( + powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0configure_wifi_and_reset.ps1" -Ssid "%SSID%" -Password "%PASSWORD%" -Port "%PORT%" +) + +endlocal diff --git a/ESP32/configure_wifi_and_reset.ps1 b/ESP32/configure_wifi_and_reset.ps1 new file mode 100644 index 0000000..bdd76ee --- /dev/null +++ b/ESP32/configure_wifi_and_reset.ps1 @@ -0,0 +1,189 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Ssid, + + [Parameter(Mandatory = $true)] + [string]$Password, + + [string]$Port, + + [int]$Baud = 115200, + + [int]$CommandResponseTimeoutSeconds = 5, + + [int]$IpReadTimeoutSeconds = 30 +) + +$ErrorActionPreference = "Continue" + +function Get-AutoPort { + $available = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object + if (-not $available -or $available.Count -eq 0) { + throw "No serial ports found." + } + + $preferred = @() + try { + $deviceRows = Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -match "\(COM\d+\)" } + foreach ($row in $deviceRows) { + if ($row.Name -match "\((COM\d+)\)") { + $com = $Matches[1] + if ( + $row.Name -match "ESP32|USB|UART|CP210|CH340|CH910|Silicon Labs|FTDI" -and + $available -contains $com + ) { + $preferred += $com + } + } + } + } + catch { + # Fallback to first port when CIM query is unavailable. + } + + if ($preferred.Count -gt 0) { + return $preferred[0] + } + + return $available[0] +} + +if (-not $Port) { + $Port = Get-AutoPort +} + +Write-Host "Using serial port: $Port" +Write-Host "Baud: $Baud" + +$serial = New-Object System.IO.Ports.SerialPort $Port, $Baud, ([System.IO.Ports.Parity]::None), 8, ([System.IO.Ports.StopBits]::One) +$serial.NewLine = "`n" +$serial.ReadTimeout = 300 +$serial.WriteTimeout = 1000 +$serial.DtrEnable = $false +$serial.RtsEnable = $false + +function Read-Until { + param( + [int]$TimeoutSeconds, + [string[]]$SuccessPatterns, + [string[]]$FailurePatterns + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + $lines = New-Object System.Collections.Generic.List[string] + + while ((Get-Date) -lt $deadline) { + try { + $line = $serial.ReadLine() + if ($null -eq $line) { + continue + } + + $line = $line.Trim() + if ($line.Length -eq 0) { + continue + } + + Write-Host "< $line" + $lines.Add($line) + + foreach ($failPattern in $FailurePatterns) { + if ($line -match $failPattern) { + throw "Device reported an error: $line" + } + } + + foreach ($successPattern in $SuccessPatterns) { + if ($line -match $successPattern) { + return @{ Matched = $true; Lines = $lines } + } + } + } + catch [System.TimeoutException] { + Start-Sleep -Milliseconds 80 + } + } + + return @{ Matched = $false; Lines = $lines } +} + +function Send-And-Validate { + param( + [string]$Command, + [string]$Display, + [string[]]$SuccessPatterns, + [switch]$RequireMatch + ) + + $failPatterns = @( + "Unknown command", + "Unknown save command", + "Invalid command", + "Invalid value" + ) + + Write-Host "Sending: $Display" + $serial.WriteLine($Command) + + $result = Read-Until -TimeoutSeconds $CommandResponseTimeoutSeconds -SuccessPatterns $SuccessPatterns -FailurePatterns $failPatterns + + if ($RequireMatch -and -not $result.Matched) { + throw "Did not receive expected confirmation for command: $Display" + } +} + +function Read-DeviceIp { + $ipPatterns = @( + "IP\s*Address:\s*([0-9]{1,3}(\.[0-9]{1,3}){3})", + "WiFi\s*connected:\s*([0-9]{1,3}(\.[0-9]{1,3}){3})", + "Captive\s*portal\s*IP:\s*([0-9]{1,3}(\.[0-9]{1,3}){3})" + ) + + $deadline = (Get-Date).AddSeconds($IpReadTimeoutSeconds) + while ((Get-Date) -lt $deadline) { + Write-Host "Sending: #ip" + $serial.WriteLine("#ip") + + $result = Read-Until -TimeoutSeconds 2 -SuccessPatterns $ipPatterns -FailurePatterns @() + if ($result.Matched) { + foreach ($line in $result.Lines) { + foreach ($pattern in $ipPatterns) { + if ($line -match $pattern) { + $candidate = $Matches[1] + if ($candidate -and $candidate -ne "0.0.0.0") { + return $candidate + } + } + } + } + } + + Start-Sleep -Milliseconds 500 + } + + throw "Did not detect device IP via #ip query within timeout." +} + +try { + $serial.Open() + Start-Sleep -Milliseconds 500 + + # Drain boot noise before issuing commands. + $null = Read-Until -TimeoutSeconds 1 -SuccessPatterns @() -FailurePatterns @() + + Send-And-Validate -Command "#wifi-ssid:$Ssid" -Display "#wifi-ssid:" -SuccessPatterns @("Wifi SSID changed to:", "Restart is required after save") -RequireMatch + Send-And-Validate -Command "#wifi-pass:$Password" -Display "#wifi-pass:" -SuccessPatterns @("Wifi password changed to a value of", "Restart is required after save") -RequireMatch + Send-And-Validate -Command '$save' -Display '$save' -SuccessPatterns @("Settings saved!") -RequireMatch + Send-And-Validate -Command "#restart" -Display "#restart" -SuccessPatterns @() + + Write-Host "Waiting for device to reboot and report Wi-Fi IP..." + $ip = Read-DeviceIp + Write-Host "Device IP: $ip" + Write-Host "Done. Validation passed and device restart completed." +} +finally { + if ($serial.IsOpen) { + $serial.Close() + } + $serial.Dispose() +} diff --git a/ESP32/configure_wifi_and_reset.sh b/ESP32/configure_wifi_and_reset.sh new file mode 100644 index 0000000..7f41941 --- /dev/null +++ b/ESP32/configure_wifi_and_reset.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 2 || $# -gt 3 ]]; then + echo "Usage: $0 [serial_port]" + echo "Example: $0 MyWifi MyPass123 /dev/ttyUSB0" + exit 1 +fi + +SSID="$1" +PASSWORD="$2" +PORT="${3:-}" +BAUD=115200 +COMMAND_TIMEOUT=5 +IP_TIMEOUT=30 + +pick_port() { + if [[ -n "$PORT" ]]; then + if [[ ! -e "$PORT" ]]; then + echo "Serial port not found: $PORT" >&2 + exit 1 + fi + echo "$PORT" + return + fi + + if compgen -G "/dev/serial/by-id/*" > /dev/null; then + local first_by_id + first_by_id=$(ls -1 /dev/serial/by-id/* | head -n 1) + if [[ -n "$first_by_id" ]]; then + readlink -f "$first_by_id" + return + fi + fi + + local candidates=(/dev/ttyUSB* /dev/ttyACM*) + for c in "${candidates[@]}"; do + if [[ -e "$c" ]]; then + echo "$c" + return + fi + done + + echo "No serial port found. Pass one explicitly as the third argument." >&2 + exit 1 +} + +PORT="$(pick_port)" + +echo "Using serial port: $PORT" +echo "Baud: $BAUD" + +stty -F "$PORT" "$BAUD" cs8 -cstopb -parenb -ixon -ixoff -echo -hupcl +exec 3<> "$PORT" + +send_cmd() { + local cmd="$1" + local display="${2:-$1}" + echo "Sending: $display" + printf '%s\r\n' "$cmd" >&3 +} + +read_until() { + local timeout="$1" + shift + local pattern_count="$1" + shift + + local patterns=() + local i + for ((i=0; i&2 + return 2 + fi + + if (( ${#patterns[@]} == 0 )); then + continue + fi + + for pat in "${patterns[@]}"; do + if [[ "$line" =~ $pat ]]; then + return 0 + fi + done + fi + done + + return 1 +} + +extract_ip_from_line() { + local line="$1" + if [[ "$line" =~ IP[[:space:]]Address:[[:space:]]([0-9]{1,3}(\.[0-9]{1,3}){3}) ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + if [[ "$line" =~ WiFi[[:space:]]connected:[[:space:]]([0-9]{1,3}(\.[0-9]{1,3}){3}) ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + if [[ "$line" =~ Captive[[:space:]]portal[[:space:]]IP:[[:space:]]([0-9]{1,3}(\.[0-9]{1,3}){3}) ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + return 1 +} + +read_until 1 0 || true + +send_cmd "#wifi-ssid:$SSID" "#wifi-ssid:" +if ! read_until "$COMMAND_TIMEOUT" 2 "Wifi SSID changed to:" "Restart is required after save"; then + echo "Missing confirmation for #wifi-ssid." >&2 + exit 1 +fi + +send_cmd "#wifi-pass:$PASSWORD" "#wifi-pass:" +if ! read_until "$COMMAND_TIMEOUT" 2 "Wifi password changed to a value of" "Restart is required after save"; then + echo "Missing confirmation for #wifi-pass." >&2 + exit 1 +fi + +send_cmd '$save' +if ! read_until "$COMMAND_TIMEOUT" 1 "Settings saved!"; then + echo "Missing confirmation for \$save." >&2 + exit 1 +fi + +send_cmd "#restart" +echo "Waiting for device to reboot and report Wi-Fi IP..." + +deadline=$((SECONDS + IP_TIMEOUT)) +DEVICE_IP="" +next_query=0 +while (( SECONDS < deadline )); do + if (( SECONDS >= next_query )); then + send_cmd "#ip" + next_query=$((SECONDS + 2)) + fi + + if IFS= read -r -t 0.2 line <&3; then + line="${line%$'\r'}" + [[ -z "$line" ]] && continue + echo "< $line" + + candidate_ip="" + if candidate_ip="$(extract_ip_from_line "$line")"; then + if [[ "$candidate_ip" != "0.0.0.0" ]]; then + DEVICE_IP="$candidate_ip" + break + fi + fi + fi +done + +if [[ -z "$DEVICE_IP" ]]; then + echo "Did not detect device IP in serial logs within timeout." >&2 + exit 1 +fi + +echo "Device IP: $DEVICE_IP" +echo "Done. Validation passed and device restart completed." diff --git a/ESP32/data/www/battery-min.js.gz b/ESP32/data/www/battery-min.js.gz deleted file mode 100644 index 55ca568..0000000 Binary files a/ESP32/data/www/battery-min.js.gz and /dev/null differ diff --git a/ESP32/data/www/bldc-motor-min.js.gz b/ESP32/data/www/bldc-motor-min.js.gz deleted file mode 100644 index ccbf21e..0000000 Binary files a/ESP32/data/www/bldc-motor-min.js.gz and /dev/null differ diff --git a/ESP32/data/www/buttons-min.js.gz b/ESP32/data/www/buttons-min.js.gz deleted file mode 100644 index c545cac..0000000 Binary files a/ESP32/data/www/buttons-min.js.gz and /dev/null differ diff --git a/ESP32/data/www/esp-timer-setup-min.js.gz b/ESP32/data/www/esp-timer-setup-min.js.gz deleted file mode 100644 index 8f8f38e..0000000 Binary files a/ESP32/data/www/esp-timer-setup-min.js.gz and /dev/null differ diff --git a/ESP32/data/www/index-min.html.gz b/ESP32/data/www/index-min.html.gz index d902a95..e05e2d4 100644 Binary files a/ESP32/data/www/index-min.html.gz and b/ESP32/data/www/index-min.html.gz differ diff --git a/ESP32/data/www/modal-component-min.js.gz b/ESP32/data/www/modal-component-min.js.gz deleted file mode 100644 index 4f6701d..0000000 Binary files a/ESP32/data/www/modal-component-min.js.gz and /dev/null differ diff --git a/ESP32/data/www/motion-generator-min.js.gz b/ESP32/data/www/motion-generator-min.js.gz deleted file mode 100644 index bee7ced..0000000 Binary files a/ESP32/data/www/motion-generator-min.js.gz and /dev/null differ diff --git a/ESP32/data/www/range-slider-min.css.gz b/ESP32/data/www/range-slider-min.css.gz deleted file mode 100644 index 9acba93..0000000 Binary files a/ESP32/data/www/range-slider-min.css.gz and /dev/null differ diff --git a/ESP32/data/www/range-slider-min.js.gz b/ESP32/data/www/range-slider-min.js.gz deleted file mode 100644 index fec7197..0000000 Binary files a/ESP32/data/www/range-slider-min.js.gz and /dev/null differ diff --git a/ESP32/data/www/settings-min.js.gz b/ESP32/data/www/settings-min.js.gz deleted file mode 100644 index ea6a256..0000000 Binary files a/ESP32/data/www/settings-min.js.gz and /dev/null differ diff --git a/ESP32/data/www/style-min.css.gz b/ESP32/data/www/style-min.css.gz deleted file mode 100644 index 1d735e5..0000000 Binary files a/ESP32/data/www/style-min.css.gz and /dev/null differ diff --git a/ESP32/data/www/utils-min.js.gz b/ESP32/data/www/utils-min.js.gz deleted file mode 100644 index 053fb55..0000000 Binary files a/ESP32/data/www/utils-min.js.gz and /dev/null differ diff --git a/ESP32/dataEdit/www/battery.js b/ESP32/dataEdit/www/battery.js index 93a446a..947f7a5 100644 --- a/ESP32/dataEdit/www/battery.js +++ b/ESP32/dataEdit/www/battery.js @@ -27,6 +27,21 @@ function batterySetup() { document.getElementById('batteryLevelNumeric').checked = userSettings["batteryLevelNumeric"]; //document.getElementById('batteryVoltageMax').value = userSettings["batteryVoltageMax"]; document.getElementById('batteryCapacityMax').value = userSettings["batteryCapacityMax"]; + + document.getElementById('powerMonitor3v3DividerRatio').value = userSettings["powerMonitor3v3DividerRatio"]; + document.getElementById('powerMonitor5vDividerRatio').value = userSettings["powerMonitor5vDividerRatio"]; + document.getElementById('powerMonitorBatteryDividerRatio').value = userSettings["powerMonitorBatteryDividerRatio"]; + document.getElementById('powerMonitorMotorDividerRatio').value = userSettings["powerMonitorMotorDividerRatio"]; + document.getElementById('powerMonitorBusDividerRatio').value = userSettings["powerMonitorBusDividerRatio"]; + + document.getElementById('powerMonitor3v3Offset').value = userSettings["powerMonitor3v3Offset"]; + document.getElementById('powerMonitor5vOffset').value = userSettings["powerMonitor5vOffset"]; + document.getElementById('powerMonitorBatteryOffset').value = userSettings["powerMonitorBatteryOffset"]; + document.getElementById('powerMonitorMotorOffset').value = userSettings["powerMonitorMotorOffset"]; + document.getElementById('powerMonitorBusOffset').value = userSettings["powerMonitorBusOffset"]; + + document.getElementById('powerMonitorVBusNominal').value = userSettings["powerMonitorVBusNominal"]; + document.getElementById('powerMonitorVMotorNominal').value = userSettings["powerMonitorVMotorNominal"]; } function wsBatteryStatus(data) { @@ -35,13 +50,63 @@ function wsBatteryStatus(data) { var batteryCapacityRemainingPercentage = status["batteryCapacityRemainingPercentage"]; var batteryCapacityRemaining = status["batteryCapacityRemaining"]; var batteryTemperature = status["batteryTemperature"]; - + document.getElementById("batteryVoltage").value = batteryVoltage; document.getElementById("batteryCapacityRemaining").value = batteryCapacityRemaining; document.getElementById("batteryCapacityRemainingPercentage").value = batteryCapacityRemainingPercentage; document.getElementById("batteryTemperature").value = batteryTemperature; } +// Timestamp of the most recent user toggle of the VMOTOR enable checkbox. +// While inside the hold-off window we ignore servoVoltageEnabled values from +// powerStatus broadcasts so the UI stays authoritative and a stale broadcast +// (emitted before the firmware processed the toggle) can't snap the checkbox +// back to the previous state. +var vmotorEnabledLastUserToggleMs = 0; +const VMOTOR_TOGGLE_HOLDOFF_MS = 2500; + +function wsPowerStatus(data) { + var status = data["message"] || {}; + + setPowerVoltageField("powerVoltage3v3", status["Voltage_3V3"]); + setPowerVoltageField("powerVoltage5v", status["Voltage_5V"]); + setPowerVoltageField("powerVoltageBattery", status["Voltage_Battery"]); + setPowerVoltageField("powerVoltageMotor", status["Voltage_Motor"]); + setPowerVoltageField("powerVoltageBus", status["Voltage_Bus"]); + + // Update VMOTOR enable state if available, but defer to the user during + // the post-click hold-off so an in-flight broadcast can't undo their toggle. + if (status["servoVoltageEnabled"] !== undefined) { + const vmotorEnabledElement = document.getElementById("vmotorEnabled"); + if (vmotorEnabledElement) { + const sinceToggle = Date.now() - vmotorEnabledLastUserToggleMs; + if (sinceToggle >= VMOTOR_TOGGLE_HOLDOFF_MS) { + vmotorEnabledElement.checked = status["servoVoltageEnabled"]; + } + } + } +} + +function setPowerVoltageField(elementId, sourceStatus) { + var element = document.getElementById(elementId); + if(!element) + return; + if(!sourceStatus) { + element.value = "Unset"; + return; + } + var adcVoltage = sourceStatus["adcVoltage"]; + var railVoltage = sourceStatus["railVoltage"]; + var pin = sourceStatus["pin"]; + var percentage = sourceStatus["percentage"]; + if(adcVoltage === undefined || railVoltage === undefined || pin === undefined) { + element.value = "Unknown"; + return; + } + element.value = railVoltage.toFixed(2) + " V (ADC " + adcVoltage.toFixed(3) + "V @ pin " + pin + ")" + + (percentage !== undefined ? " [" + percentage.toFixed(1) + "%]" : ""); +} + function toggleBatterySettings(batteryEnabled) { var batteryOnly = document.getElementsByClassName('batteryOnly'); for(var i=0;i < batteryOnly.length; i++) @@ -55,7 +120,35 @@ function setBatterySettings() { userSettings["batteryCapacityMax"] = parseFloat(document.getElementById('batteryCapacityMax').value); setRestartRequired(); updateUserSettings(); -} +} + +function setPowerMonitorSettings() { + userSettings["powerMonitor3v3DividerRatio"] = parseFloat(document.getElementById('powerMonitor3v3DividerRatio').value); + userSettings["powerMonitor5vDividerRatio"] = parseFloat(document.getElementById('powerMonitor5vDividerRatio').value); + userSettings["powerMonitorBatteryDividerRatio"] = parseFloat(document.getElementById('powerMonitorBatteryDividerRatio').value); + userSettings["powerMonitorMotorDividerRatio"] = parseFloat(document.getElementById('powerMonitorMotorDividerRatio').value); + userSettings["powerMonitorBusDividerRatio"] = parseFloat(document.getElementById('powerMonitorBusDividerRatio').value); + + userSettings["powerMonitor3v3Offset"] = parseFloat(document.getElementById('powerMonitor3v3Offset').value); + userSettings["powerMonitor5vOffset"] = parseFloat(document.getElementById('powerMonitor5vOffset').value); + userSettings["powerMonitorBatteryOffset"] = parseFloat(document.getElementById('powerMonitorBatteryOffset').value); + userSettings["powerMonitorMotorOffset"] = parseFloat(document.getElementById('powerMonitorMotorOffset').value); + userSettings["powerMonitorBusOffset"] = parseFloat(document.getElementById('powerMonitorBusOffset').value); + + userSettings["powerMonitorVBusNominal"] = parseFloat(document.getElementById('powerMonitorVBusNominal').value); + userSettings["powerMonitorVMotorNominal"] = parseFloat(document.getElementById('powerMonitorVMotorNominal').value); + + setRestartRequired(); + updateUserSettings(); +} function setBatteryFull() { sendWebsocketCommand("setBatteryFull"); +} + +function setVmotorEnabled() { + const enabled = document.getElementById('vmotorEnabled').checked; + // Mark the click time so wsPowerStatus ignores the next few broadcasts and + // the user's choice remains authoritative. + vmotorEnabledLastUserToggleMs = Date.now(); + sendWebsocketCommand("setServoVoltageEnabled", enabled ? "true" : "false"); } \ No newline at end of file diff --git a/ESP32/dataEdit/www/bldc-motor.js b/ESP32/dataEdit/www/bldc-motor.js index ea3df02..08b0131 100644 --- a/ESP32/dataEdit/www/bldc-motor.js +++ b/ESP32/dataEdit/www/bldc-motor.js @@ -1,6 +1,6 @@ -/* MIT License +/* MIT License -Copyright (c) 2026 Jason C. Fain +Copyright (c) 2024 Jason C. Fain Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -20,163 +20,236 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -class BLDCMotor { - name = ""; - Names = {}; - deviceType; - ModalNode; - ParentNode; - initialized = false; - initializedPins = false; - constructor(deviceType, name = "") { - this.name = name; - this.deviceType = deviceType; - this.ModalNode = document.getElementById(this.name + "MotorSettings"); - if(!this.ModalNode) - { - this.ModalNode = document.createElement("modal-component"); - this.ModalNode.id = "motor" + this.name + "Settings"; - const header = document.createElement("span"); - // This is like this because the settings for the A motor does not have 'A' in it for SSR1. - header.innerText = isSSR1() ? "Motor Settings" : "Motor " + (this.name.length ? this.name : "A") + " Settings" - header.setAttribute("slot", "title"); - this.ModalNode.appendChild(header); - document.body.appendChild(this.ModalNode); - - const ParentTable = document.createElement("div"); - ParentTable.id = this.name + "MotorSettingsTable"; - ParentTable.setAttribute("name", this.name + "MotorSettingsTable"); - ParentTable.classList.add("formTable"); - this.ParentNode = document.createElement("div"); - ParentTable.appendChild(this.ParentNode); - this.ModalNode.appendChild(ParentTable); +BLDCMotor = { + setup() { + var encoderEl = document.getElementById("BLDC_Encoder"); + encoderEl.value = userSettings["BLDC_Encoder"]; + // SSR1PCB always ships with an MT6701 SSI encoder; lock the dropdown + // to that selection (BLDCEncoderType::MT6701 = 1). + if (typeof isBoardType === "function" && isBoardType(BoardType.SSR1PCB)) { + encoderEl.value = 1; // MT6701 SSI + encoderEl.disabled = true; + encoderEl.title = "SSR1PCB ships with an MT6701 SSI encoder; this is fixed."; + // Persist if the saved value is wrong so other consumers see MT6701 + if (userSettings["BLDC_Encoder"] !== 1) { + userSettings["BLDC_Encoder"] = 1; + if (typeof updateUserSettings === "function") updateUserSettings(); + } + } else { + encoderEl.disabled = false; + encoderEl.title = ""; } - - // this.Names.BLDC_Encoder = "BLDC_" + name + "Encoder"; - // this.Names.BLDC_UseHallSensor = "BLDC_" + name + "UseHallSensor"; - this.Names.BLDC_Encoder = "BLDC_Encoder"; - // this.Names.BLDC_UseHallSensor = "BLDC_UseHallSensor"; - this.Names.BLDC_Pulley_Circumference = "BLDC_" + name + "Pulley_Circumference"; - this.Names.BLDC_Motor_VoltageLimit = "BLDC_" + name + "Motor_VoltageLimit"; - this.Names.BLDC_Motor_SupplyVoltage = "BLDC_" + name + "Motor_SupplyVoltage"; - this.Names.BLDC_Motor_Current = "BLDC_" + name + "Motor_Current"; - this.Names.BLDC_Motor_ZeroElecAngle = "BLDC_" + name + "Motor_ZeroElecAngle"; - // this.Names.BLDC_RailLength = "BLDC_" + name + "RailLength"; - // this.Names.BLDC_Range = "BLDC_" + (name.length == 0 ? "Stroke" : name) + "Length"; - this.Names.BLDC_ChipSelect_PIN = "BLDC_" + name + "ChipSelect_PIN"; - this.Names.BLDC_Encoder_PIN = "BLDC_" + name + "Encoder_PIN"; - this.Names.BLDC_Enable_PIN = "BLDC_" + name + "Enable_PIN"; - this.Names.BLDC_PWMchannel1_PIN = "BLDC_" + name + "PWMchannel1_PIN"; - this.Names.BLDC_PWMchannel2_PIN = "BLDC_" + name + "PWMchannel2_PIN"; - this.Names.BLDC_PWMchannel3_PIN = "BLDC_" + name + "PWMchannel3_PIN"; - // this.Names.BLDC_HallEffect_PIN = "BLDC_" + name + "HallEffect_PIN"; - // this.Names.HallEffect_Row = name + "HallEffect", - this.Names.ZeroElecAngle_Row = name + "ZeroElecAngle"; + document.getElementById("BLDC_UseHallSensor").checked = userSettings["BLDC_UseHallSensor"]; + document.getElementById("BLDC_Pulley_Circumference").value = userSettings["BLDC_Pulley_Circumference"]; + document.getElementById("BLDC_MotorA_VoltageLimit").value = Utils.round2(userSettings["BLDC_MotorA_VoltageLimit"]); + document.getElementById("BLDC_MotorA_SupplyVoltage").value = Utils.round2(userSettings["BLDC_MotorA_SupplyVoltage"]); + document.getElementById("BLDC_MotorA_Current").value = Utils.round2(userSettings["BLDC_MotorA_Current"]); + document.getElementById("BLDC_MotorA_ZeroElecAngle").value = Utils.round2(userSettings["BLDC_MotorA_ZeroElecAngle"]); + document.getElementById("BLDC_MotorA_ParametersKnown").checked = userSettings["BLDC_MotorA_ParametersKnown"]; + document.getElementById("BLDC_RailLength").value = userSettings["BLDC_RailLength"]; + document.getElementById("BLDC_StrokeLength").value = userSettings["BLDC_StrokeLength"]; + if (userSettings["BLDC_PIDProportionalConstant"] !== undefined) + document.getElementById("BLDC_PIDProportionalConstant").value = userSettings["BLDC_PIDProportionalConstant"]; + if (userSettings["BLDC_LowPassFilter"] !== undefined) + document.getElementById("BLDC_LowPassFilter").value = userSettings["BLDC_LowPassFilter"]; + + toggleBLDCEncoderOptions(); + Utils.toggleControlVisibilityByID("HallEffect", userSettings["BLDC_UseHallSensor"]); + Utils.toggleControlVisibilityByID("ZeroElecAngle", userSettings["BLDC_MotorA_ParametersKnown"]); + }, + setupPins() { + document.getElementById("BLDC_ChipSelect_PIN").value = pinoutSettings["BLDC_ChipSelect_PIN"]; + document.getElementById("BLDC_Encoder_PIN").value = pinoutSettings["BLDC_Encoder_PIN"]; + document.getElementById("BLDC_Enable_PIN").value = pinoutSettings["BLDC_Enable_PIN"]; + document.getElementById("BLDC_PWMchannel1_PIN").value = pinoutSettings["BLDC_PWMchannel1_PIN"]; + document.getElementById("BLDC_PWMchannel2_PIN").value = pinoutSettings["BLDC_PWMchannel2_PIN"]; + document.getElementById("BLDC_PWMchannel3_PIN").value = pinoutSettings["BLDC_PWMchannel3_PIN"]; + document.getElementById("BLDC_HallEffect_PIN").value = pinoutSettings["BLDC_HallEffect_PIN"]; } + // TODO: move bldc stuff in to here. Follow this pattern moving forward. +} +function updateBLDCSettings() { + userSettings["BLDC_UseHallSensor"] = document.getElementById('BLDC_UseHallSensor').checked; + Utils.toggleControlVisibilityByID("HallEffect", userSettings["BLDC_UseHallSensor"]); + userSettings["BLDC_Pulley_Circumference"] = parseInt(document.getElementById('BLDC_Pulley_Circumference').value); + userSettings["BLDC_MotorA_VoltageLimit"] = Utils.round2(parseFloat(document.getElementById('BLDC_MotorA_VoltageLimit').value)); + userSettings["BLDC_MotorA_SupplyVoltage"] = Utils.round2(parseFloat(document.getElementById('BLDC_MotorA_SupplyVoltage').value)); + userSettings["BLDC_MotorA_Current"] = Utils.round2(parseFloat(document.getElementById('BLDC_MotorA_Current').value)); + userSettings["BLDC_MotorA_ZeroElecAngle"] = Utils.round2(parseFloat(document.getElementById('BLDC_MotorA_ZeroElecAngle').value)); + userSettings["BLDC_MotorA_ParametersKnown"] = document.getElementById("BLDC_MotorA_ParametersKnown").checked; + userSettings["BLDC_RailLength"] = parseInt(document.getElementById('BLDC_RailLength').value); + userSettings["BLDC_StrokeLength"] = parseInt(document.getElementById('BLDC_StrokeLength').value); + var pidEl = document.getElementById('BLDC_PIDProportionalConstant'); + if (pidEl && pidEl.value !== "") + userSettings["BLDC_PIDProportionalConstant"] = parseFloat(pidEl.value); + var lpEl = document.getElementById('BLDC_LowPassFilter'); + if (lpEl && lpEl.value !== "") + userSettings["BLDC_LowPassFilter"] = parseFloat(lpEl.value); + Utils.toggleControlVisibilityByID("ZeroElecAngle", userSettings["BLDC_MotorA_ParametersKnown"]); + setRestartRequired(); + updateUserSettings(); +} + +function updateBLDCPins() { + if(upDateTimeout !== null) + { + clearTimeout(upDateTimeout); + } + upDateTimeout = setTimeout(() => + { + var pinValues = validateBLDCPins(); + if(pinValues) { + pinoutSettings["BLDC_ChipSelect_PIN"] = pinValues.BLDC_ChipSelect_PIN; + pinoutSettings["BLDC_Encoder_PIN"] = pinValues.BLDC_Encoder_PIN; + pinoutSettings["BLDC_Enable_PIN"] = pinValues.BLDC_Enable_PIN; + pinoutSettings["BLDC_PWMchannel1_PIN"] = pinValues.BLDC_PWMchannel1_PIN; + pinoutSettings["BLDC_PWMchannel2_PIN"] = pinValues.BLDC_PWMchannel2_PIN; + pinoutSettings["BLDC_PWMchannel3_PIN"] = pinValues.BLDC_PWMchannel3_PIN; + pinoutSettings["BLDC_HallEffect_PIN"] = pinValues.BLDC_HallEffect_PIN; + updateCommonPins(pinValues); + setRestartRequired(); + postPinoutSettings(); + } + }, 2000); +} + +function getBLDCPinValues() { + var pinValues = {}; + pinValues.BLDC_ChipSelect_PIN = parseInt(document.getElementById('BLDC_ChipSelect_PIN').value); + pinValues.BLDC_Encoder_PIN = parseInt(document.getElementById('BLDC_Encoder_PIN').value); + pinValues.BLDC_Enable_PIN = parseInt(document.getElementById('BLDC_Enable_PIN').value); + pinValues.BLDC_PWMchannel1_PIN = parseInt(document.getElementById('BLDC_PWMchannel1_PIN').value); + pinValues.BLDC_PWMchannel2_PIN = parseInt(document.getElementById('BLDC_PWMchannel2_PIN').value); + pinValues.BLDC_PWMchannel3_PIN = parseInt(document.getElementById('BLDC_PWMchannel3_PIN').value); + pinValues.BLDC_HallEffect_PIN = parseInt(document.getElementById('BLDC_HallEffect_PIN').value); + getCommonPinValues(pinValues); + return pinValues; +} + +function validateBLDCPins() { + clearErrors("pinValidation"); + var assignedPins = []; + var duplicatePins = []; + var pwmErrors = []; + var pinValues = getBLDCPinValues(); + if(userSettings["disablePinValidation"]) + return pinValues; - setup() { - if(this.initialized) - return; - - // let channelRow = Utils.createNumericFormRow(0, "Update rate (ms)", 'motionUpdate'+profileIndex+channelIndex, motionChannel ? motionChannel.update : 100, 0, 2147483647, 1, - // function(profileIndex, channelIndex, name) {setMotionGeneratorSettings(profileIndex, channelIndex, name)}.bind(this, profileIndex, channelIndex, name)); - // channelRow.title = `This is the time in between updates that gives the system time to process other tasks. (DO NOT SET TOO LOW ON ESP32!) - // It may be best to just leave at default.` - // channelTableDiv.appendChild(channelRow.row); - - //ToDo create combo box maybe - // const encoderNode = Utils.createNumericFormRow(null, "Encoder", this.Names.BLDC_Encoder, userSettings[this.Names.BLDC_Encoder], null, null, null, this.setEncoderType); - // motorSettingsTable.appendChild(encoderNode.row); - // // document.getElementById(this.Names.BLDC_Encoder).value = userSettings[this.Names.BLDC_Encoder]; - // this.createBLDCCheckboxFormNode(this.Names.BLDC_UseHallSensor, "Use hall sensor", userSettings[this.Names.BLDC_UseHallSensor], () => this.updateBLDCSettings(0)); - if(this.deviceType == DeviceType.SSR1) - { - this.createBLDCNumericFormNode(this.Names.BLDC_Pulley_Circumference, "Pulley Circumference (mm)", userSettings[this.Names.BLDC_Pulley_Circumference], () => this.updateBLDCSettings(), 0, 2147483647, 1); + if(isModuleType(ModuleType.S3)) + { + if(isBoardType(BoardType.ZERO)) { + if(isBLDCSPI()) { + assignedPins.push({name:"SPI MOSI", pin:11}); + } + } else { + // TODO validate this for N8R8 + //assignedPins.push({name:"SPI1", pin:5}); + assignedPins.push({name:"SPI CLK", pin:18}); + assignedPins.push({name:"SPI MISO", pin:19}); + if(isBLDCSPI()) { + assignedPins.push({name:"SPI MOSI", pin:23}); + } } - this.createBLDCNumericFormNode(this.Names.BLDC_Motor_VoltageLimit, "Voltage limit (v)", userSettings[this.Names.BLDC_Motor_VoltageLimit], () => this.updateBLDCSettings(), 0.0, 2147483647.0, 0.01); - this.createBLDCNumericFormNode(this.Names.BLDC_Motor_SupplyVoltage, "Supply voltage (v)", userSettings[this.Names.BLDC_Motor_SupplyVoltage], () => this.updateBLDCSettings(), 0.0, 2147483647.0, 0.01); - this.createBLDCNumericFormNode(this.Names.BLDC_Motor_Current, "Motor current (a)", userSettings[this.Names.BLDC_Motor_Current], () => this.updateBLDCSettings(), 0.0, 2147483647.0, 0.01); - this.createBLDCNumericFormNode(this.Names.BLDC_Motor_ZeroElecAngle, "Zero elec angle (rad)", userSettings[this.Names.BLDC_Motor_ZeroElecAngle], () => this.updateBLDCSettings(), -2147483647.0, 2147483647.0, 0.01, this.Names.ZeroElecAngle_Row); - // this.createBLDCNumericFormNode(this.Names.BLDC_RailLength, "Rail length (mm)", userSettings[this.Names.BLDC_RailLength], () => this.updateBLDCSettings(), 0, 2147483647); - // this.createBLDCNumericFormNode(this.Names.BLDC_Range, (this.name.length == 0 ? "Stroke" : this.name) + " length (mm)", userSettings[this.Names.BLDC_Range], () => this.updateBLDCSettings(), 0, 2147483647, 1); - - // this.toggleBLDCEncoderOptions(); - // Utils.toggleControlVisibilityByID(this.Names.HallEffect_Row, userSettings[this.Names.BLDC_UseHallSensor]); - this.initialized = true; } - - createBLDCNumericFormNode(key, label, value, callback, min = undefined, max = undefined, step = undefined, rowName = undefined, classList = []) { - const node = Utils.createNumericFormRow(rowName, label, key, value, min, max, step, callback); - if(classList.length > 0) { - node.row.classList.add(...classList); + else + { + //assignedPins.push({name:"SPI1", pin:5}); + assignedPins.push({name:"SPI CLK", pin:18}); + assignedPins.push({name:"SPI MISO", pin:19}); + if(isBLDCSPI()) { + assignedPins.push({name:"SPI MOSI", pin:23}); } - this.ParentNode.appendChild(node.row); } - createBLDCCheckboxFormNode(key, label, value, callback, rowName = undefined) { - const node = Utils.createCheckboxFormRow(rowName, label, key, value, callback); - this.ParentNode.appendChild(node.row); + // var pinDupeIndex = -1; + // if(pinValues.BLDC_Encoder_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_Encoder_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("Encoder pin and "+assignedPins[pinDupeIndex].name); + // assignedPins.push({name:"Encoder", pin:pinValues.BLDC_Encoder_PIN}); + // } + validatePin(pinValues.BLDC_Encoder_PIN, "Encoder", assignedPins, duplicatePins); + + + // if(pinValues.BLDC_ChipSelect_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_ChipSelect_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("Chip select and "+assignedPins[pinDupeIndex].name); + // assignedPins.push({name:"Chip select", pin:pinValues.BLDC_ChipSelect_PIN}); + // } + validatePin(pinValues.BLDC_ChipSelect_PIN, "Chip select", assignedPins, duplicatePins); + + // if(pinValues.BLDC_Enable_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_Enable_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("Enable pin and "+assignedPins[pinDupeIndex].name); + // assignedPins.push({name:"Enable", pin:pinValues.BLDC_Enable_PIN}); + // } + validatePin(pinValues.BLDC_Enable_PIN, "Enable", assignedPins, duplicatePins); + + // if(pinValues.BLDC_PWMchannel1_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_PWMchannel1_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("PWMchannel1 pin and "+assignedPins[pinDupeIndex].name); + // if(validPWMpins.indexOf(pinValues.BLDC_PWMchannel1_PIN) == -1) + // pwmErrors.push("PWMchannel1 pin: "+pinValues.BLDC_PWMchannel1_PIN); + // assignedPins.push({name:"PWMchannel1", pin:pinValues.BLDC_PWMchannel1_PIN}); + // } + validatePWMPin(pinValues.rightPin, "PWMchannel1", assignedPins, duplicatePins, pwmErrors); + + // if(pinValues.BLDC_PWMchannel2_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_PWMchannel2_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("PWMchannel2 pin and "+assignedPins[pinDupeIndex].name); + // if(validPWMpins.indexOf(pinValues.BLDC_PWMchannel2_PIN) == -1) + // pwmErrors.push("PWMchannel2 pin: "+pinValues.BLDC_PWMchannel2_PIN); + // assignedPins.push({name:"PWMchannel2", pin:pinValues.BLDC_PWMchannel2_PIN}); + // } + validatePWMPin(pinValues.rightPin, "PWMchannel2", assignedPins, duplicatePins, pwmErrors); + + // if(pinValues.BLDC_PWMchannel3_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_PWMchannel3_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("PWMchannel3 pin and "+assignedPins[pinDupeIndex].name); + // if(validPWMpins.indexOf(pinValues.BLDC_PWMchannel3_PIN) == -1) + // pwmErrors.push("PWMchannel3pin: "+pinValues.BLDC_PWMchannel3_PIN); + // assignedPins.push({name:"PWMchannel3", pin:pinValues.BLDC_PWMchannel3_PIN}); + // } + validatePWMPin(pinValues.BLDC_PWMchannel3_PIN, "PWMchannel3", assignedPins, duplicatePins, pwmErrors); + + if(userSettings["BLDC_UseHallSensor"]) { + // if(pinValues.BLDC_HallEffect_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_HallEffect_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("Hall effect pin and "+assignedPins[pinDupeIndex].name); + // assignedPins.push({name:"HallEffect", pin:pinValues.BLDC_HallEffect_PIN}); + // } + validatePin(pinValues.BLDC_HallEffect_PIN, "HallEffect", assignedPins, duplicatePins); } - setupPins() { - if(this.initializedPins) - return; - this.createBLDCNumericFormNode(this.Names.BLDC_ChipSelect_PIN, "Chip select PIN", pinoutSettings[this.Names.BLDC_ChipSelect_PIN], () => this.updateBLDCPins(), -1, 2147483647, 1, null, ["BLDCSPI"]); - this.createBLDCNumericFormNode(this.Names.BLDC_Encoder_PIN, "Encoder PIN", pinoutSettings[this.Names.BLDC_Encoder_PIN], () => this.updateBLDCPins(), -1, 2147483647, 1, null, ["BLDCPWM"]); - this.createBLDCNumericFormNode(this.Names.BLDC_Enable_PIN, "Enable PIN", pinoutSettings[this.Names.BLDC_Enable_PIN], () => this.updateBLDCPins(), -1, 2147483647); - this.createBLDCNumericFormNode(this.Names.BLDC_PWMchannel1_PIN, "PWM channel 1 PIN", pinoutSettings[this.Names.BLDC_PWMchannel1_PIN], () => this.updateBLDCPins(), -1, 2147483647); - this.createBLDCNumericFormNode(this.Names.BLDC_PWMchannel2_PIN, "PWM channel 2 PIN", pinoutSettings[this.Names.BLDC_PWMchannel2_PIN], () => this.updateBLDCPins(), -1, 2147483647); - this.createBLDCNumericFormNode(this.Names.BLDC_PWMchannel3_PIN, "PWM channel 3 PIN", pinoutSettings[this.Names.BLDC_PWMchannel3_PIN], () => this.updateBLDCPins(), -1, 2147483647); - // this.createBLDCNumericFormNode(this.Names.BLDC_HallEffect_PIN, "Hall effect PIN", pinoutSettings[this.Names.BLDC_HallEffect_PIN], () => this.updateBLDCPins(), -1, 2147483647, this.Names.HallEffect_Row); - this.initializedPins = true; - } + validateCommonPWMPins(assignedPins, duplicatePins, pinValues, pwmErrors); - // TODO: move bldc stuff in to here. Follow this pattern moving forward. - updateBLDCSettings(delay = defaultDebounce) { - Utils.debounce("updateBLDCSettings", () => { - if(this.deviceType == DeviceType.SSR1) - { - userSettings[this.Names.BLDC_Pulley_Circumference] = parseInt(document.getElementById(this.Names.BLDC_Pulley_Circumference).value); - } - userSettings[this.Names.BLDC_Motor_VoltageLimit] = Utils.round2(parseFloat(document.getElementById(this.Names.BLDC_Motor_VoltageLimit).value)); - userSettings[this.Names.BLDC_Motor_SupplyVoltage] = Utils.round2(parseFloat(document.getElementById(this.Names.BLDC_Motor_SupplyVoltage).value)); - userSettings[this.Names.BLDC_Motor_Current] = Utils.round2(parseFloat(document.getElementById(this.Names.BLDC_Motor_Current).value)); - userSettings[this.Names.BLDC_Motor_ZeroElecAngle] = Utils.round2(parseFloat(document.getElementById(this.Names.BLDC_Motor_ZeroElecAngle).value)); - // userSettings[this.Names.BLDC_RailLength] = parseInt(document.getElementById(this.Names.BLDC_RailLength).value); - // userSettings[this.Names.BLDC_Range] = parseInt(document.getElementById(this.Names.BLDC_Range).value); - setRestartRequired(); - updateUserSettings(0); - }, delay); - } - - updateBLDCPins(pinValues) { - pinoutSettings[this.Names.BLDC_ChipSelect_PIN] = pinValues[this.Names.BLDC_ChipSelect_PIN]; - pinoutSettings[this.Names.BLDC_Encoder_PIN] = pinValues[this.Names.BLDC_Encoder_PIN]; - pinoutSettings[this.Names.BLDC_Enable_PIN] = pinValues[this.Names.BLDC_Enable_PIN]; - pinoutSettings[this.Names.BLDC_PWMchannel1_PIN] = pinValues[this.Names.BLDC_PWMchannel1_PIN]; - pinoutSettings[this.Names.BLDC_PWMchannel2_PIN] = pinValues[this.Names.BLDC_PWMchannel2_PIN]; - pinoutSettings[this.Names.BLDC_PWMchannel3_PIN] = pinValues[this.Names.BLDC_PWMchannel3_PIN]; - } + var invalidPins = []; + validateNonPWMPins(assignedPins, duplicatePins, invalidPins, pinValues); - getBLDCPinValues(pinValues = {}) { - pinValues[this.Names.BLDC_ChipSelect_PIN] = parseInt(document.getElementById(this.Names.BLDC_ChipSelect_PIN).value); - pinValues[this.Names.BLDC_Encoder_PIN] = parseInt(document.getElementById(this.Names.BLDC_Encoder_PIN).value); - pinValues[this.Names.BLDC_Enable_PIN] = parseInt(document.getElementById(this.Names.BLDC_Enable_PIN).value); - pinValues[this.Names.BLDC_PWMchannel1_PIN] = parseInt(document.getElementById(this.Names.BLDC_PWMchannel1_PIN).value); - pinValues[this.Names.BLDC_PWMchannel2_PIN] = parseInt(document.getElementById(this.Names.BLDC_PWMchannel2_PIN).value); - pinValues[this.Names.BLDC_PWMchannel3_PIN] = parseInt(document.getElementById(this.Names.BLDC_PWMchannel3_PIN).value); - return pinValues; - } + if (duplicatePins.length || pwmErrors.length || invalidPins.length) { + var errorString = "
Pins NOT saved due to invalid input.
"; + if(duplicatePins.length ) + errorString += "
The following pins are duplicated:
"+duplicatePins.join("
")+"
"; + if(invalidPins.length) { + if(duplicatePins.length) + errorString += "
"; + errorString += "
The following pins are invalid:
"+invalidPins.join("
")+"
"; + } + if (pwmErrors.length) { + if(duplicatePins.length || invalidPins.length) { + errorString += "
"; + } + errorString += "
The following pins are invalid PWM pins:
"+pwmErrors.join("
")+"
"; + } - validateBLDCPins(pinValues, assignedPins, duplicatePins, pwmErrors, invalidPins) { - const name = "Motor " + (this.name.length ? this.name : "A"); - if(!isBLDCSPI()) - validatePin(pinValues[this.Names.BLDC_Encoder_PIN], name+" Encoder", assignedPins, duplicatePins, false, invalidPins); - else - validatePin(pinValues[this.Names.BLDC_ChipSelect_PIN], name+" Chip select", assignedPins, duplicatePins, false, invalidPins); - validatePin(pinValues[this.Names.BLDC_Enable_PIN], name+" Enable", assignedPins, duplicatePins, false, invalidPins); - validatePWMPin(pinValues[this.Names.BLDC_PWMchannel1_PIN], name+" PWMchannel1", assignedPins, duplicatePins, pwmErrors, invalidPins); - validatePWMPin(pinValues[this.Names.BLDC_PWMchannel2_PIN], name+" PWMchannel2", assignedPins, duplicatePins, pwmErrors, invalidPins); - validatePWMPin(pinValues[this.Names.BLDC_PWMchannel3_PIN], name+" PWMchannel3", assignedPins, duplicatePins, pwmErrors, invalidPins); - return pinValues; + errorString += "
"; + showError(errorString); + return undefined; } -} \ No newline at end of file + return pinValues; +} diff --git a/ESP32/dataEdit/www/esp-timer-setup.js b/ESP32/dataEdit/www/esp-timer-setup.js index fefaf84..f6c7615 100644 --- a/ESP32/dataEdit/www/esp-timer-setup.js +++ b/ESP32/dataEdit/www/esp-timer-setup.js @@ -24,6 +24,8 @@ ESPTimer = { initialized: false, model: {}, debounces: [], + // PwmDriver enum values must match C++ enum class PwmDriver + PwmDriver: { MCPWM: 0, LEDC: 1 }, show() { this.modal.show(); }, @@ -35,13 +37,15 @@ ESPTimer = { this.modal = document.getElementById("espTimerSetupModal"); let table = Utils.createModalTableSection(this.modal, "Timer setup"); let availableTimers = systemInfo["availableTimers"]; - // We dont know what the resolution of the attached device at this point. - // Maybe with a lookup and a bit of a redesign we can validate the frequency. + // We dont know what the resolution of the attached device at this point. + // Maybe with a lookup and a bit of a redesign we can validate the frequency. // For now, the user needs to know what they are doing here. //const maxHz = Math.floor(80000000 / (2 ** systemInfo.servoPWMResolution));// 2^16bit = 65536. 80000000(80Mhz) ÷ 65536 = 1220.703125. floor = 1220 for (let index = 0; index < availableTimers.length; index++) { const timerObj = availableTimers[index]; - let timerFrequencyRow = Utils.createNumericFormRow(0, timerObj.name + " (hz)", 'timerFrequency'+index, pinoutSettings[timerObj.id], 1, Number.MAX_SAFE_INTEGER, 1); + + // Frequency row + let timerFrequencyRow = Utils.createNumericFormRow(0, timerObj.name + " (hz)", 'timerFrequency'+index, pinoutSettings[timerObj.id], 50, 80000000); timerFrequencyRow.title = `Set the frequency of this timer`; timerFrequencyRow.input.oninput = function(timerObj, timerFrequencyRow) { if(this.debounces[timerObj.id]) @@ -53,20 +57,88 @@ ESPTimer = { } }, defaultDebounce); }.bind(this, timerObj, timerFrequencyRow); - table.body.appendChild(timerFrequencyRow.row); + + // Driver row — only shown when the driverKey is present (ESP-IDF 5+ with PwmDriver support) + if(timerObj.driverKey) { + let driverRow = Utils.createFormRow(0); + let driverLabel = Utils.createFormCell(0, timerObj.name + " driver"); + let driverCell = Utils.createFormCell(); + let driverSelect = document.createElement("select"); + driverSelect.id = 'timerDriver' + index; + driverSelect.title = "PWM driver for outputs assigned to this timer. MCPWM gives servo-grade timing precision; LEDC is for motors/misc. The firmware falls back to LEDC automatically if MCPWM is full."; + + let mcpwmOption = document.createElement("option"); + mcpwmOption.value = this.PwmDriver.MCPWM; + mcpwmOption.innerText = "MCPWM (servo)"; + let ledcOption = document.createElement("option"); + ledcOption.value = this.PwmDriver.LEDC; + ledcOption.innerText = "LEDC (vibe/misc)"; + driverSelect.appendChild(mcpwmOption); + driverSelect.appendChild(ledcOption); + driverSelect.value = pinoutSettings[timerObj.driverKey] !== undefined + ? pinoutSettings[timerObj.driverKey] + : timerObj.pwmDriver; + + driverSelect.onchange = function(timerObj, driverSelect) { + pinoutSettings[timerObj.driverKey] = parseInt(driverSelect.value); + setRestartRequired(); + postPinoutSettings(0); + validatePwmDriverContention(); + }.bind(this, timerObj, driverSelect); + + driverCell.appendChild(driverSelect); + driverRow.appendChild(driverLabel); + driverRow.appendChild(driverCell); + table.body.appendChild(driverRow); + } } const helpTextNodeDiv = document.createElement("div"); helpTextNodeDiv.style = "font-size: 0.6em;" const freqMhz = systemInfo.apbClockFrequency / 1000000; - helpTextNodeDiv.innerHTML = + helpTextNodeDiv.innerHTML = ` To calculate the MAXIMUM frequency for your chip (Not the servo) -
use the formula: +
use the formula:
     ${systemInfo.apbClockFrequency} ÷ (2^resolution)
The max resolution for your chip is ${systemInfo.maxPWMResolution} bit
The APB clock frequency is ${freqMhz} Mhz `; table.body.appendChild(helpTextNodeDiv); + }, + /** + * Returns an object { mcpwm: N, ledc: N } counting how many LEDC-channel + * outputs are assigned to MCPWM timers and vice-versa, based on the current + * pinoutSettings and availableTimers driver config. + */ + getDriverCounts() { + let counts = { mcpwm: 0, ledc: 0 }; + let timers = systemInfo["availableTimers"]; + if(!timers) return counts; + + // Build a map from channel numeric value -> pwmDriver for quick lookup + let channelDriverMap = {}; + timers.forEach(timer => { + // Read the current (possibly unsaved) driver from pinoutSettings if available + let driver = timer.driverKey && pinoutSettings[timer.driverKey] !== undefined + ? parseInt(pinoutSettings[timer.driverKey]) + : timer.pwmDriver; + if(timer.channels) { + timer.channels.forEach(ch => { + channelDriverMap[ch.value] = driver; + }); + } + }); + + // All channel select elements contribute an active output if their value != -1 + const timerSelects = document.getElementsByName('timerChannels'); + timerSelects.forEach(sel => { + let val = parseInt(sel.value); + if(val > -1 && channelDriverMap[val] !== undefined) { + if(channelDriverMap[val] === ESPTimer.PwmDriver.MCPWM) counts.mcpwm++; + else counts.ledc++; + } + }); + return counts; } }; \ No newline at end of file diff --git a/ESP32/dataEdit/www/index.html b/ESP32/dataEdit/www/index.html index db14eef..d40a365 100644 --- a/ESP32/dataEdit/www/index.html +++ b/ESP32/dataEdit/www/index.html @@ -7,18 +7,9 @@ TCode ESP32 - - - - - - - - - - - - + + +