Compare commits

..

No commits in common. "main" and "sl-android/issue-617-waypoint-logger" have entirely different histories.

519 changed files with 3123 additions and 38914 deletions

View File

@ -1,162 +0,0 @@
# .gitea/workflows/ota-release.yml
# Gitea Actions — ESP32 OTA firmware build & release (bd-9kod)
#
# Triggers on signed release tags:
# esp32-balance/vX.Y.Z → builds esp32s3/balance/ (ESP32-S3 Balance board)
# esp32-io/vX.Y.Z → builds esp32s3-io/ (ESP32-S3 IO board)
#
# Uses the official espressif/idf Docker image for reproducible builds.
# Attaches <app>_<version>.bin + <app>_<version>.sha256 to the Gitea release.
# The ESP32 Balance OTA system fetches the .bin from the release asset URL.
name: OTA release — build & attach firmware
on:
push:
tags:
- "esp32-balance/v*"
- "esp32-io/v*"
permissions:
contents: write
jobs:
build-and-release:
name: Build ${{ github.ref_name }}
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.2.2
options: --user root
steps:
# ── 1. Checkout ───────────────────────────────────────────────────────────
- name: Checkout
uses: actions/checkout@v4
# ── 2. Resolve build target from tag ─────────────────────────────────────
# Tag format: esp32-balance/v1.2.3 or esp32-io/v1.2.3
- name: Resolve project from tag
id: proj
shell: bash
run: |
TAG="${GITHUB_REF_NAME}"
case "$TAG" in
esp32-balance/*)
DIR="esp32s3/balance"
APP="esp32s3_balance"
;;
esp32-io/*)
DIR="esp32s3-io"
APP="esp32s3_io"
;;
*)
echo "::error::Unrecognised tag prefix: ${TAG}"
exit 1
;;
esac
VERSION="${TAG#*/}"
echo "dir=${DIR}" >> "$GITHUB_OUTPUT"
echo "app=${APP}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "Build: ${APP} ${VERSION} from ${DIR}"
# ── 3. Build with ESP-IDF ─────────────────────────────────────────────────
- name: Build firmware (idf.py build)
shell: bash
run: |
. "${IDF_PATH}/export.sh"
cd "${{ steps.proj.outputs.dir }}"
idf.py build
# ── 4. Collect binary & generate checksum ────────────────────────────────
- name: Collect artifacts
id: art
shell: bash
run: |
APP="${{ steps.proj.outputs.app }}"
VER="${{ steps.proj.outputs.version }}"
BIN_SRC="${{ steps.proj.outputs.dir }}/build/${APP}.bin"
BIN_OUT="${APP}_${VER}.bin"
SHA_OUT="${APP}_${VER}.sha256"
cp "$BIN_SRC" "$BIN_OUT"
sha256sum "$BIN_OUT" > "$SHA_OUT"
echo "bin=${BIN_OUT}" >> "$GITHUB_OUTPUT"
echo "sha=${SHA_OUT}" >> "$GITHUB_OUTPUT"
echo "Binary: ${BIN_OUT} ($(wc -c < "$BIN_OUT") bytes)"
echo "Checksum: $(cat "$SHA_OUT")"
# ── 5. Archive artifacts in CI workspace ─────────────────────────────────
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: firmware-${{ steps.proj.outputs.app }}-${{ steps.proj.outputs.version }}
path: |
${{ steps.art.outputs.bin }}
${{ steps.art.outputs.sha }}
# ── 6. Create Gitea release (if needed) & upload assets ──────────────────
# Uses GITHUB_TOKEN (auto-provided, contents:write from permissions block).
# URL-encodes the tag to handle the slash in esp32-balance/vX.Y.Z.
- name: Publish assets to Gitea release
shell: bash
env:
GITEA_URL: https://gitea.vayrette.com
TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ steps.proj.outputs.tag }}
BIN: ${{ steps.art.outputs.bin }}
SHA: ${{ steps.art.outputs.sha }}
run: |
API="${GITEA_URL}/api/v1/repos/${REPO}"
# URL-encode the tag (slash in esp32-balance/vX.Y.Z must be escaped)
TAG_ENC=$(python3 -c "
import urllib.parse, sys
print(urllib.parse.quote(sys.argv[1], safe=''))
" "$TAG")
# Try to fetch an existing release for this tag
RELEASE=$(curl -sf \
-H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG_ENC}") || true
# If no release yet, create it
if [ -z "$RELEASE" ]; then
echo "Creating release for tag: ${TAG}"
RELEASE=$(curl -sf \
-X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json, sys
print(json.dumps({
'tag_name': sys.argv[1],
'name': sys.argv[1],
'draft': False,
'prerelease': False,
}))
" "$TAG")" \
"${API}/releases")
fi
RELEASE_ID=$(echo "$RELEASE" | python3 -c "
import sys, json; print(json.load(sys.stdin)['id'])
")
echo "Release ID: ${RELEASE_ID}"
# Upload binary and checksum
for FILE in "$BIN" "$SHA"; do
FNAME=$(basename "$FILE")
echo "Uploading: ${FNAME}"
curl -sf \
-X POST \
-H "Authorization: token ${TOKEN}" \
-F "attachment=@${FILE}" \
"${API}/releases/${RELEASE_ID}/assets?name=${FNAME}"
done
echo "Published: ${BIN} + ${SHA} → release ${TAG}"

View File

@ -7,11 +7,7 @@ The robot can now be armed and operated autonomously from the Jetson without req
### Jetson Autonomous Arming ### Jetson Autonomous Arming
- Command: `A\n` (single byte 'A' followed by newline) - Command: `A\n` (single byte 'A' followed by newline)
<<<<<<< HEAD - Sent via USB CDC to the STM32 firmware
- Sent via USB CDC to the ESP32 BALANCE firmware
=======
- Sent via USB Serial (CH343) to the ESP32-S3 firmware
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
- Robot arms after ARMING_HOLD_MS (~500ms) safety hold period - Robot arms after ARMING_HOLD_MS (~500ms) safety hold period
- Works even when RC is not connected or not armed - Works even when RC is not connected or not armed
@ -46,11 +42,7 @@ The robot can now be armed and operated autonomously from the Jetson without req
## Command Protocol ## Command Protocol
<<<<<<< HEAD ### From Jetson to STM32 (USB CDC)
### From Jetson to ESP32 BALANCE (USB CDC)
=======
### From Jetson to ESP32-S3 (USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
``` ```
A — Request arm (triggers safety hold, then motors enable) A — Request arm (triggers safety hold, then motors enable)
D — Request disarm (immediate motor stop) D — Request disarm (immediate motor stop)
@ -60,11 +52,7 @@ H — Heartbeat (refresh timeout timer, every 500ms)
C<spd>,<str> — Drive command: speed, steer (also refreshes heartbeat) C<spd>,<str> — Drive command: speed, steer (also refreshes heartbeat)
``` ```
<<<<<<< HEAD ### From STM32 to Jetson (USB CDC)
### From ESP32 BALANCE to Jetson (USB CDC)
=======
### From ESP32-S3 to Jetson (USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
Motor commands are gated by `bal.state == BALANCE_ARMED`: Motor commands are gated by `bal.state == BALANCE_ARMED`:
- When ARMED: Motor commands sent every 20ms (50 Hz) - When ARMED: Motor commands sent every 20ms (50 Hz)
- When DISARMED: Zero sent every 20ms (prevents ESC timeout) - When DISARMED: Zero sent every 20ms (prevents ESC timeout)

View File

@ -1,36 +1,17 @@
# SaltyLab Firmware — Agent Playbook # SaltyLab Firmware — Agent Playbook
## Project ## Project
<<<<<<< HEAD Self-balancing two-wheeled robot: STM32F722 flight controller, hoverboard hub motors, Jetson Nano for AI/SLAM.
**SAUL-TEE** — 4-wheel wagon (870×510×550 mm, 23 kg).
Two ESP32-S3 boards + Jetson Orin via CAN. Full spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md`
| Board | Role |
|-------|------|
| **ESP32-S3 BALANCE** | QMI8658 IMU, PID balance, CAN→VESC (L:68 / R:56), GC9A01 LCD (Waveshare Touch LCD 1.28) |
| **ESP32-S3 IO** | TBS Crossfire RC, ELRS failover, BTS7960 motors, NFC/baro/ToF, WS2812 |
| **Jetson Orin** | AI/SLAM, CANable2 USB→CAN, cmds 0x3000x303, telemetry 0x4000x401 |
> **Legacy:** `src/` and `include/` = archived STM32 HAL — do not extend. New firmware in `esp32/`.
=======
Self-balancing two-wheeled robot: ESP32-S3 ESP32-S3 BALANCE, hoverboard hub motors, Jetson Orin Nano Super for AI/SLAM.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Team ## Team
| Agent | Role | Focus | | Agent | Role | Focus |
|-------|------|-------| |-------|------|-------|
<<<<<<< HEAD | **sl-firmware** | Embedded Firmware Lead | STM32 HAL, USB CDC debugging, SPI/UART, PlatformIO, DFU bootloader |
| **sl-firmware** | Embedded Firmware Lead | ESP32-S3, ESP-IDF, QMI8658, CAN/UART protocol, BTS7960 |
| **sl-controls** | Control Systems Engineer | PID tuning, IMU fusion, balance loop, safety |
| **sl-perception** | Perception / SLAM Engineer | Jetson Orin, RealSense D435i, RPLIDAR, ROS2, Nav2 |
=======
| **sl-firmware** | Embedded Firmware Lead | ESP-IDF, USB Serial (CH343) debugging, SPI/UART, PlatformIO, DFU bootloader |
| **sl-controls** | Control Systems Engineer | PID tuning, IMU sensor fusion, real-time control loops, safety systems | | **sl-controls** | Control Systems Engineer | PID tuning, IMU sensor fusion, real-time control loops, safety systems |
| **sl-perception** | Perception / SLAM Engineer | Jetson Orin Nano Super, RealSense D435i, RPLIDAR, ROS2, Nav2 | | **sl-perception** | Perception / SLAM Engineer | Jetson Nano, RealSense D435i, RPLIDAR, ROS2, Nav2 |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Status ## Status
USB Serial (CH343) TX bug resolved (PR #10 — DCache MPU non-cacheable region + IWDG ordering fix). USB CDC TX bug resolved (PR #10 — DCache MPU non-cacheable region + IWDG ordering fix).
## Repo Structure ## Repo Structure
- `projects/saltybot/SALTYLAB.md` — Design doc - `projects/saltybot/SALTYLAB.md` — Design doc
@ -48,11 +29,11 @@ USB Serial (CH343) TX bug resolved (PR #10 — DCache MPU non-cacheable region +
| `saltyrover-dev` | Integration — rover variant | | `saltyrover-dev` | Integration — rover variant |
| `saltytank` | Stable — tracked tank variant | | `saltytank` | Stable — tracked tank variant |
| `saltytank-dev` | Integration — tank variant | | `saltytank-dev` | Integration — tank variant |
| `main` | Shared code only (IMU drivers, USB Serial (CH343), balance core, safety) | | `main` | Shared code only (IMU drivers, USB CDC, balance core, safety) |
### Rules ### Rules
- Agents branch FROM `<variant>-dev` and PR back TO `<variant>-dev` - Agents branch FROM `<variant>-dev` and PR back TO `<variant>-dev`
- Shared/infrastructure code (IMU drivers, USB Serial (CH343), balance core, safety) goes in `main` - Shared/infrastructure code (IMU drivers, USB CDC, balance core, safety) goes in `main`
- Variant-specific code (motor topology, kinematics, config) goes in variant branches - Variant-specific code (motor topology, kinematics, config) goes in variant branches
- Stable branches get promoted from `-dev` after review and hardware testing - Stable branches get promoted from `-dev` after review and hardware testing
- **Current SaltyLab team** works against `saltylab-dev` - **Current SaltyLab team** works against `saltylab-dev`

52
TEAM.md
View File

@ -1,22 +1,12 @@
# SaltyLab — Ideal Team # SaltyLab — Ideal Team
## Project ## Project
<<<<<<< HEAD Self-balancing two-wheeled robot using a drone flight controller (STM32F722), hoverboard hub motors, and eventually a Jetson Nano for AI/SLAM.
**SAUL-TEE** — 4-wheel wagon (870×510×550 mm, 23 kg).
Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFERENCE.md`.
## Current Status
- **Hardware:** ESP32-S3 BALANCE (Waveshare Touch LCD 1.28, CH343 USB) + ESP32-S3 IO (bare devkit, JTAG USB)
- **Firmware:** ESP-IDF/PlatformIO target; legacy `src/` STM32 HAL archived
- **Comms:** UART 460800 baud inter-board; CANable2 USB→CAN for Orin; CAN 500 kbps to VESCs (L:68 / R:56)
=======
Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hoverboard hub motors, and eventually a Jetson Orin Nano Super for AI/SLAM.
## Current Status ## Current Status
- **Hardware:** Assembled — FC, motors, ESC, IMU, battery, RC all on hand - **Hardware:** Assembled — FC, motors, ESC, IMU, battery, RC all on hand
- **Firmware:** Balance PID + hoverboard ESC protocol written, but blocked by USB Serial (CH343) bug - **Firmware:** Balance PID + hoverboard ESC protocol written, but blocked by USB CDC bug
- **Blocker:** USB Serial (CH343) TX stops working when peripheral inits (SPI/UART/GPIO) are added alongside USB on ESP32-S3 — see `legacy/stm32/USB_CDC_BUG.md` for historical context - **Blocker:** USB CDC TX stops working when peripheral inits (SPI/UART/GPIO) are added alongside USB OTG FS — see `USB_CDC_BUG.md`
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
--- ---
@ -24,30 +14,18 @@ Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hove
### 1. Embedded Firmware Engineer (Lead) ### 1. Embedded Firmware Engineer (Lead)
**Must-have:** **Must-have:**
<<<<<<< HEAD - Deep STM32 HAL experience (F7 series specifically)
- Deep ESP32 (Arduino/ESP-IDF) or STM32 HAL experience
- USB OTG FS / CDC ACM debugging (TxState, endpoint management, DMA conflicts) - USB OTG FS / CDC ACM debugging (TxState, endpoint management, DMA conflicts)
- SPI + UART + USB coexistence on ESP32 - SPI + UART + USB coexistence on STM32
- PlatformIO or bare-metal ESP32 toolchain - PlatformIO or bare-metal STM32 toolchain
- DFU bootloader implementation - DFU bootloader implementation
=======
- Deep ESP-IDF experience (ESP32-S3 specifically)
- USB Serial (CH343) / UART debugging on ESP32-S3
- SPI + UART + USB coexistence on ESP32-S3
- ESP-IDF / Arduino-ESP32 toolchain
- OTA firmware update implementation
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
**Nice-to-have:** **Nice-to-have:**
- ESP32-S3 peripheral coexistence (SPI + UART + USB) - Betaflight/iNav/ArduPilot codebase familiarity
- PID control loop tuning for balance robots - PID control loop tuning for balance robots
- FOC motor control (hoverboard ESC protocol) - FOC motor control (hoverboard ESC protocol)
<<<<<<< HEAD **Why:** The immediate blocker is a USB peripheral conflict. Need someone who's debugged STM32 USB issues before — this is not a software logic bug, it's a hardware peripheral interaction issue.
**Why:** The immediate blocker is a USB peripheral conflict. Need someone who's debugged STM32 USB issues before — ESP32 firmware for the balance loop and I/O needs to be written from scratch.
=======
**Why:** The immediate blocker is a USB peripheral conflict on ESP32-S3. Need someone who's debugged ESP32-S3 USB Serial (CH343) issues before — this is not a software logic bug, it's a hardware peripheral interaction issue.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 2. Control Systems / Robotics Engineer ### 2. Control Systems / Robotics Engineer
**Must-have:** **Must-have:**
@ -65,7 +43,7 @@ Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hove
### 3. Perception / SLAM Engineer (Phase 2) ### 3. Perception / SLAM Engineer (Phase 2)
**Must-have:** **Must-have:**
- Jetson Orin Nano Super / NVIDIA Jetson platform - Jetson Nano / NVIDIA Jetson platform
- Intel RealSense D435i depth camera - Intel RealSense D435i depth camera
- RPLIDAR integration - RPLIDAR integration
- SLAM (ORB-SLAM3, RTAB-Map, or similar) - SLAM (ORB-SLAM3, RTAB-Map, or similar)
@ -76,23 +54,19 @@ Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hove
- Obstacle avoidance - Obstacle avoidance
- Nav2 stack - Nav2 stack
**Why:** Phase 2 goal is autonomous navigation. Jetson Orin Nano Super with RealSense + RPLIDAR for indoor mapping and person following. **Why:** Phase 2 goal is autonomous navigation. Jetson Nano with RealSense + RPLIDAR for indoor mapping and person following.
--- ---
## Hardware Reference ## Hardware Reference
| Component | Details | | Component | Details |
|-----------|---------| |-----------|---------|
<<<<<<< HEAD | FC | MAMBA F722S (STM32F722RET6, MPU6000) |
| FC | ESP32 BALANCE (ESP32RET6, MPU6000) |
=======
| FC | ESP32-S3 BALANCE (ESP32-S3RET6, QMI8658) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Motors | 2x 8" pneumatic hoverboard hub motors | | Motors | 2x 8" pneumatic hoverboard hub motors |
| ESC | Hoverboard ESC (EFeru FOC firmware) | | ESC | Hoverboard ESC (EFeru FOC firmware) |
| Battery | 36V pack | | Battery | 36V pack |
| RC | BetaFPV ELRS 2.4GHz TX + RX | | RC | BetaFPV ELRS 2.4GHz TX + RX |
| AI Brain | Jetson Orin Nano Super + Noctua fan | | AI Brain | Jetson Nano + Noctua fan |
| Depth | Intel RealSense D435i | | Depth | Intel RealSense D435i |
| LIDAR | RPLIDAR A1M8 | | LIDAR | RPLIDAR A1M8 |
| Spare IMUs | BNO055, MPU6050 | | Spare IMUs | BNO055, MPU6050 |
@ -100,4 +74,4 @@ Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hove
## Repo ## Repo
- Gitea: https://gitea.vayrette.com/seb/saltylab-firmware - Gitea: https://gitea.vayrette.com/seb/saltylab-firmware
- Design doc: `projects/saltybot/SALTYLAB.md` - Design doc: `projects/saltybot/SALTYLAB.md`
- Bug doc: `legacy/stm32/USB_CDC_BUG.md` (archived — STM32 era) - Bug doc: `USB_CDC_BUG.md`

View File

@ -127,7 +127,7 @@ loop — USB would never enumerate cleanly.
| LED2 | PC15 | GPIO | | LED2 | PC15 | GPIO |
| Buzzer | PB2 | GPIO/TIM4_CH3 | | Buzzer | PB2 | GPIO/TIM4_CH3 |
MCU: ESP32RET6 (ESP32 BALANCE FC, Betaflight target DIAT-MAMBAF722_2022B) MCU: STM32F722RET6 (MAMBA F722S FC, Betaflight target DIAT-MAMBAF722_2022B)
--- ---

View File

@ -1,46 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdk 34
namespace 'com.saltylab.uwbtag'
defaultConfig {
applicationId "com.saltylab.uwbtag"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
}
}
buildFeatures {
viewBinding true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'com.google.code.gson:gson:2.10.1'
}

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- BLE permissions (API 31+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- Legacy BLE (API < 31) -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<application
android:allowBackup="true"
android:label="UWB Tag Config"
android:theme="@style/Theme.MaterialComponents.DayNight.DarkActionBar">
<activity
android:name=".UwbTagBleActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,444 +0,0 @@
package com.saltylab.uwbtag
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputEditText
import com.google.gson.Gson
import com.saltylab.uwbtag.databinding.ActivityUwbTagBleBinding
import java.util.UUID
// ---------------------------------------------------------------------------
// GATT service / characteristic UUIDs
// ---------------------------------------------------------------------------
private val SERVICE_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef0")
private val CHAR_CONFIG_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef1") // read/write JSON config
private val CHAR_STATUS_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef2") // notify: tag status string
private val CHAR_BATT_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef3") // notify: battery %
private val CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
// BLE scan timeout
private const val SCAN_TIMEOUT_MS = 15_000L
// Permissions request code
private const val REQ_PERMISSIONS = 1001
// ---------------------------------------------------------------------------
// Data model
// ---------------------------------------------------------------------------
data class TagConfig(
val tag_name: String = "UWB_TAG_0001",
val sleep_timeout_s: Int = 300,
val display_brightness: Int = 50,
val uwb_channel: Int = 9,
val ranging_interval_ms: Int = 100,
val battery_report: Boolean = true
)
data class ScannedDevice(
val name: String,
val address: String,
var rssi: Int,
val device: BluetoothDevice
)
// ---------------------------------------------------------------------------
// RecyclerView adapter for scanned devices
// ---------------------------------------------------------------------------
class DeviceAdapter(
private val onConnect: (ScannedDevice) -> Unit
) : RecyclerView.Adapter<DeviceAdapter.VH>() {
private val items = mutableListOf<ScannedDevice>()
fun update(device: ScannedDevice) {
val idx = items.indexOfFirst { it.address == device.address }
if (idx >= 0) {
items[idx] = device
notifyItemChanged(idx)
} else {
items.add(device)
notifyItemInserted(items.size - 1)
}
}
fun clear() {
items.clear()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_ble_device, parent, false)
return VH(view)
}
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(items[position])
override fun getItemCount() = items.size
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
private val tvName = view.findViewById<TextView>(R.id.tvDeviceName)
private val tvAddress = view.findViewById<TextView>(R.id.tvDeviceAddress)
private val tvRssi = view.findViewById<TextView>(R.id.tvRssi)
private val btnConn = view.findViewById<Button>(R.id.btnConnect)
fun bind(item: ScannedDevice) {
tvName.text = item.name
tvAddress.text = item.address
tvRssi.text = "${item.rssi} dBm"
btnConn.setOnClickListener { onConnect(item) }
}
}
}
// ---------------------------------------------------------------------------
// Activity
// ---------------------------------------------------------------------------
@SuppressLint("MissingPermission") // permissions checked at runtime before any BLE call
class UwbTagBleActivity : AppCompatActivity() {
private lateinit var binding: ActivityUwbTagBleBinding
private val gson = Gson()
private val mainHandler = Handler(Looper.getMainLooper())
// BLE
private val btManager by lazy { getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager }
private val btAdapter by lazy { btManager.adapter }
private var bleScanner: BluetoothLeScanner? = null
private var gatt: BluetoothGatt? = null
private var configChar: BluetoothGattCharacteristic? = null
private var statusChar: BluetoothGattCharacteristic? = null
private var battChar: BluetoothGattCharacteristic? = null
private var isScanning = false
private val deviceAdapter = DeviceAdapter(onConnect = ::connectToDevice)
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUwbTagBleBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
binding.rvDevices.layoutManager = LinearLayoutManager(this)
binding.rvDevices.adapter = deviceAdapter
binding.btnScan.setOnClickListener {
if (isScanning) stopScan() else startScanIfPermitted()
}
binding.btnDisconnect.setOnClickListener { disconnectGatt() }
binding.btnReadConfig.setOnClickListener { readConfig() }
binding.btnWriteConfig.setOnClickListener { writeConfig() }
requestBlePermissions()
}
override fun onDestroy() {
super.onDestroy()
stopScan()
disconnectGatt()
}
// ---------------------------------------------------------------------------
// Permissions
// ---------------------------------------------------------------------------
private fun requestBlePermissions() {
val needed = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!hasPermission(Manifest.permission.BLUETOOTH_SCAN))
needed += Manifest.permission.BLUETOOTH_SCAN
if (!hasPermission(Manifest.permission.BLUETOOTH_CONNECT))
needed += Manifest.permission.BLUETOOTH_CONNECT
} else {
if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION))
needed += Manifest.permission.ACCESS_FINE_LOCATION
}
if (needed.isNotEmpty()) {
ActivityCompat.requestPermissions(this, needed.toTypedArray(), REQ_PERMISSIONS)
}
}
private fun hasPermission(perm: String) =
ContextCompat.checkSelfPermission(this, perm) == PackageManager.PERMISSION_GRANTED
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQ_PERMISSIONS &&
grantResults.any { it != PackageManager.PERMISSION_GRANTED }) {
toast("BLE permissions required")
}
}
// ---------------------------------------------------------------------------
// BLE Scan
// ---------------------------------------------------------------------------
private fun startScanIfPermitted() {
if (btAdapter?.isEnabled != true) { toast("Bluetooth is off"); return }
bleScanner = btAdapter.bluetoothLeScanner
val filter = ScanFilter.Builder()
.setDeviceNamePattern("UWB_TAG_.*".toRegex().toPattern())
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
deviceAdapter.clear()
bleScanner?.startScan(listOf(filter), settings, scanCallback)
isScanning = true
binding.btnScan.text = "Stop"
binding.tvScanStatus.text = "Scanning…"
mainHandler.postDelayed({ stopScan() }, SCAN_TIMEOUT_MS)
}
private fun stopScan() {
bleScanner?.stopScan(scanCallback)
isScanning = false
binding.btnScan.text = "Scan"
binding.tvScanStatus.text = "Scan stopped"
}
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val name = result.device.name ?: return
if (!name.startsWith("UWB_TAG_")) return
val dev = ScannedDevice(
name = name,
address = result.device.address,
rssi = result.rssi,
device = result.device
)
mainHandler.post { deviceAdapter.update(dev) }
}
override fun onScanFailed(errorCode: Int) {
mainHandler.post {
binding.tvScanStatus.text = "Scan failed (code $errorCode)"
isScanning = false
binding.btnScan.text = "Scan"
}
}
}
// ---------------------------------------------------------------------------
// GATT Connection
// ---------------------------------------------------------------------------
private fun connectToDevice(scanned: ScannedDevice) {
stopScan()
binding.tvScanStatus.text = "Connecting to ${scanned.name}"
gatt = scanned.device.connectGatt(this, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
}
private fun disconnectGatt() {
gatt?.disconnect()
gatt?.close()
gatt = null
configChar = null
statusChar = null
battChar = null
mainHandler.post {
binding.cardConfig.visibility = View.GONE
binding.tvScanStatus.text = "Disconnected"
}
}
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
mainHandler.post { binding.tvScanStatus.text = "Connected — discovering services…" }
g.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
mainHandler.post {
binding.cardConfig.visibility = View.GONE
binding.tvScanStatus.text = "Disconnected"
toast("Tag disconnected")
}
gatt?.close()
gatt = null
}
}
}
override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
mainHandler.post { toast("Service discovery failed") }
return
}
val service = g.getService(SERVICE_UUID)
if (service == null) {
mainHandler.post { toast("UWB config service not found on tag") }
return
}
configChar = service.getCharacteristic(CHAR_CONFIG_UUID)
statusChar = service.getCharacteristic(CHAR_STATUS_UUID)
battChar = service.getCharacteristic(CHAR_BATT_UUID)
// Subscribe to status notifications
statusChar?.let { enableNotifications(g, it) }
battChar?.let { enableNotifications(g, it) }
// Initial config read
configChar?.let { g.readCharacteristic(it) }
mainHandler.post {
val devName = g.device.name ?: g.device.address
binding.tvConnectedName.text = "Connected: $devName"
binding.cardConfig.visibility = View.VISIBLE
binding.tvScanStatus.text = "Connected to $devName"
}
}
override fun onCharacteristicRead(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status != BluetoothGatt.GATT_SUCCESS) return
if (characteristic.uuid == CHAR_CONFIG_UUID) {
val json = characteristic.value?.toString(Charsets.UTF_8) ?: return
val cfg = runCatching { gson.fromJson(json, TagConfig::class.java) }.getOrNull() ?: return
mainHandler.post { populateFields(cfg) }
}
}
// API 33+ callback
override fun onCharacteristicRead(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
if (status != BluetoothGatt.GATT_SUCCESS) return
if (characteristic.uuid == CHAR_CONFIG_UUID) {
val json = value.toString(Charsets.UTF_8)
val cfg = runCatching { gson.fromJson(json, TagConfig::class.java) }.getOrNull() ?: return
mainHandler.post { populateFields(cfg) }
}
}
override fun onCharacteristicWrite(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
val msg = if (status == BluetoothGatt.GATT_SUCCESS) "Config written" else "Write failed ($status)"
mainHandler.post { toast(msg) }
}
override fun onCharacteristicChanged(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val value = characteristic.value ?: return
handleNotification(characteristic.uuid, value)
}
// API 33+ callback
override fun onCharacteristicChanged(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
handleNotification(characteristic.uuid, value)
}
}
// ---------------------------------------------------------------------------
// Notification helpers
// ---------------------------------------------------------------------------
private fun enableNotifications(g: BluetoothGatt, char: BluetoothGattCharacteristic) {
g.setCharacteristicNotification(char, true)
val descriptor = char.getDescriptor(CCCD_UUID) ?: return
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
g.writeDescriptor(descriptor)
}
private fun handleNotification(uuid: UUID, value: ByteArray) {
val text = value.toString(Charsets.UTF_8)
mainHandler.post {
when (uuid) {
CHAR_STATUS_UUID -> binding.tvTagStatus.text = "Status: $text"
CHAR_BATT_UUID -> {
val pct = text.toIntOrNull() ?: return@post
binding.tvTagStatus.text = binding.tvTagStatus.text.toString()
.replace(Regex("\\| Batt:.*"), "")
.trimEnd() + " | Batt: $pct%"
}
}
}
}
// ---------------------------------------------------------------------------
// Config read / write
// ---------------------------------------------------------------------------
private fun readConfig() {
val g = gatt ?: run { toast("Not connected"); return }
val c = configChar ?: run { toast("Config char not found"); return }
g.readCharacteristic(c)
}
private fun writeConfig() {
val g = gatt ?: run { toast("Not connected"); return }
val c = configChar ?: run { toast("Config char not found"); return }
val cfg = buildConfigFromFields()
val json = gson.toJson(cfg)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
g.writeCharacteristic(c, json.toByteArray(Charsets.UTF_8),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
} else {
@Suppress("DEPRECATION")
c.value = json.toByteArray(Charsets.UTF_8)
@Suppress("DEPRECATION")
g.writeCharacteristic(c)
}
}
// ---------------------------------------------------------------------------
// UI helpers
// ---------------------------------------------------------------------------
private fun populateFields(cfg: TagConfig) {
binding.etTagName.setText(cfg.tag_name)
binding.etSleepTimeout.setText(cfg.sleep_timeout_s.toString())
binding.etBrightness.setText(cfg.display_brightness.toString())
binding.etUwbChannel.setText(cfg.uwb_channel.toString())
binding.etRangingInterval.setText(cfg.ranging_interval_ms.toString())
binding.switchBatteryReport.isChecked = cfg.battery_report
}
private fun buildConfigFromFields() = TagConfig(
tag_name = binding.etTagName.text?.toString() ?: "UWB_TAG_0001",
sleep_timeout_s = binding.etSleepTimeout.text?.toString()?.toIntOrNull() ?: 300,
display_brightness = binding.etBrightness.text?.toString()?.toIntOrNull() ?: 50,
uwb_channel = binding.etUwbChannel.text?.toString()?.toIntOrNull() ?: 9,
ranging_interval_ms = binding.etRangingInterval.text?.toString()?.toIntOrNull() ?: 100,
battery_report = binding.switchBatteryReport.isChecked
)
private fun toast(msg: String) =
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}

View File

@ -1,238 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:title="UWB Tag BLE Config" />
<!-- Scan controls -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<Button
android:id="@+id/btnScan"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Scan" />
<TextView
android:id="@+id/tvScanStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:text="Tap Scan to find UWB tags"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" />
</LinearLayout>
<!-- Scan results list -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:text="Nearby Tags"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvDevices"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clipToPadding="false" />
<!-- Connected device config panel -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardConfig"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:visibility="gone"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvConnectedName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Connected: —"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textStyle="bold" />
<Button
android:id="@+id/btnDisconnect"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Disconnect" />
</LinearLayout>
<!-- tag_name -->
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Tag Name">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etTagName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<!-- sleep_timeout_s and uwb_channel (row) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:hint="Sleep Timeout (s)">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSleepTimeout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:hint="UWB Channel">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etUwbChannel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- display_brightness and ranging_interval_ms (row) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:hint="Brightness (0-100)">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etBrightness"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:hint="Ranging Interval (ms)">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etRangingInterval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- battery_report toggle -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchBatteryReport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Battery Reporting" />
<!-- Action buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<Button
android:id="@+id/btnReadConfig"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="Read" />
<Button
android:id="@+id/btnWriteConfig"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="Write" />
</LinearLayout>
<!-- Status notifications from tag -->
<TextView
android:id="@+id/tvTagStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="#1A000000"
android:fontFamily="monospace"
android:padding="8dp"
android:text="Tag status: —"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardElevation="2dp"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvDeviceName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="UWB_TAG_XXXX"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDeviceAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="XX:XX:XX:XX:XX:XX"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" />
</LinearLayout>
<TextView
android:id="@+id/tvRssi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-70 dBm"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?attr/colorSecondary" />
<Button
android:id="@+id/btnConnect"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Connect" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -60,7 +60,7 @@ color("Purple", 0.9)
translate([0, 0, h_fc]) translate([0, 0, h_fc])
cube([36, 36, 5], center=true); cube([36, 36, 5], center=true);
// Jetson Orin Nano Super // Jetson Nano
color("LimeGreen", 0.7) color("LimeGreen", 0.7)
translate([0, 0, h_jetson]) translate([0, 0, h_jetson])
cube([100, 80, 29], center=true); cube([100, 80, 29], center=true);

View File

@ -20,7 +20,7 @@ fc_hole_dia = 3.2; // M3 clearance
fc_board_size = 36; // Typical FC PCB fc_board_size = 36; // Typical FC PCB
fc_standoff_h = 5; // Rubber standoff height fc_standoff_h = 5; // Rubber standoff height
// --- Jetson Orin Nano Super --- // --- Jetson Nano ---
jetson_w = 100; jetson_w = 100;
jetson_d = 80; jetson_d = 80;
jetson_h = 29; // With heatsink jetson_h = 29; // With heatsink

View File

@ -1,7 +1,7 @@
// ============================================ // ============================================
// SaltyLab Jetson Orin Nano Super Shelf // SaltyLab Jetson Nano Shelf
// 120×100×15mm PETG // 120×100×15mm PETG
// Mounts Jetson Orin Nano Super to 2020 extrusion // Mounts Jetson Nano to 2020 extrusion
// ============================================ // ============================================
include <dimensions.scad> include <dimensions.scad>

View File

@ -56,24 +56,15 @@
3. Fasten 4× M4×12 SHCS. Torque 2.5 N·m. 3. Fasten 4× M4×12 SHCS. Torque 2.5 N·m.
4. Insert battery pack; route Velcro straps through slots and cinch. 4. Insert battery pack; route Velcro straps through slots and cinch.
<<<<<<< HEAD ### 7 FC mount (MAMBA F722S)
### 7 MCU mount (ESP32 BALANCE + ESP32 IO)
> ⚠️ **ARCHITECTURE CHANGE (2026-04-03):** ESP32 BALANCE retired. Two ESP32 boards replace it.
> Board dimensions and hole patterns TBD — await spec from max before machining mount plate.
=======
### 7 FC mount (ESP32-S3 BALANCE)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
1. Place silicone anti-vibration grommets onto nylon M3 standoffs. 1. Place silicone anti-vibration grommets onto nylon M3 standoffs.
2. Lower ESP32 BALANCE board onto standoffs; secure with M3×6 BHCS. Snug only. 2. Lower FC onto standoffs; secure with M3×6 BHCS. Snug only — do not over-torque.
3. Mount ESP32 IO board adjacent — exact placement TBD pending board dimensions. 3. Orient USB-C port toward front of robot for cable access.
4. Orient USB connectors toward front of robot for cable access.
### 8 Jetson Orin Nano Super mount plate ### 8 Jetson Nano mount plate
1. Press or thread M3 nylon standoffs (8mm) into plate holes. 1. Press or thread M3 nylon standoffs (8mm) into plate holes.
2. Bolt plate to deck: 4× M3×10 SHCS at deck corners. 2. Bolt plate to deck: 4× M3×10 SHCS at deck corners.
3. Set Jetson Orin Nano Super B01 carrier onto plate standoffs; fasten M3×6 BHCS. 3. Set Jetson Nano B01 carrier onto plate standoffs; fasten M3×6 BHCS.
### 9 Bumper brackets ### 9 Bumper brackets
1. Slide 22mm EMT conduit through saddle clamp openings. 1. Slide 22mm EMT conduit through saddle clamp openings.
@ -95,8 +86,7 @@
| Wheelbase (axle C/L to C/L) | 600 mm | ±1 mm | | Wheelbase (axle C/L to C/L) | 600 mm | ±1 mm |
| Motor fork slot width | 24 mm | +0.5 / 0 | | Motor fork slot width | 24 mm | +0.5 / 0 |
| Motor fork dropout depth | 60 mm | ±0.5 mm | | Motor fork dropout depth | 60 mm | ±0.5 mm |
| ESP32 BALANCE hole pattern | TBD — await spec from max | ±0.2 mm | | FC hole pattern | 30.5 × 30.5 mm | ±0.2 mm |
| ESP32 IO hole pattern | TBD — await spec from max | ±0.2 mm |
| Jetson hole pattern | 58 × 58 mm | ±0.2 mm | | Jetson hole pattern | 58 × 58 mm | ±0.2 mm |
| Battery tray inner | 185 × 72 × 52 mm | +2 / 0 mm | | Battery tray inner | 185 × 72 × 52 mm | +2 / 0 mm |

View File

@ -41,11 +41,7 @@ PR #7 (`chassis_frame.scad`) used placeholder values. The table below records th
| 3 | Dropout clamp — upper | 2 | 8mm 6061-T6 Al | 90×70mm blank | D-cut bore; `RENDER="clamp_upper_2d"` | | 3 | Dropout clamp — upper | 2 | 8mm 6061-T6 Al | 90×70mm blank | D-cut bore; `RENDER="clamp_upper_2d"` |
| 4 | Stem flange ring | 2 | 6mm Al or acrylic | Ø82mm disc | One above + one below plate; `RENDER="stem_flange_2d"` | | 4 | Stem flange ring | 2 | 6mm Al or acrylic | Ø82mm disc | One above + one below plate; `RENDER="stem_flange_2d"` |
| 5 | Vertical stem tube | 1 | 38.1mm OD × 1.5mm wall 6061-T6 Al | 1050mm length | 1.5" EMT conduit is a drop-in alternative | | 5 | Vertical stem tube | 1 | 38.1mm OD × 1.5mm wall 6061-T6 Al | 1050mm length | 1.5" EMT conduit is a drop-in alternative |
<<<<<<< HEAD | 6 | FC standoff M3×6mm nylon | 4 | Nylon | — | MAMBA F722S vibration isolation |
| 6 | MCU standoff M3×6mm nylon | 4 | Nylon | — | ESP32 BALANCE / IO board isolation (dimensions TBD) |
=======
| 6 | FC standoff M3×6mm nylon | 4 | Nylon | — | ESP32-S3 BALANCE vibration isolation |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| 7 | Ø4mm × 16mm alignment pin | 8 | Steel dowel | — | Dropout clamp-to-plate alignment | | 7 | Ø4mm × 16mm alignment pin | 8 | Steel dowel | — | Dropout clamp-to-plate alignment |
### Battery Stem Clamp (`stem_battery_clamp.scad`) — Part B ### Battery Stem Clamp (`stem_battery_clamp.scad`) — Part B
@ -74,7 +70,7 @@ PR #7 (`chassis_frame.scad`) used placeholder values. The table below records th
| 10 | Motor fork bracket (R) | 1 | 8mm 6061 aluminium | Mirror of item 9 | | 10 | Motor fork bracket (R) | 1 | 8mm 6061 aluminium | Mirror of item 9 |
| 11 | Battery tray | 1 | 3mm PETG FDM or 3mm aluminium fold | `chassis_frame.scad``battery_tray()` module | | 11 | Battery tray | 1 | 3mm PETG FDM or 3mm aluminium fold | `chassis_frame.scad``battery_tray()` module |
| 12 | FC mount plate / standoffs | 1 set | PETG or nylon FDM | Includes 4× M3 nylon standoffs, 6mm height | | 12 | FC mount plate / standoffs | 1 set | PETG or nylon FDM | Includes 4× M3 nylon standoffs, 6mm height |
| 13 | Jetson Orin Nano Super mount plate | 1 | 4mm 5052 aluminium or 4mm PETG FDM | B01 58×58mm hole pattern | | 13 | Jetson Nano mount plate | 1 | 4mm 5052 aluminium or 4mm PETG FDM | B01 58×58mm hole pattern |
| 14 | Front bumper bracket | 1 | 5mm PETG FDM | Saddle clamps for 22mm EMT conduit | | 14 | Front bumper bracket | 1 | 5mm PETG FDM | Saddle clamps for 22mm EMT conduit |
| 15 | Rear bumper bracket | 1 | 5mm PETG FDM | Mirror of item 14 | | 15 | Rear bumper bracket | 1 | 5mm PETG FDM | Mirror of item 14 |
@ -92,23 +88,12 @@ PR #7 (`chassis_frame.scad`) used placeholder values. The table below records th
## Electronics Mounts ## Electronics Mounts
> ⚠️ **ARCHITECTURE CHANGE (2026-04-03):** ESP32 BALANCE (ESP32) is retired.
> Replaced by **ESP32 BALANCE** + **ESP32 IO**. Board dimensions and hole patterns TBD — await spec from max.
| # | Part | Qty | Spec | Notes | | # | Part | Qty | Spec | Notes |
|---|------|-----|------|-------| |---|------|-----|------|-------|
<<<<<<< HEAD | 13 | STM32 MAMBA F722S FC | 1 | 36×36mm PCB, 30.5×30.5mm M3 mount | Oriented USB-C port toward front |
| 13 | ESP32 BALANCE board | 1 | TBD — mount pattern TBD | PID balance loop; replaces ESP32 BALANCE |
| 13b | ESP32 IO board | 1 | TBD — mount pattern TBD | Motor/sensor/comms I/O |
| 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | ESP32 board isolation |
| 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under ESP32 mount pads |
| 16 | Jetson Orin module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern |
=======
| 13 | ESP32-S3 ESP32-S3 BALANCE FC | 1 | 36×36mm PCB, 30.5×30.5mm M3 mount | Oriented USB-C port toward front |
| 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | FC vibration isolation | | 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | FC vibration isolation |
| 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under FC mount pads | | 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under FC mount pads |
| 16 | Jetson Orin Nano Super B01 module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern | | 16 | Jetson Nano B01 module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| 17 | Nylon M3 standoff 8mm | 4 | F/F nylon | Jetson board standoffs | | 17 | Nylon M3 standoff 8mm | 4 | F/F nylon | Jetson board standoffs |
--- ---
@ -159,8 +144,8 @@ Slide entire carousel up/down the stem with M6 collar bolts loosened. Tighten at
| 26 | M6×60 SHCS | 4 | ISO 4762, SS | Collar clamping bolts | | 26 | M6×60 SHCS | 4 | ISO 4762, SS | Collar clamping bolts |
| 27 | M6 hex nut | 4 | ISO 4032, SS | Captured in collar pockets | | 27 | M6 hex nut | 4 | ISO 4032, SS | Captured in collar pockets |
| 28 | M6×12 set screw | 2 | ISO 4026, SS cup-point | Stem height lock (1 per collar half) | | 28 | M6×12 set screw | 2 | ISO 4026, SS cup-point | Stem height lock (1 per collar half) |
| 29 | M3×10 SHCS | 12 | ISO 4762, SS | ESP32 mount + miscellaneous | | 29 | M3×10 SHCS | 12 | ISO 4762, SS | FC mount + miscellaneous |
| 30 | M3×6 BHCS | 4 | ISO 4762, SS | ESP32 board bolts (qty TBD pending board spec) | | 30 | M3×6 BHCS | 4 | ISO 4762, SS | FC board bolts |
| 31 | Axle lock nut (match axle tip thread) | 4 | Flanged, confirm thread | 2 per motor | | 31 | Axle lock nut (match axle tip thread) | 4 | Flanged, confirm thread | 2 per motor |
| 32 | Flat washer M5 | 32 | SS | | | 32 | Flat washer M5 | 32 | SS | |
| 33 | Flat washer M4 | 32 | SS | | | 33 | Flat washer M4 | 32 | SS | |

View File

@ -1,410 +0,0 @@
// ============================================================
// Cable Management Tray Issue #628
// Agent : sl-mechanical
// Date : 2026-03-15
// Part catalogue:
// 1. tray_body under-plate tray with snap-in cable channels, Velcro
// tie-down slots every 40 mm, pass-through holes, label slots
// 2. tnut_bracket 2020 T-nut rail mount bracket (×2, slide under tray)
// 3. channel_clip snap-in divider clip separating power / signal / servo zones
// 4. cover_panel hinged snap-on lid (living-hinge PETG flexure strip)
// 5. cable_saddle individual cable saddle / strain-relief clip (×n)
//
// BOM:
// 4 × M5×10 BHCS + M5 T-nuts (tnut_bracket × 2 to rail)
// 4 × M3×8 SHCS (tnut_bracket to tray body)
// n × 100 mm Velcro tie-down strips (through 6×2 mm slots, every 40 mm)
//
// Cable channel layout (X axis, inside tray):
// Zone A Power (2S6S LiPo, XT60/XT30): 20 mm wide, 14 mm deep
// Zone B Signal (JST-SH, PWM, I2C, UART): 14 mm wide, 10 mm deep
// Zone C Servo (JST-PH, thick servo leads): 14 mm wide, 12 mm deep
// Divider walls: 2.5 mm thick between zones
//
// Print settings (PETG):
// tray_body / tnut_bracket / channel_clip : 5 perimeters, 40 % gyroid, no supports
// cover_panel : 3 perimeters, 20 % gyroid, no supports
// (living-hinge print flat, thin strip flexes)
// cable_saddle : 3 perimeters, 30 % gyroid, no supports
//
// Export commands:
// openscad -D 'RENDER="tray_body"' -o tray_body.stl cable_tray.scad
// openscad -D 'RENDER="tnut_bracket"' -o tnut_bracket.stl cable_tray.scad
// openscad -D 'RENDER="channel_clip"' -o channel_clip.stl cable_tray.scad
// openscad -D 'RENDER="cover_panel"' -o cover_panel.stl cable_tray.scad
// openscad -D 'RENDER="cable_saddle"' -o cable_saddle.stl cable_tray.scad
// openscad -D 'RENDER="assembly"' -o assembly.png cable_tray.scad
// ============================================================
RENDER = "assembly"; // tray_body | tnut_bracket | channel_clip | cover_panel | cable_saddle | assembly
$fn = 48;
EPS = 0.01;
// 2020 rail constants
RAIL_W = 20.0;
TNUT_W = 9.8;
TNUT_H = 5.5;
TNUT_L = 12.0;
SLOT_NECK_H = 3.2;
M5_D = 5.2;
M5_HEAD_D = 9.5;
M5_HEAD_H = 4.0;
// Tray geometry
TRAY_L = 280.0; // length along rail (7 × 40 mm tie-down pitch)
TRAY_W = 60.0; // width across rail (covers standard 40 mm rail pair)
TRAY_WALL = 2.5; // side / floor wall thickness
TRAY_DEPTH = 18.0; // interior depth (tallest zone + wall)
// Cable channel zones (widths must sum to TRAY_W - 2*TRAY_WALL - 2*DIV_T)
DIV_T = 2.5; // divider wall thickness
ZONE_A_W = 20.0; // Power
ZONE_A_D = 14.0;
ZONE_B_W = 14.0; // Signal
ZONE_B_D = 10.0;
ZONE_C_W = 14.0; // Servo
ZONE_C_D = 12.0;
// Total inner width used: ZONE_A_W + ZONE_B_W + ZONE_C_W + 2*DIV_T = 55 mm < TRAY_W - 2*TRAY_WALL = 55 mm
// Tie-down slots (Velcro strips)
TIEDOWN_PITCH = 40.0;
TIEDOWN_W = 6.0; // slot width (fits 6 mm wide Velcro)
TIEDOWN_T = 2.2; // slot through-thickness (floor)
TIEDOWN_CNT = 7; // 7 positions along tray
// Pass-through holes in floor
PASSTHRU_D = 12.0; // circular grommet-compatible pass-through
PASSTHRU_CNT = 3; // one per zone, at tray mid-length
// Label slots (rear outer wall)
LABEL_W = 24.0;
LABEL_H = 8.0;
LABEL_T = 1.0; // depth from outer face
// Snap ledge for cover
SNAP_LEDGE_H = 2.5;
SNAP_LEDGE_D = 1.5;
// T-nut bracket
BKT_L = 60.0;
BKT_W = TRAY_W;
BKT_T = 6.0;
BOLT_PITCH = 40.0;
M3_D = 3.2;
M3_HEAD_D = 6.0;
M3_HEAD_H = 3.0;
M3_NUT_W = 5.5;
M3_NUT_H = 2.4;
// Cover panel
CVR_T = 1.8; // panel thickness
HINGE_T = 0.6; // living-hinge strip thickness (printed in PETG)
HINGE_W = 3.0; // hinge strip width (flexes easily)
SNAP_HOOK_H = 3.5; // snap hook height
SNAP_HOOK_T = 2.2;
// Cable saddle
SAD_W = 12.0;
SAD_H = 8.0;
SAD_T = 2.5;
SAD_BORE_D = 7.0; // cable bundle bore
SAD_CLIP_T = 1.6; // snap arm thickness
// Utilities
module chamfer_cube(size, ch=1.0) {
hull() {
translate([ch, ch, 0]) cube([size[0]-2*ch, size[1]-2*ch, EPS]);
translate([0, 0, ch]) cube(size - [0, 0, ch]);
}
}
module hex_pocket(af, depth) {
cylinder(d=af/cos(30), h=depth, $fn=6);
}
// Part 1: tray_body
module tray_body() {
difference() {
// Outer shell
union() {
chamfer_cube([TRAY_L, TRAY_W, TRAY_DEPTH + TRAY_WALL], ch=1.5);
// Snap ledge along top of both long walls (for cover_panel)
for (y = [-SNAP_LEDGE_D, TRAY_W])
translate([0, y, TRAY_DEPTH])
cube([TRAY_L, TRAY_WALL + SNAP_LEDGE_D, SNAP_LEDGE_H]);
}
// Interior cavity
translate([TRAY_WALL, TRAY_WALL, TRAY_WALL])
cube([TRAY_L - 2*TRAY_WALL, TRAY_W - 2*TRAY_WALL,
TRAY_DEPTH + EPS]);
// Zone dividers (subtract from solid to leave walls)
// Zone A (Power) inner floor cut full depth A
translate([TRAY_WALL, TRAY_WALL, TRAY_WALL + (TRAY_DEPTH - ZONE_A_D)])
cube([TRAY_L - 2*TRAY_WALL, ZONE_A_W, ZONE_A_D + EPS]);
// Zone B (Signal) inner floor cut
translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T,
TRAY_WALL + (TRAY_DEPTH - ZONE_B_D)])
cube([TRAY_L - 2*TRAY_WALL, ZONE_B_W, ZONE_B_D + EPS]);
// Zone C (Servo) inner floor cut
translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T,
TRAY_WALL + (TRAY_DEPTH - ZONE_C_D)])
cube([TRAY_L - 2*TRAY_WALL, ZONE_C_W, ZONE_C_D + EPS]);
// Velcro tie-down slots (floor, every 40 mm)
for (i = [0:TIEDOWN_CNT-1]) {
x = TRAY_WALL + 20 + i * TIEDOWN_PITCH - TIEDOWN_W/2;
// Zone A slot
translate([x, TRAY_WALL + 2, -EPS])
cube([TIEDOWN_W, ZONE_A_W - 4, TRAY_WALL + 2*EPS]);
// Zone B slot
translate([x, TRAY_WALL + ZONE_A_W + DIV_T + 2, -EPS])
cube([TIEDOWN_W, ZONE_B_W - 4, TRAY_WALL + 2*EPS]);
// Zone C slot
translate([x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + 2, -EPS])
cube([TIEDOWN_W, ZONE_C_W - 4, TRAY_WALL + 2*EPS]);
}
// Pass-through holes in floor (centre of each zone at mid-length)
mid_x = TRAY_L / 2;
// Zone A
translate([mid_x, TRAY_WALL + ZONE_A_W/2, -EPS])
cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS);
// Zone B
translate([mid_x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2, -EPS])
cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS);
// Zone C
translate([mid_x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2, -EPS])
cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS);
// Label slots on front wall (y = 0) one per zone
zone_ctrs = [TRAY_WALL + ZONE_A_W/2,
TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2,
TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2];
label_z = TRAY_WALL + 2;
for (yc = zone_ctrs)
translate([TRAY_L/2 - LABEL_W/2, -EPS, label_z])
cube([LABEL_W, LABEL_T + EPS, LABEL_H]);
// M3 bracket bolt holes in floor (4 corners)
for (x = [20, TRAY_L - 20])
for (y = [TRAY_W/4, 3*TRAY_W/4])
translate([x, y, -EPS])
cylinder(d=M3_D, h=TRAY_WALL + 2*EPS);
// Channel clip snap sockets (top of each divider, every 80 mm)
for (i = [0:2]) {
cx = 40 + i * 80;
for (dy = [ZONE_A_W, ZONE_A_W + DIV_T + ZONE_B_W])
translate([cx - 3, TRAY_WALL + dy - 1, TRAY_DEPTH - 4])
cube([6, DIV_T + 2, 4 + EPS]);
}
}
// Divider walls (positive geometry)
// Wall between Zone A and Zone B
translate([TRAY_WALL, TRAY_WALL + ZONE_A_W, TRAY_WALL])
cube([TRAY_L - 2*TRAY_WALL, DIV_T,
TRAY_DEPTH - ZONE_A_D]); // partial height lower in A zone
// Wall between Zone B and Zone C
translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W, TRAY_WALL])
cube([TRAY_L - 2*TRAY_WALL, DIV_T,
TRAY_DEPTH - ZONE_B_D]);
}
// Part 2: tnut_bracket
module tnut_bracket() {
difference() {
chamfer_cube([BKT_L, BKT_W, BKT_T], ch=1.5);
// M5 T-nut holes (2 per bracket, on rail centreline)
for (x = [BKT_L/2 - BOLT_PITCH/2, BKT_L/2 + BOLT_PITCH/2]) {
translate([x, BKT_W/2, -EPS]) {
cylinder(d=M5_D, h=BKT_T + 2*EPS);
cylinder(d=M5_HEAD_D, h=M5_HEAD_H + EPS);
}
translate([x - TNUT_L/2, BKT_W/2 - TNUT_W/2, BKT_T - TNUT_H])
cube([TNUT_L, TNUT_W, TNUT_H + EPS]);
}
// M3 tray-attachment holes (4 corners)
for (x = [10, BKT_L - 10])
for (y = [10, BKT_W - 10]) {
translate([x, y, -EPS])
cylinder(d=M3_D, h=BKT_T + 2*EPS);
// M3 hex nut captured pocket (from top)
translate([x, y, BKT_T - M3_NUT_H - 0.2])
hex_pocket(M3_NUT_W + 0.3, M3_NUT_H + 0.3);
}
// Weight relief
translate([15, 8, -EPS])
cube([BKT_L - 30, BKT_W - 16, BKT_T/2]);
}
}
// Part 3: channel_clip
// Snap-in clip that locks into divider-wall snap sockets;
// holds individual bundles in their zone and acts as colour-coded zone marker.
module channel_clip() {
clip_body_w = 6.0;
clip_body_h = DIV_T + 4.0;
clip_body_t = 8.0;
tab_h = 3.5;
tab_w = 2.5;
difference() {
union() {
// Body spanning divider
cube([clip_body_t, clip_body_w, clip_body_h]);
// Snap tabs (bottom, straddle divider)
for (s = [0, clip_body_w - tab_w])
translate([clip_body_t/2 - 1, s, -tab_h])
cube([2, tab_w, tab_h + 1]);
}
// Cable radius slot on each face
translate([-EPS, clip_body_w/2, clip_body_h * 0.6])
rotate([0, 90, 0])
cylinder(d=5.0, h=clip_body_t + 2*EPS);
// Snap tab undercut for flex
for (s = [0, clip_body_w - tab_w])
translate([clip_body_t/2 - 2, s - EPS, -tab_h + 1.5])
cube([4, tab_w + 2*EPS, 1.5]);
}
}
// Part 4: cover_panel
// Flat snap-on lid with living-hinge along one long edge.
// Print flat; PETG living hinge flexes ~90° to snap onto tray.
module cover_panel() {
total_w = TRAY_W + 2 * SNAP_HOOK_T;
difference() {
union() {
// Main panel
cube([TRAY_L, TRAY_W, CVR_T]);
// Living hinge strip along back edge (thin, flexes)
translate([0, TRAY_W - EPS, 0])
cube([TRAY_L, HINGE_W, HINGE_T]);
// Snap hooks along front edge (clips under tray snap ledge)
for (x = [20, TRAY_L/2 - 20, TRAY_L/2 + 20, TRAY_L - 20])
translate([x - SNAP_HOOK_T/2, -SNAP_HOOK_H + EPS, 0])
difference() {
cube([SNAP_HOOK_T, SNAP_HOOK_H, CVR_T + 1.5]);
// Hook nose chamfer
translate([-EPS, -EPS, CVR_T])
rotate([0, 0, 0])
cube([SNAP_HOOK_T + 2*EPS, 1.5, 1.5]);
}
}
// Ventilation slots (3 rows × 6 slots)
for (row = [0:2])
for (col = [0:5]) {
sx = 20 + col * 40 + row * 10;
sy = 10 + row * 12;
if (sx + 25 < TRAY_L && sy + 6 < TRAY_W)
translate([sx, sy, -EPS])
cube([25, 6, CVR_T + 2*EPS]);
}
// Zone label windows (align with tray label slots)
zone_ctrs = [TRAY_WALL + ZONE_A_W/2,
TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2,
TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2];
for (yc = zone_ctrs)
translate([TRAY_L/2 - LABEL_W/2, yc - LABEL_H/2, -EPS])
cube([LABEL_W, LABEL_H, CVR_T + 2*EPS]);
}
}
// Part 5: cable_saddle
// Snap-in cable saddle / strain-relief clip; press-fits onto tray top edge.
module cable_saddle() {
arm_gap = TRAY_WALL + 0.4; // fits over tray wall
arm_len = 8.0;
difference() {
union() {
// Body
chamfer_cube([SAD_W, SAD_T * 2 + arm_gap, SAD_H], ch=1.0);
// Cable retaining arch
translate([SAD_W/2, SAD_T + arm_gap/2, SAD_H])
scale([1, 0.6, 1])
difference() {
cylinder(d=SAD_BORE_D + SAD_CLIP_T * 2, h=SAD_T);
translate([0, 0, -EPS])
cylinder(d=SAD_BORE_D, h=SAD_T + 2*EPS);
translate([-SAD_BORE_D, 0, -EPS])
cube([SAD_BORE_D * 2, SAD_BORE_D, SAD_T + 2*EPS]);
}
}
// Slot for tray wall (negative)
translate([0, SAD_T, -EPS])
cube([SAD_W, arm_gap, arm_len + EPS]);
// M3 tie-down hole
translate([SAD_W/2, SAD_T + arm_gap/2, -EPS])
cylinder(d=M3_D, h=SAD_H + 2*EPS);
}
}
// Assembly
module assembly() {
// Tray body (open face up for visibility)
color("SteelBlue")
tray_body();
// Two T-nut brackets underneath at 1/4 and 3/4 length
for (bx = [TRAY_L/4 - BKT_L/2, 3*TRAY_L/4 - BKT_L/2])
color("DodgerBlue")
translate([bx, 0, -BKT_T])
tnut_bracket();
// Channel clips (3 per divider position, every 80 mm)
for (i = [0:2]) {
cx = 40 + i * 80;
// Divider A/B
color("Tomato", 0.8)
translate([cx - 4, TRAY_WALL + ZONE_A_W - 2, TRAY_DEPTH - 3])
channel_clip();
// Divider B/C
color("Orange", 0.8)
translate([cx - 4,
TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W - 2,
TRAY_DEPTH - 3])
channel_clip();
}
// Cover panel (raised above tray to show interior)
color("LightSteelBlue", 0.5)
translate([0, 0, TRAY_DEPTH + SNAP_LEDGE_H + 4])
cover_panel();
// Cable saddles along front tray edge
for (x = [40, 120, 200])
color("SlateGray")
translate([x - SAD_W/2, -SAD_T * 2 - TRAY_WALL, 0])
cable_saddle();
}
// Dispatch
if (RENDER == "tray_body") tray_body();
else if (RENDER == "tnut_bracket") tnut_bracket();
else if (RENDER == "channel_clip") channel_clip();
else if (RENDER == "cover_panel") cover_panel();
else if (RENDER == "cable_saddle") cable_saddle();
else assembly();

View File

@ -1,265 +0,0 @@
// ============================================================
// canable_mount.scad CANable 2.0 USB-CAN Adapter Cradle
// Issue #654 / sl-mechanical 2026-03-16
// ============================================================
// Snap-fit cradle for CANable 2.0 PCB (~60 × 18 × 10 mm).
// Attaches to 2020 aluminium T-slot rail via 2× M5 T-nuts.
//
// Port access:
// USB-C port X end wall cutout (connector protrudes through)
// CAN terminal X+ end wall cutout (CANH / CANL / GND wire exit)
// LED status window slot in Y+ side wall, PCB top-face LEDs visible
//
// Retention: snap-fit cantilever lips on both side walls (PETG flex).
// Cable strain relief: zip-tie boss pair on X+ shelf (CAN wires).
//
// VERIFY WITH CALIPERS BEFORE PRINTING:
// PCB_L, PCB_W board outline
// USBC_W, USBC_H USB-C shell at X edge
// TERM_W, TERM_H 3-pos terminal block at X+ edge
// LED_X_CTR, LED_WIN_W LED window position on Y+ wall
//
// Print settings (PETG):
// 3 perimeters, 40 % gyroid infill, no supports, 0.2 mm layer
// Print orientation: open face UP (as modelled)
//
// BOM:
// 2 × M5×10 BHCS + 2 × M5 slide-in T-nut (2020 rail)
//
// Export commands:
// openscad -D 'RENDER="mount"' -o canable_mount.stl canable_mount.scad
// openscad -D 'RENDER="assembly"' -o canable_assembly.png canable_mount.scad
// ============================================================
RENDER = "assembly"; // mount | assembly
$fn = 48;
EPS = 0.01;
// Verify before printing
// CANable 2.0 PCB
PCB_L = 60.0; // board length (X: USB-C end terminal end)
PCB_W = 18.0; // board width (Y)
PCB_T = 1.6; // board thickness
COMP_H = 8.5; // tallest component above board (USB-C shell 3.5 mm;
// terminal block 8.5 mm)
// USB-C connector (at X end face of PCB)
USBC_W = 9.5; // connector outer width
USBC_H = 3.8; // connector outer height above board surface
USBC_Z0 = 0.0; // connector bottom offset above board surface
// CAN screw-terminal block (at X+ end face, 3-pos 5.0 mm pitch)
TERM_W = 16.0; // terminal block span (3 × 5 mm + housing)
TERM_H = 9.0; // terminal block height above board surface
TERM_Z0 = 0.5; // terminal bottom offset above board surface
// Status LED window (LEDs near USB-C end on PCB top face)
// Rectangular slot cut in Y+ side wall LEDs visible from the side
LED_X_CTR = 11.0; // LED zone centre measured from PCB X edge
LED_WIN_W = 14.0; // window width (X)
LED_WIN_H = 5.5; // window height (Z) opens top portion of side wall
// Cradle geometry
WALL_T = 2.5; // side/end wall thickness
FLOOR_T = 4.0; // floor plate thickness (accommodates M5 BHCS head pocket)
CL_SIDE = 0.30; // Y clearance per side (total 0.6 mm play)
CL_END = 0.40; // X clearance per end
// Interior cavity
INN_W = PCB_W + 2*CL_SIDE; // Y span
INN_L = PCB_L + 2*CL_END; // X span
INN_H = PCB_T + COMP_H + 1.2; // Z height (board + tallest comp + margin)
// Outer body
OTR_W = INN_W + 2*WALL_T; // Y
OTR_L = INN_L + 2*WALL_T; // X
OTR_H = FLOOR_T + INN_H; // Z
// PCB reference origin within body (lower-left corner of board)
PCB_X0 = WALL_T + CL_END; // board X start inside body
PCB_Y0 = WALL_T + CL_SIDE; // board Y start inside body
PCB_Z0 = FLOOR_T; // board bottom sits on floor
// Snap-fit lips
// Cantilever ledge on inner face of each side wall, at PCB-top Z.
// Tapered (chamfered) entry guides PCB in from above.
SNAP_IN = 0.8; // how far inward ledge protrudes over PCB edge
SNAP_T = 1.2; // snap-arm thickness (thin for PETG flex)
SNAP_H = 4.0; // cantilever arm height (root at OTR_H, tip near PCB_Z0+PCB_T)
SNAP_L = 18.0; // arm length along X (centred on PCB, shorter = more flex)
// Snap on Y wall protrudes in +Y direction; Y+ wall protrudes in Y direction
// M5 T-nut mount (2020 rail)
M5_D = 5.3; // M5 bolt clearance bore
M5_HEAD_D = 9.5; // M5 BHCS head pocket diameter (from bottom face)
M5_HEAD_H = 3.0; // BHCS head pocket depth
M5_SPAC = 20.0; // bolt spacing along X (centred on cradle)
// Standard M5 slide-in T-nuts used no T-nut pocket moulded in.
// Cable strain relief
// Two zip-tie anchor bosses on a shelf inside the X+ end, straddling
// the CAN terminal wires.
SR_BOSS_OD = 7.0; // boss outer diameter
SR_BOSS_H = 5.5; // boss height above floor
SR_SLOT_W = 3.5; // zip-tie slot width
SR_SLOT_T = 2.2; // zip-tie slot through-height
// Boss Y positions (straddle terminal block)
SR_Y1 = WALL_T + INN_W * 0.25;
SR_Y2 = WALL_T + INN_W * 0.75;
SR_X = OTR_L - WALL_T - SR_BOSS_OD/2 - 2.5; // just inside X+ end wall
//
module canable_mount() {
difference() {
// Outer solid body
union() {
cube([OTR_L, OTR_W, OTR_H]);
// Snap cantilever arms on Y wall (protrude inward +Y)
// Arms hang down from top of Y wall inner face.
// Root is flush with inner face (Y = WALL_T); tip at PCB level.
translate([OTR_L/2 - SNAP_L/2, WALL_T - SNAP_T, OTR_H - SNAP_H])
cube([SNAP_L, SNAP_T, SNAP_H]);
// Snap cantilever arms on Y+ wall (protrude inward Y)
translate([OTR_L/2 - SNAP_L/2, OTR_W - WALL_T, OTR_H - SNAP_H])
cube([SNAP_L, SNAP_T, SNAP_H]);
// Cable strain relief bosses (X+ end, inside)
for (sy = [SR_Y1, SR_Y2])
translate([SR_X, sy, 0])
cylinder(d=SR_BOSS_OD, h=SR_BOSS_H);
}
// Interior cavity
translate([WALL_T, WALL_T, FLOOR_T])
cube([INN_L, INN_W, INN_H + EPS]);
// USB-C cutout X end wall
// Centred on PCB width; opened from board surface upward
translate([-EPS,
PCB_Y0 + PCB_W/2 - (USBC_W + 1.5)/2,
PCB_Z0 + USBC_Z0 - 0.5])
cube([WALL_T + 2*EPS, USBC_W + 1.5, USBC_H + 2.5]);
// CAN terminal cutout X+ end wall
// Full terminal width + 2 mm margin for screwdriver access;
// height clears terminal block + wire bend radius
translate([OTR_L - WALL_T - EPS,
PCB_Y0 + PCB_W/2 - (TERM_W + 2.0)/2,
PCB_Z0 + TERM_Z0 - 0.5])
cube([WALL_T + 2*EPS, TERM_W + 2.0, TERM_H + 5.0]);
// LED status window Y+ side wall
// Rectangular slot; LEDs at top-face of PCB are visible through it
translate([PCB_X0 + LED_X_CTR - LED_WIN_W/2,
OTR_W - WALL_T - EPS,
OTR_H - LED_WIN_H])
cube([LED_WIN_W, WALL_T + 2*EPS, LED_WIN_H + EPS]);
// M5 BHCS head pockets (from bottom face of floor)
for (mx = [OTR_L/2 - M5_SPAC/2, OTR_L/2 + M5_SPAC/2])
translate([mx, OTR_W/2, -EPS]) {
// Clearance bore through full floor
cylinder(d=M5_D, h=FLOOR_T + 2*EPS);
// BHCS head pocket from bottom face
cylinder(d=M5_HEAD_D, h=M5_HEAD_H + EPS);
}
// Snap-arm ledge slot Y arm (hollow out to thin arm)
// Arm is SNAP_T thick; cut away material behind arm
translate([OTR_L/2 - SNAP_L/2 - EPS, EPS, OTR_H - SNAP_H])
cube([SNAP_L + 2*EPS, WALL_T - SNAP_T - EPS, SNAP_H + EPS]);
// Snap-arm ledge slot Y+ arm
translate([OTR_L/2 - SNAP_L/2 - EPS, OTR_W - WALL_T + SNAP_T, OTR_H - SNAP_H])
cube([SNAP_L + 2*EPS, WALL_T - SNAP_T - EPS, SNAP_H + EPS]);
// Snap-arm inward ledge notch (entry chamfer removed)
// Chamfer top of snap arm so PCB slides in easily
// Y arm: chamfer on upper-inner edge 45° wedge on +Y/+Z corner
translate([OTR_L/2 - SNAP_L/2 - EPS,
WALL_T - SNAP_T - EPS,
OTR_H - SNAP_IN])
rotate([0, 0, 0])
rotate([45, 0, 0])
cube([SNAP_L + 2*EPS, SNAP_IN * 1.5, SNAP_IN * 1.5]);
// Y+ arm: chamfer on upper-inner edge
translate([OTR_L/2 - SNAP_L/2 - EPS,
OTR_W - WALL_T + SNAP_T - SNAP_IN * 1.5 + EPS,
OTR_H - SNAP_IN])
rotate([45, 0, 0])
cube([SNAP_L + 2*EPS, SNAP_IN * 1.5, SNAP_IN * 1.5]);
// Snap ledge cutout on Y arm inner tip
// Creates inward nub: remove top portion of arm inner tip
// leaving bottom SNAP_IN height as the retaining ledge
translate([OTR_L/2 - SNAP_L/2 - EPS,
WALL_T - SNAP_T - EPS,
PCB_Z0 + PCB_T + SNAP_IN])
cube([SNAP_L + 2*EPS, SNAP_T + 2*EPS,
OTR_H - (PCB_Z0 + PCB_T + SNAP_IN) + EPS]);
// Snap ledge cutout on Y+ arm inner tip
translate([OTR_L/2 - SNAP_L/2 - EPS,
OTR_W - WALL_T - EPS,
PCB_Z0 + PCB_T + SNAP_IN])
cube([SNAP_L + 2*EPS, SNAP_T + 2*EPS,
OTR_H - (PCB_Z0 + PCB_T + SNAP_IN) + EPS]);
// Zip-tie slots through strain relief bosses
for (sy = [SR_Y1, SR_Y2])
translate([SR_X, sy,
SR_BOSS_H/2 - SR_SLOT_T/2])
rotate([0, 90, 0])
cube([SR_SLOT_T, SR_SLOT_W,
SR_BOSS_OD + 2*EPS],
center=true);
// Weight relief pocket in floor (underside)
translate([WALL_T + 8, WALL_T + 3, -EPS])
cube([OTR_L - 2*WALL_T - 16, OTR_W - 2*WALL_T - 6,
FLOOR_T - 1.5 + EPS]);
}
}
// Assembly preview
if (RENDER == "assembly") {
color("DimGray", 0.93) canable_mount();
// Phantom PCB
color("MidnightBlue", 0.35)
translate([PCB_X0, PCB_Y0, PCB_Z0])
cube([PCB_L, PCB_W, PCB_T]);
// Phantom component block (top of PCB)
color("DarkSlateGray", 0.25)
translate([PCB_X0, PCB_Y0, PCB_Z0 + PCB_T])
cube([PCB_L, PCB_W, COMP_H]);
// USB-C port highlight
color("Gold", 0.8)
translate([-1,
PCB_Y0 + PCB_W/2 - USBC_W/2,
PCB_Z0 + USBC_Z0])
cube([WALL_T + 2, USBC_W, USBC_H]);
// Terminal block highlight
color("Tomato", 0.7)
translate([OTR_L - WALL_T - 1,
PCB_Y0 + PCB_W/2 - TERM_W/2,
PCB_Z0 + TERM_Z0])
cube([WALL_T + 2, TERM_W, TERM_H]);
// LED zone highlight
color("LimeGreen", 0.9)
translate([PCB_X0 + LED_X_CTR - LED_WIN_W/2,
OTR_W - WALL_T - 0.5,
OTR_H - LED_WIN_H + 1])
cube([LED_WIN_W, 1, LED_WIN_H - 2]);
} else {
canable_mount();
}

View File

@ -8,9 +8,9 @@
// Requirements: // Requirements:
// - 600mm wheelbase // - 600mm wheelbase
// - 2x hoverboard hub motors (170mm OD) // - 2x hoverboard hub motors (170mm OD)
// - ESP32-S3 ESP32-S3 BALANCE FC mount (30.5x30.5mm pattern) // - STM32 MAMBA F722S FC mount (30.5x30.5mm pattern)
// - Battery tray (24V 4Ah ~180x70x50mm pack) // - Battery tray (24V 4Ah ~180x70x50mm pack)
// - Jetson Orin Nano Super B01 mount plate (100x80mm, M3 holes) // - Jetson Nano B01 mount plate (100x80mm, M3 holes)
// - Front/rear bumper brackets // - Front/rear bumper brackets
// ============================================================================= // =============================================================================
@ -37,7 +37,7 @@ MOTOR_FORK_H = 80; // mm, total height of motor fork bracket
MOTOR_FORK_T = 8; // mm, fork plate thickness MOTOR_FORK_T = 8; // mm, fork plate thickness
AXLE_HEIGHT = 310; // mm, axle CL above ground (motor radius + clearance) AXLE_HEIGHT = 310; // mm, axle CL above ground (motor radius + clearance)
// FC mount (ESP32-S3 BALANCE 30.5 × 30.5 mm M3 pattern) // FC mount (MAMBA F722S 30.5 × 30.5 mm M3 pattern)
FC_MOUNT_SPACING = 30.5; // mm, hole pattern pitch FC_MOUNT_SPACING = 30.5; // mm, hole pattern pitch
FC_MOUNT_HOLE_D = 3.2; // mm, M3 clearance FC_MOUNT_HOLE_D = 3.2; // mm, M3 clearance
FC_STANDOFF_H = 6; // mm, standoff height FC_STANDOFF_H = 6; // mm, standoff height
@ -52,7 +52,7 @@ BATT_FLOOR = 4; // mm, tray floor thickness
BATT_STRAP_W = 20; // mm, Velcro strap slot width BATT_STRAP_W = 20; // mm, Velcro strap slot width
BATT_STRAP_T = 2; // mm, strap slot depth BATT_STRAP_T = 2; // mm, strap slot depth
// Jetson Orin Nano Super B01 mount plate // Jetson Nano B01 mount plate
// B01 carrier board hole pattern: 58 x 58 mm M3 (inner) + corner pass-throughs // B01 carrier board hole pattern: 58 x 58 mm M3 (inner) + corner pass-throughs
JETSON_HOLE_PITCH = 58; // mm, M3 mounting hole pattern JETSON_HOLE_PITCH = 58; // mm, M3 mounting hole pattern
JETSON_HOLE_D = 3.2; // mm JETSON_HOLE_D = 3.2; // mm
@ -210,7 +210,7 @@ module battery_tray() {
// FC mount holes helper // FC mount holes helper
module fc_mount_holes(z_offset=0, depth=10) { module fc_mount_holes(z_offset=0, depth=10) {
// ESP32-S3 BALANCE: 30.5×30.5 mm M3 pattern, centred at origin // MAMBA F722S: 30.5×30.5 mm M3 pattern, centred at origin
for (x = [-FC_MOUNT_SPACING/2, FC_MOUNT_SPACING/2]) for (x = [-FC_MOUNT_SPACING/2, FC_MOUNT_SPACING/2])
for (y = [-FC_MOUNT_SPACING/2, FC_MOUNT_SPACING/2]) for (y = [-FC_MOUNT_SPACING/2, FC_MOUNT_SPACING/2])
translate([x, y, z_offset]) translate([x, y, z_offset])
@ -247,7 +247,7 @@ module fc_mount_plate() {
} }
} }
// Jetson Orin Nano Super B01 mount plate // Jetson Nano B01 mount plate
// Positioned rear of deck, elevated on standoffs // Positioned rear of deck, elevated on standoffs
module jetson_mount_plate() { module jetson_mount_plate() {
jet_x = 60; // offset toward rear jet_x = 60; // offset toward rear

View File

@ -104,11 +104,7 @@ IP54-rated enclosures and sensor housings for all-weather outdoor robot operatio
| Component | Thermal strategy | Max junction | Enclosure budget | | Component | Thermal strategy | Max junction | Enclosure budget |
|-----------|-----------------|-------------|-----------------| |-----------|-----------------|-------------|-----------------|
| Jetson Orin NX | Al pad → lid → fan forced convection | 95 °C Tj | Target ≤ 60 °C case | | Jetson Orin NX | Al pad → lid → fan forced convection | 95 °C Tj | Target ≤ 60 °C case |
<<<<<<< HEAD | FC (MAMBA F722S) | Passive; FC has own EMI shield | 85 °C | <60 °C ambient OK |
| FC (ESP32 BALANCE) | Passive; FC has own EMI shield | 85 °C | <60 °C ambient OK |
=======
| FC (ESP32-S3 BALANCE) | Passive; FC has own EMI shield | 85 °C | <60 °C ambient OK |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| ESC × 2 | Al pad → lid | 100 °C Tj | Target ≤ 60 °C | | ESC × 2 | Al pad → lid | 100 °C Tj | Target ≤ 60 °C |
| D435i | Passive; housing vent gap on rear cap | 45 °C surface | — | | D435i | Passive; housing vent gap on rear cap | 45 °C surface | — |

View File

@ -1,386 +0,0 @@
// ============================================================
// Jetson Orin Nano Carrier Board Mount Issue #612
// Agent : sl-mechanical
// Date : 2026-03-15
// Part catalogue:
// 1. tnut_base 2020 T-slot rail interface plate, M5 T-nut captive pockets
// 2. standoff_post M2.5 captive-nut standoff post (×4), 10 mm airflow gap
// 3. side_brace lateral stiffening brace with port-access cutouts (×2)
// 4. duct_shroud optional top heatsink duct / fan-exhaust channel
// 5. cable_clip snap-on cable management clip for brace edge
//
// BOM:
// 4 × M5×10 BHCS + M5 T-nuts (tnut_base to rail, 2 per rail)
// 4 × M2.5×20 SHCS (board to standoff posts)
// 4 × M2.5 hex nuts (captured in standoff posts)
// 4 × M3×8 SHCS + washers (side_brace to tnut_base)
// 2 × M3×16 SHCS (duct_shroud to side_brace tops)
//
// Jetson Orin Nano carrier board (Seeed reComputer / official dev kit):
// Board dims : 100 × 80 mm
// Mounting hole pattern : 86 × 58 mm (centre-to-centre), M2.5, Ø3.5 pad
// PCB thickness: 1.6 mm
// Connector side: -Y (USB-A, USB-C, HDMI, DP, GbE, SD on one long edge)
// Fan header & PWM header: +X short edge
// M.2 / NVMe: bottom face
//
// Print settings (PETG):
// tnut_base / standoff_post / side_brace / duct_shroud : 5 perimeters, 40 % gyroid, no supports
// cable_clip : 3 perimeters, 30 % gyroid, no supports
//
// Export commands:
// openscad -D 'RENDER="tnut_base"' -o tnut_base.stl jetson_orin_mount.scad
// openscad -D 'RENDER="standoff_post"' -o standoff_post.stl jetson_orin_mount.scad
// openscad -D 'RENDER="side_brace"' -o side_brace.stl jetson_orin_mount.scad
// openscad -D 'RENDER="duct_shroud"' -o duct_shroud.stl jetson_orin_mount.scad
// openscad -D 'RENDER="cable_clip"' -o cable_clip.stl jetson_orin_mount.scad
// openscad -D 'RENDER="assembly"' -o assembly.png jetson_orin_mount.scad
// ============================================================
// Render selector
RENDER = "assembly"; // tnut_base | standoff_post | side_brace | duct_shroud | cable_clip | assembly
// Global constants
$fn = 64;
EPS = 0.01;
// 2020 rail
RAIL_W = 20.0;
SLOT_NECK_H = 3.2;
TNUT_W = 9.8;
TNUT_H = 5.5;
TNUT_L = 12.0;
M5_D = 5.2;
M5_HEAD_D = 9.5;
M5_HEAD_H = 4.0;
// Jetson Orin Nano carrier board
BOARD_L = 100.0; // board X
BOARD_W = 80.0; // board Y
BOARD_T = 1.6; // PCB thickness
MH_SX = 86.0; // mounting hole span X (centre-to-centre)
MH_SY = 58.0; // mounting hole span Y
M25_D = 2.7; // M2.5 clearance bore
M25_NUT_W = 5.0; // M2.5 hex nut across-flats
M25_NUT_H = 2.0; // M2.5 hex nut height
M25_HEAD_D = 5.0; // M2.5 SHCS head diameter
M25_HEAD_H = 2.5;
// Base plate
BASE_L = 120.0; // length along X (covers board + overhang for braces)
BASE_W = 50.0; // width along Y (rail mount footprint)
BASE_T = 6.0; // plate thickness
BOLT_PITCH = 40.0; // M5 rail bolt pitch (per rail, 2 rails at Y=0 & Y=BASE_W)
M3_D = 3.2;
M3_HEAD_D = 6.0;
M3_HEAD_H = 3.0;
// Standoff posts
POST_H = 12.0; // airflow gap + PCB seating (>= 10 mm clearance spec)
POST_OD = 8.0; // outer diameter
POST_BASE_D = 11.0; // flange diameter
POST_BASE_H = 3.0; // flange height
NUT_TRAP_H = M25_NUT_H + 0.3;
NUT_TRAP_W = M25_NUT_W + 0.4;
// Side braces
BRACE_T = 5.0; // brace thickness (X)
BRACE_H = POST_H + POST_BASE_H + BOARD_T + 4.0; // full height
BRACE_W = BASE_W; // same width as base
// Port-access cutouts (connector side -Y)
USB_CUT_W = 60.0; // wide cutout for USB-A stack + HDMI + DP
USB_CUT_H = 22.0;
GBE_CUT_W = 20.0; // GbE jack
GBE_CUT_H = 18.0;
// Duct shroud
DUCT_T = 3.0; // wall thickness
DUCT_FLANGE = 6.0; // side tab width for M3 attachment
FAN_W = 40.0; // standard 40 mm blower clearance cutout
FAN_H = 10.0; // duct outlet height
// Cable clip
CLIP_OD = 12.0;
CLIP_ID = 7.0;
CLIP_GAP = 7.5;
CLIP_W = 10.0;
SNAP_T = 1.8;
// Utilities
module chamfer_cube(size, ch=1.0) {
hull() {
translate([ch, ch, 0]) cube([size[0]-2*ch, size[1]-2*ch, EPS]);
translate([0, 0, ch]) cube(size - [0, 0, ch]);
}
}
module hex_pocket(af, depth) {
cylinder(d=af/cos(30), h=depth, $fn=6);
}
// Part 1: tnut_base
module tnut_base() {
difference() {
union() {
chamfer_cube([BASE_L, BASE_W, BASE_T], ch=1.5);
// Raised mounting bosses for M3 brace attachment (4 corners)
for (x = [8, BASE_L-8])
for (y = [8, BASE_W-8])
translate([x, y, BASE_T])
cylinder(d=10, h=2.5);
}
// T-nut pockets and M5 bolts front rail (y = BASE_W/4)
for (x = [BASE_L/2 - BOLT_PITCH/2, BASE_L/2 + BOLT_PITCH/2]) {
translate([x, BASE_W/4, -EPS]) {
cylinder(d=M5_D, h=BASE_T + 2*EPS);
cylinder(d=M5_HEAD_D, h=M5_HEAD_H + EPS);
}
translate([x - TNUT_L/2, BASE_W/4 - TNUT_W/2, BASE_T - TNUT_H])
cube([TNUT_L, TNUT_W, TNUT_H + EPS]);
}
// T-nut pockets and M5 bolts rear rail (y = 3*BASE_W/4)
for (x = [BASE_L/2 - BOLT_PITCH/2, BASE_L/2 + BOLT_PITCH/2]) {
translate([x, 3*BASE_W/4, -EPS]) {
cylinder(d=M5_D, h=BASE_T + 2*EPS);
cylinder(d=M5_HEAD_D, h=M5_HEAD_H + EPS);
}
translate([x - TNUT_L/2, 3*BASE_W/4 - TNUT_W/2, BASE_T - TNUT_H])
cube([TNUT_L, TNUT_W, TNUT_H + EPS]);
}
// M3 boss bolt holes (corner braces)
for (x = [8, BASE_L-8])
for (y = [8, BASE_W-8])
translate([x, y, -EPS])
cylinder(d=M3_D, h=BASE_T + 2.5 + 2*EPS);
// M3 boss counterbores (head from bottom)
for (x = [8, BASE_L-8])
for (y = [8, BASE_W-8])
translate([x, y, -EPS])
cylinder(d=M3_HEAD_D, h=M3_HEAD_H + EPS);
// Standoff post seating holes (board hole pattern, centred on plate)
bx0 = BASE_L/2 - MH_SX/2;
by0 = BASE_W/2 - MH_SY/2;
for (dx = [0, MH_SX])
for (dy = [0, MH_SY])
translate([bx0+dx, by0+dy, -EPS])
cylinder(d=POST_BASE_D + 0.4, h=BASE_T + 2*EPS);
// Weight relief grid (2 pockets)
translate([20, 12, -EPS]) cube([30, BASE_W-24, BASE_T/2]);
translate([BASE_L-50, 12, -EPS]) cube([30, BASE_W-24, BASE_T/2]);
// Cable pass-through slot
translate([BASE_L/2 - 8, BASE_W/2 - 3, -EPS])
cube([16, 6, BASE_T + 2*EPS]);
}
}
// Part 2: standoff_post
module standoff_post() {
difference() {
union() {
// Flange
cylinder(d=POST_BASE_D, h=POST_BASE_H);
// Post body
translate([0, 0, POST_BASE_H])
cylinder(d=POST_OD, h=POST_H);
}
// M2.5 through bore
translate([0, 0, -EPS])
cylinder(d=M25_D, h=POST_BASE_H + POST_H + 2*EPS);
// Captured hex nut trap (from top)
translate([0, 0, POST_BASE_H + POST_H - NUT_TRAP_H])
hex_pocket(NUT_TRAP_W, NUT_TRAP_H + EPS);
// Anti-rotation flat on nut pocket
translate([-M25_NUT_W/2 - 0.2, -POST_OD/2 - EPS,
POST_BASE_H + POST_H - NUT_TRAP_H])
cube([M25_NUT_W + 0.4, 2.0, NUT_TRAP_H + EPS]);
}
}
// Part 3: side_brace
// Printed as +X face. Mirror for -X side.
module side_brace() {
difference() {
union() {
chamfer_cube([BRACE_T, BRACE_W, BRACE_H], ch=1.0);
// Top lip to retain board edge
translate([0, 0, BRACE_H])
cube([BRACE_T + 8.0, BRACE_W, 2.5]);
}
// M3 bolt holes at base (attach to tnut_base bosses)
for (y = [8, BRACE_W-8])
translate([-EPS, y, 4])
rotate([0, 90, 0])
cylinder(d=M3_D, h=BRACE_T + 2*EPS);
// M3 counterbore from outer face
for (y = [8, BRACE_W-8])
translate([-EPS, y, 4])
rotate([0, 90, 0])
cylinder(d=M3_HEAD_D, h=M3_HEAD_H + EPS);
// Port-access cutout USB/HDMI/DP cluster (centred on brace face)
translate([-EPS, BRACE_W/2 - USB_CUT_W/2, POST_BASE_H + 2.0])
cube([BRACE_T + 2*EPS, USB_CUT_W, USB_CUT_H]);
// GbE cutout (offset toward +Y)
translate([-EPS, BRACE_W/2 + USB_CUT_W/2 - GBE_CUT_W - 2, POST_BASE_H + 2.0])
cube([BRACE_T + 2*EPS, GBE_CUT_W, GBE_CUT_H]);
// M3 duct attachment holes (top edge)
for (y = [BRACE_W/4, 3*BRACE_W/4])
translate([BRACE_T/2, y, BRACE_H - 2])
cylinder(d=M3_D, h=10);
// Ventilation slots (3 tall slots for airflow)
for (i = [0:2])
translate([-EPS,
(BRACE_W - 3*8 - 2*4) / 2 + i*(8+4),
POST_BASE_H + USB_CUT_H + 6])
cube([BRACE_T + 2*EPS, 8, BRACE_H - POST_BASE_H - USB_CUT_H - 10]);
}
}
// Part 4: duct_shroud
// Top cap that channels fan exhaust away from board; optional print.
module duct_shroud() {
duct_l = BASE_L - 2*BRACE_T - 1.0; // span between inner brace faces
duct_w = BRACE_W;
difference() {
union() {
// Top plate
cube([duct_l, duct_w, DUCT_T]);
// Front wall (fan inlet side)
translate([0, 0, -FAN_H])
cube([DUCT_T, duct_w, FAN_H + DUCT_T]);
// Rear wall (exhaust side open centre)
translate([duct_l - DUCT_T, 0, -FAN_H])
cube([DUCT_T, duct_w, FAN_H + DUCT_T]);
// Side flanges for M3 attachment
translate([-DUCT_FLANGE, 0, -FAN_H])
cube([DUCT_FLANGE, duct_w, FAN_H + DUCT_T]);
translate([duct_l, 0, -FAN_H])
cube([DUCT_FLANGE, duct_w, FAN_H + DUCT_T]);
}
// Fan cutout on top plate (centred)
translate([duct_l/2 - FAN_W/2, duct_w/2 - FAN_W/2, -EPS])
cube([FAN_W, FAN_W, DUCT_T + 2*EPS]);
// Fan screw holes (40 mm fan, Ø3.2 at 32 mm BC)
for (dx = [-16, 16])
for (dy = [-16, 16])
translate([duct_l/2 + dx, duct_w/2 + dy, -EPS])
cylinder(d=M3_D, h=DUCT_T + 2*EPS);
// Exhaust slot on rear wall (full width minus corners)
translate([duct_l - DUCT_T - EPS, 4, -FAN_H + 2])
cube([DUCT_T + 2*EPS, duct_w - 8, FAN_H - 2]);
// M3 flange attachment holes
for (y = [duct_w/4, 3*duct_w/4]) {
translate([-DUCT_FLANGE - EPS, y, -FAN_H/2])
rotate([0, 90, 0])
cylinder(d=M3_D, h=DUCT_FLANGE + 2*EPS);
translate([duct_l + DUCT_T - EPS, y, -FAN_H/2])
rotate([0, 90, 0])
cylinder(d=M3_D, h=DUCT_FLANGE + 2*EPS);
}
}
}
// Part 5: cable_clip
module cable_clip() {
difference() {
union() {
// Snap-wrap body
difference() {
cylinder(d=CLIP_OD + 2*SNAP_T, h=CLIP_W);
translate([0, 0, -EPS])
cylinder(d=CLIP_ID, h=CLIP_W + 2*EPS);
// Front gap
translate([-CLIP_GAP/2, 0, -EPS])
cube([CLIP_GAP, CLIP_OD, CLIP_W + 2*EPS]);
}
// Mounting tab for brace edge
translate([CLIP_OD/2 + SNAP_T - EPS, -SNAP_T, 0])
cube([8, SNAP_T*2, CLIP_W]);
}
// Tab screw hole
translate([CLIP_OD/2 + SNAP_T + 4, 0, CLIP_W/2])
rotate([90, 0, 0])
cylinder(d=M3_D, h=SNAP_T*2 + 2*EPS, center=true);
}
}
// Assembly
module assembly() {
// Base plate
color("SteelBlue")
tnut_base();
// Standoff posts (board hole pattern)
bx0 = BASE_L/2 - MH_SX/2;
by0 = BASE_W/2 - MH_SY/2;
for (dx = [0, MH_SX])
for (dy = [0, MH_SY])
color("DodgerBlue")
translate([bx0+dx, by0+dy, BASE_T])
standoff_post();
// Side braces (left and right)
color("CornflowerBlue")
translate([0, 0, BASE_T])
side_brace();
color("CornflowerBlue")
translate([BASE_L, BRACE_W, BASE_T])
mirror([1, 0, 0])
mirror([0, 1, 0])
side_brace();
// Board silhouette (translucent, for clearance visualisation)
color("ForestGreen", 0.25)
translate([BASE_L/2 - BOARD_L/2, BASE_W/2 - BOARD_W/2,
BASE_T + POST_BASE_H + POST_H])
cube([BOARD_L, BOARD_W, BOARD_T]);
// Duct shroud (above board)
color("LightSteelBlue", 0.7)
translate([BRACE_T + 0.5, 0,
BASE_T + POST_BASE_H + POST_H + BOARD_T + 2.0])
duct_shroud();
// Cable clips (on brace edge, 2×)
for (y = [BRACE_W/3, 2*BRACE_W/3])
color("SlateGray")
translate([BASE_L + 2, y, BASE_T + BRACE_H/2 - CLIP_W/2])
rotate([0, 90, 0])
cable_clip();
}
// Dispatch
if (RENDER == "tnut_base") tnut_base();
else if (RENDER == "standoff_post") standoff_post();
else if (RENDER == "side_brace") side_brace();
else if (RENDER == "duct_shroud") duct_shroud();
else if (RENDER == "cable_clip") cable_clip();
else assembly();

View File

@ -65,7 +65,7 @@ CLAMP_ALIGN_D = 4.1; // Ø4 pin
// D-cut bore clearance // D-cut bore clearance
DCUT_CL = 0.3; DCUT_CL = 0.3;
// FC mount ESP32-S3 BALANCE 30.5 × 30.5 mm M3 // FC mount MAMBA F722S 30.5 × 30.5 mm M3
FC_PITCH = 30.5; FC_PITCH = 30.5;
FC_HOLE_D = 3.2; FC_HOLE_D = 3.2;
// FC is offset toward front of plate (away from stem) // FC is offset toward front of plate (away from stem)
@ -202,7 +202,7 @@ module base_plate() {
translate([STEM_FLANGE_BC/2, 0, -1]) translate([STEM_FLANGE_BC/2, 0, -1])
cylinder(d=M5, h=PLATE_THICK + 2); cylinder(d=M5, h=PLATE_THICK + 2);
// FC mount (ESP32-S3 BALANCE 30.5 × 30.5 M3) // FC mount (MAMBA F722S 30.5 × 30.5 M3)
for (x = [FC_X_OFFSET - FC_PITCH/2, FC_X_OFFSET + FC_PITCH/2]) for (x = [FC_X_OFFSET - FC_PITCH/2, FC_X_OFFSET + FC_PITCH/2])
for (y = [-FC_PITCH/2, FC_PITCH/2]) for (y = [-FC_PITCH/2, FC_PITCH/2])
translate([x, y, -1]) translate([x, y, -1])

View File

@ -11,7 +11,7 @@
// Ventilation slots all 4 walls + lid // Ventilation slots all 4 walls + lid
// //
// Shared mounting patterns (swappable with SaltyLab): // Shared mounting patterns (swappable with SaltyLab):
// FC : 30.5 × 30.5 mm M3 (ESP32-S3 BALANCE / Pixhawk) // FC : 30.5 × 30.5 mm M3 (MAMBA F722S / Pixhawk)
// Jetson: 58 × 49 mm M3 (Orin NX / Nano Devkit carrier) // Jetson: 58 × 49 mm M3 (Orin NX / Nano Devkit carrier)
// //
// Coordinate: bay centred at origin; Z=0 = deck top face. // Coordinate: bay centred at origin; Z=0 = deck top face.

View File

@ -17,7 +17,7 @@
// Weight target: <2 kg frame (excl. motors/electronics) // Weight target: <2 kg frame (excl. motors/electronics)
// //
// Shared SaltyLab patterns (swappable electronics): // Shared SaltyLab patterns (swappable electronics):
// FC : 30.5 × 30.5 mm M3 (ESP32-S3 BALANCE / Pixhawk) // FC : 30.5 × 30.5 mm M3 (MAMBA F722S / Pixhawk)
// Jetson: 58 × 49 mm M3 (Orin NX / Nano carrier board) // Jetson: 58 × 49 mm M3 (Orin NX / Nano carrier board)
// Stem : Ø25 mm bore (sensor head unchanged) // Stem : Ø25 mm bore (sensor head unchanged)
// //
@ -87,7 +87,7 @@ STEM_COLLAR_OD = 50.0;
STEM_COLLAR_H = 20.0; // raised boss height above deck top STEM_COLLAR_H = 20.0; // raised boss height above deck top
STEM_FLANGE_BC = 40.0; // 4× M4 bolt circle for stem adapter STEM_FLANGE_BC = 40.0; // 4× M4 bolt circle for stem adapter
// FC mount ESP32-S3 BALANCE / Pixhawk (30.5 × 30.5 mm M3) // FC mount MAMBA F722S / Pixhawk (30.5 × 30.5 mm M3)
// Shared with SaltyLab swappable electronics // Shared with SaltyLab swappable electronics
FC_PITCH = 30.5; FC_PITCH = 30.5;
FC_HOLE_D = 3.2; FC_HOLE_D = 3.2;

View File

@ -1,296 +0,0 @@
// ============================================================
// vesc_mount.scad FSESC 6.7 Pro Mini Dual ESC Mount Cradle
// Issue #699 / sl-mechanical 2026-03-17
// ============================================================
// Open-top tray for Flipsky FSESC 6.7 Pro Mini Dual (~100 × 68 × 28 mm).
// Attaches to 2020 aluminium T-slot rail via 4× M5 T-nuts
// (2× per rail, two parallel rails, 60 mm bolt spacing in X,
// 20 mm bolt spacing in Y matching 2020 slot pitch).
//
// Connector access:
// XT60 battery inputs X end wall cutouts (2 connectors, side-by-side)
// XT30 motor outputs Y+ and Y side wall cutouts (2 per side wall)
// CAN/UART terminal X+ end wall cutout (screw terminal, wire exit)
//
// Ventilation:
// Open top face heatsink fins fully exposed
// Floor grille slots under-board airflow
// Side vent louvres 4 slots on each Y± wall at heatsink height
//
// Retention: 4× M3 heat-set insert boss in floor board screws down through
// ESC mounting holes via M3×8 FHCS. Board sits on 4 mm raised posts for
// under-board airflow.
//
// VERIFY WITH CALIPERS BEFORE PRINTING:
// PCB_L, PCB_W board outline
// XT60_W, XT60_H XT60 shell at X edge
// XT30_W, XT30_H XT30 shells at Y± edges
// TERM_W, TERM_H CAN screw terminal at X+ edge
// MOUNT_X1/X2, MOUNT_Y1/Y2 ESC board mounting hole pattern
//
// Print settings (PETG):
// 3 perimeters, 40 % gyroid infill, no supports, 0.2 mm layer
// Print orientation: open face UP (as modelled)
//
// BOM:
// 4 × M5×10 BHCS + 4 × M5 slide-in T-nut (2020 rail)
// 4 × M3 heat-set insert (Voron-style, OD 4.5 mm × 4 mm deep)
// 4 × M3×8 FHCS (board retention)
//
// Export commands:
// openscad -D 'RENDER="mount"' -o vesc_mount.stl vesc_mount.scad
// openscad -D 'RENDER="assembly"' -o vesc_assembly.png vesc_mount.scad
// ============================================================
RENDER = "assembly"; // mount | assembly
$fn = 48;
EPS = 0.01;
// Verify before printing
// FSESC 6.7 Pro Mini Dual PCB
PCB_L = 100.0; // board length (X: XT60 end CAN terminal end)
PCB_W = 68.0; // board width (Y)
PCB_T = 2.0; // board thickness (incl. bottom-side components)
COMP_H = 26.0; // tallest component above board top face (heatsink ~26 mm)
// XT60 battery connectors at X end (2 connectors, side-by-side)
XT60_W = 16.0; // each XT60 shell width (Y)
XT60_H = 16.0; // each XT60 shell height (Z) above board surface
XT60_Z0 = 0.0; // connector bottom offset above board surface
// Y centres of each XT60 measured from PCB Y edge
XT60_Y1 = 16.0;
XT60_Y2 = 52.0;
// XT30 motor output connectors at Y± sides (2 per side)
XT30_W = 10.5; // each XT30 shell width (X span)
XT30_H = 12.0; // each XT30 shell height (Z) above board surface
XT30_Z0 = 0.5; // connector bottom offset above board surface
// X centres measured from PCB X edge (same layout both Y and Y+ sides)
XT30_X1 = 22.0;
XT30_X2 = 78.0;
// CAN / UART screw terminal block at X+ end (3-pos 3.5 mm pitch)
TERM_W = 14.0; // terminal block Y span
TERM_H = 10.0; // terminal block height above board surface
TERM_Z0 = 0.5; // terminal bottom offset above board surface
TERM_Y_CTR = PCB_W / 2;
// ESC board mounting hole pattern
// 4 corner holes, 4 mm inset from each PCB edge
MOUNT_INSET = 4.0;
MOUNT_X1 = MOUNT_INSET;
MOUNT_X2 = PCB_L - MOUNT_INSET;
MOUNT_Y1 = MOUNT_INSET;
MOUNT_Y2 = PCB_W - MOUNT_INSET;
M3_INSERT_OD = 4.6; // Voron M3 heat-set insert press-fit OD
M3_INSERT_H = 4.0; // insert depth
M3_CLEAR_D = 3.4; // M3 clearance bore below insert
// Cradle geometry
WALL_T = 2.8; // side / end wall thickness
FLOOR_T = 4.5; // floor plate thickness (fits M5 BHCS head pocket)
POST_H = 4.0; // standoff post height (board lifts off floor for airflow)
CL_SIDE = 0.35; // Y clearance per side
CL_END = 0.40; // X clearance per end
INN_W = PCB_W + 2*CL_SIDE;
INN_L = PCB_L + 2*CL_END;
INN_H = POST_H + PCB_T + COMP_H + 1.5;
OTR_W = INN_W + 2*WALL_T;
OTR_L = INN_L + 2*WALL_T;
OTR_H = FLOOR_T + INN_H;
PCB_X0 = WALL_T + CL_END;
PCB_Y0 = WALL_T + CL_SIDE;
PCB_Z0 = FLOOR_T + POST_H;
// M5 T-nut mount (2020 rail)
// 4 bolts: 2 columns (X) × 2 rows (Y), centred on body
M5_D = 5.3;
M5_HEAD_D = 9.5;
M5_HEAD_H = 3.0;
M5_SPAC_X = 60.0; // X bolt spacing
M5_SPAC_Y = 20.0; // Y bolt spacing (2020 T-slot pitch)
// Floor ventilation grille
GRILLE_SLOT_W = 4.0;
GRILLE_SLOT_T = FLOOR_T - 1.5;
GRILLE_PITCH = 10.0;
GRILLE_X0 = WALL_T + 14;
GRILLE_X_LEN = OTR_L - 2*WALL_T - 28;
GRILLE_N = floor((INN_W - 10) / GRILLE_PITCH);
// Side vent louvres on Y± walls
LOUV_H = 5.0;
LOUV_W = 12.0;
LOUV_Z = FLOOR_T + POST_H + PCB_T + 4.0; // mid-heatsink height
LOUV_N = 4;
LOUV_PITCH = (OTR_L - 2*WALL_T - 20) / max(LOUV_N - 1, 1);
// CAN wire strain relief bosses (X+ end)
SR_BOSS_OD = 7.0;
SR_BOSS_H = 6.0;
SR_SLOT_W = 3.5;
SR_SLOT_T = 2.2;
SR_Y1 = WALL_T + INN_W * 0.25;
SR_Y2 = WALL_T + INN_W * 0.75;
SR_X = OTR_L - WALL_T - SR_BOSS_OD/2 - 2.5;
//
module m3_insert_boss() {
// Solid post with heat-set insert bore from top
post_h = FLOOR_T + POST_H;
difference() {
cylinder(d = M3_INSERT_OD + 3.2, h = post_h);
// Insert bore from top
translate([0, 0, post_h - M3_INSERT_H])
cylinder(d = M3_INSERT_OD, h = M3_INSERT_H + EPS);
// Clearance bore from bottom
translate([0, 0, -EPS])
cylinder(d = M3_CLEAR_D, h = post_h - M3_INSERT_H + EPS);
}
}
module vesc_mount() {
difference() {
union() {
// Main body
cube([OTR_L, OTR_W, OTR_H]);
// M3 insert bosses at board mounting corners
for (mx = [MOUNT_X1, MOUNT_X2])
for (my = [MOUNT_Y1, MOUNT_Y2])
translate([PCB_X0 + mx, PCB_Y0 + my, 0])
m3_insert_boss();
// CAN strain relief bosses on X+ end
for (sy = [SR_Y1, SR_Y2])
translate([SR_X, sy, 0])
cylinder(d = SR_BOSS_OD, h = SR_BOSS_H);
}
// Interior cavity (open top)
translate([WALL_T, WALL_T, FLOOR_T])
cube([INN_L, INN_W, INN_H + EPS]);
// XT60 cutouts at X end (2 connectors)
for (yc = [XT60_Y1, XT60_Y2])
translate([-EPS,
PCB_Y0 + yc - (XT60_W + 2.0)/2,
PCB_Z0 + XT60_Z0 - 0.5])
cube([WALL_T + 2*EPS, XT60_W + 2.0, XT60_H + 3.0]);
// XT30 cutouts at Y side (2 connectors)
for (xc = [XT30_X1, XT30_X2])
translate([PCB_X0 + xc - (XT30_W + 2.0)/2,
-EPS,
PCB_Z0 + XT30_Z0 - 0.5])
cube([XT30_W + 2.0, WALL_T + 2*EPS, XT30_H + 3.0]);
// XT30 cutouts at Y+ side (2 connectors)
for (xc = [XT30_X1, XT30_X2])
translate([PCB_X0 + xc - (XT30_W + 2.0)/2,
OTR_W - WALL_T - EPS,
PCB_Z0 + XT30_Z0 - 0.5])
cube([XT30_W + 2.0, WALL_T + 2*EPS, XT30_H + 3.0]);
// CAN terminal cutout at X+ end
translate([OTR_L - WALL_T - EPS,
PCB_Y0 + TERM_Y_CTR - (TERM_W + 3.0)/2,
PCB_Z0 + TERM_Z0 - 0.5])
cube([WALL_T + 2*EPS, TERM_W + 3.0, TERM_H + 5.0]);
// Floor ventilation grille
for (i = [0 : GRILLE_N - 1]) {
sy = WALL_T + 5 + i * GRILLE_PITCH;
translate([GRILLE_X0, sy, -EPS])
cube([GRILLE_X_LEN, GRILLE_SLOT_W, GRILLE_SLOT_T + EPS]);
}
// Side vent louvres Y wall
for (i = [0 : LOUV_N - 1]) {
lx = WALL_T + 10 + i * LOUV_PITCH;
translate([lx, -EPS, LOUV_Z])
cube([LOUV_W, WALL_T + 2*EPS, LOUV_H]);
}
// Side vent louvres Y+ wall
for (i = [0 : LOUV_N - 1]) {
lx = WALL_T + 10 + i * LOUV_PITCH;
translate([lx, OTR_W - WALL_T - EPS, LOUV_Z])
cube([LOUV_W, WALL_T + 2*EPS, LOUV_H]);
}
// M5 BHCS head pockets (4 bolts, bottom face)
for (mx = [OTR_L/2 - M5_SPAC_X/2, OTR_L/2 + M5_SPAC_X/2])
for (my = [OTR_W/2 - M5_SPAC_Y/2, OTR_W/2 + M5_SPAC_Y/2])
translate([mx, my, -EPS]) {
cylinder(d = M5_D, h = FLOOR_T + 2*EPS);
cylinder(d = M5_HEAD_D, h = M5_HEAD_H + EPS);
}
// Zip-tie slots through CAN strain relief bosses
for (sy = [SR_Y1, SR_Y2])
translate([SR_X, sy, SR_BOSS_H/2 - SR_SLOT_T/2])
rotate([0, 90, 0])
cube([SR_SLOT_T, SR_SLOT_W, SR_BOSS_OD + 2*EPS],
center = true);
// Weight-relief pocket in floor underside
translate([WALL_T + 16, WALL_T + 6, -EPS])
cube([OTR_L - 2*WALL_T - 32, OTR_W - 2*WALL_T - 12,
FLOOR_T - 2.0 + EPS]);
}
}
// Assembly preview
if (RENDER == "assembly") {
color("DimGray", 0.93) vesc_mount();
// Phantom PCB
color("ForestGreen", 0.30)
translate([PCB_X0, PCB_Y0, PCB_Z0])
cube([PCB_L, PCB_W, PCB_T]);
// Phantom heatsink / component block
color("SlateGray", 0.22)
translate([PCB_X0, PCB_Y0, PCB_Z0 + PCB_T])
cube([PCB_L, PCB_W, COMP_H]);
// XT60 connector highlights (X end)
for (yc = [XT60_Y1, XT60_Y2])
color("Gold", 0.85)
translate([-2,
PCB_Y0 + yc - XT60_W/2,
PCB_Z0 + XT60_Z0])
cube([WALL_T + 3, XT60_W, XT60_H]);
// XT30 connector highlights Y side
for (xc = [XT30_X1, XT30_X2])
color("OrangeRed", 0.80)
translate([PCB_X0 + xc - XT30_W/2,
-2,
PCB_Z0 + XT30_Z0])
cube([XT30_W, WALL_T + 3, XT30_H]);
// XT30 connector highlights Y+ side
for (xc = [XT30_X1, XT30_X2])
color("OrangeRed", 0.80)
translate([PCB_X0 + xc - XT30_W/2,
OTR_W - WALL_T - 1,
PCB_Z0 + XT30_Z0])
cube([XT30_W, WALL_T + 3, XT30_H]);
// CAN terminal highlight
color("Tomato", 0.75)
translate([OTR_L - WALL_T - 1,
PCB_Y0 + TERM_Y_CTR - TERM_W/2,
PCB_Z0 + TERM_Z0])
cube([WALL_T + 3, TERM_W, TERM_H]);
} else {
vesc_mount();
}

View File

@ -2,44 +2,22 @@
You're working on **SaltyLab**, a self-balancing two-wheeled indoor robot. Read this entire file before touching anything. You're working on **SaltyLab**, a self-balancing two-wheeled indoor robot. Read this entire file before touching anything.
## ⚠️ ARCHITECTURE — SAUL-TEE (finalised 2026-04-04) ## Project Overview
<<<<<<< HEAD
Full hardware spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md` — **read it before writing firmware.**
| Board | Role |
|-------|------|
| **ESP32-S3 BALANCE** | Waveshare Touch LCD 1.28 (CH343 USB). QMI8658 IMU, PID loop, CAN→VESC L(68)/R(56), GC9A01 LCD |
| **ESP32-S3 IO** | Bare devkit (JTAG USB). TBS Crossfire RC (UART0), ELRS failover (UART2), BTS7960 motors, NFC/baro/ToF, WS2812, buzzer/horn/headlight/fan |
| **Jetson Orin** | CANable2 USB→CAN. Cmds on 0x3000x303, telemetry on 0x4000x401 |
```
Jetson Orin ──CANable2──► CAN 500kbps ◄───────────────────────┐
│ │
ESP32-S3 BALANCE ←─UART 460800─► ESP32-S3 IO
(QMI8658, PID loop) (BTS7960, RC, sensors)
│ CAN 500kbps
┌─────────┴──────────┐
VESC Left (ID 68) VESC Right (ID 56)
=======
A hoverboard-based balancing robot with two compute layers: A hoverboard-based balancing robot with two compute layers:
1. **ESP32-S3 BALANCE** — ESP32-S3 BALANCE (ESP32-S3RET6 + MPU6000 IMU). Runs a lean C balance loop at up to 8kHz. Talks UART to the hoverboard ESC. This is the safety-critical layer. 1. **FC (Flight Controller)** — MAMBA F722S (STM32F722RET6 + MPU6000 IMU). Runs a lean C balance loop at up to 8kHz. Talks UART to the hoverboard ESC. This is the safety-critical layer.
2. **Jetson Orin Nano Super** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently. 2. **Jetson Nano** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently.
``` ```
Jetson (speed+steer via UART1) ←→ ELRS RC (UART3, kill switch) Jetson (speed+steer via UART1) ←→ ELRS RC (UART3, kill switch)
ESP32-S3 BALANCE (MPU6000 IMU, PID balance) MAMBA F722S (MPU6000 IMU, PID balance)
▼ UART2 ▼ UART2
Hoverboard ESC (FOC) → 2× 8" hub motors Hoverboard ESC (FOC) → 2× 8" hub motors
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
``` ```
Frame: `[0xAA][LEN][TYPE][PAYLOAD][CRC8]`
Legacy `src/` STM32 HAL code is **archived — do not extend.**
## ⚠️ SAFETY — READ THIS OR PEOPLE GET HURT ## ⚠️ SAFETY — READ THIS OR PEOPLE GET HURT
This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, and launch the frame. Every firmware change must preserve these invariants: This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, and launch the frame. Every firmware change must preserve these invariants:
@ -57,14 +35,10 @@ This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, an
## Repository Layout ## Repository Layout
``` ```
<<<<<<< HEAD firmware/ # STM32 HAL firmware (PlatformIO)
firmware/ # Legacy ESP32/STM32 HAL firmware (PlatformIO, archived)
=======
firmware/ # ESP-IDF firmware (PlatformIO)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
├── src/ ├── src/
│ ├── main.c # Entry point, clock config, main loop │ ├── main.c # Entry point, clock config, main loop
│ ├── icm42688.c # QMI8658-P SPI driver (backup IMU — currently broken) │ ├── icm42688.c # ICM-42688-P SPI driver (backup IMU — currently broken)
│ ├── bmp280.c # Barometer driver (disabled) │ ├── bmp280.c # Barometer driver (disabled)
│ └── status.c # LED + buzzer status patterns │ └── status.c # LED + buzzer status patterns
├── include/ ├── include/
@ -75,7 +49,7 @@ firmware/ # ESP-IDF firmware (PlatformIO)
│ ├── crsf.h # ELRS CRSF protocol │ ├── crsf.h # ELRS CRSF protocol
│ ├── bmp280.h │ ├── bmp280.h
│ └── status.h │ └── status.h
├── lib/USB_CDC/ # USB Serial (CH343) stack (serial over USB) ├── lib/USB_CDC/ # USB CDC stack (serial over USB)
│ ├── src/ # CDC implementation, USB descriptors, PCD config │ ├── src/ # CDC implementation, USB descriptors, PCD config
│ └── include/ │ └── include/
└── platformio.ini # Build config └── platformio.ini # Build config
@ -108,24 +82,16 @@ PLATFORM.md # Hardware platform reference
## Hardware Quick Reference ## Hardware Quick Reference
<<<<<<< HEAD ### MAMBA F722S Flight Controller
### ESP32 BALANCE Flight Controller
| Spec | Value | | Spec | Value |
|------|-------| |------|-------|
| MCU | ESP32RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) | | MCU | STM32F722RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
=======
### ESP32-S3 BALANCE Flight Controller
| Spec | Value |
|------|-------|
| MCU | ESP32-S3RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Primary IMU | MPU6000 (WHO_AM_I = 0x68) | | Primary IMU | MPU6000 (WHO_AM_I = 0x68) |
| IMU Bus | SPI1: PA5=SCK, PA6=MISO, PA7=MOSI, CS=PA4 | | IMU Bus | SPI1: PA5=SCK, PA6=MISO, PA7=MOSI, CS=PA4 |
| IMU EXTI | PC4 (data ready interrupt) | | IMU EXTI | PC4 (data ready interrupt) |
| IMU Orientation | CW270 (Betaflight convention) | | IMU Orientation | CW270 (Betaflight convention) |
| Secondary IMU | QMI8658-P (on same SPI1, CS unknown — currently non-functional) | | Secondary IMU | ICM-42688-P (on same SPI1, CS unknown — currently non-functional) |
| Betaflight Target | DIAT-MAMBAF722_2022B | | Betaflight Target | DIAT-MAMBAF722_2022B |
| USB | OTG FS (PA11/PA12), enumerates as /dev/cu.usbmodemSALTY0011 | | USB | OTG FS (PA11/PA12), enumerates as /dev/cu.usbmodemSALTY0011 |
| VID/PID | 0x0483/0x5740 | | VID/PID | 0x0483/0x5740 |
@ -138,7 +104,7 @@ PLATFORM.md # Hardware platform reference
| UART | Pins | Connected To | Baud | | UART | Pins | Connected To | Baud |
|------|------|-------------|------| |------|------|-------------|------|
| USART1 | PA9/PA10 | Jetson Orin Nano Super | 115200 | | USART1 | PA9/PA10 | Jetson Nano | 115200 |
| USART2 | PA2/PA3 | Hoverboard ESC | 115200 | | USART2 | PA2/PA3 | Hoverboard ESC | 115200 |
| USART3 | PB10/PB11 | ELRS Receiver | 420000 (CRSF) | | USART3 | PB10/PB11 | ELRS Receiver | 420000 (CRSF) |
| UART4 | — | Spare | — | | UART4 | — | Spare | — |
@ -159,7 +125,7 @@ PLATFORM.md # Hardware platform reference
| FC board size | ~36mm square | | FC board size | ~36mm square |
| Hub motor body | Ø200mm (~8") | | Hub motor body | Ø200mm (~8") |
| Motor axle | Ø12mm, 45mm long | | Motor axle | Ø12mm, 45mm long |
| Jetson Orin Nano Super | 100×80×29mm, M2.5 holes at 86×58mm | | Jetson Nano | 100×80×29mm, M2.5 holes at 86×58mm |
| RealSense D435i | 90×25×25mm, 1/4-20 tripod mount | | RealSense D435i | 90×25×25mm, 1/4-20 tripod mount |
| RPLIDAR A1 | Ø70×41mm, 4× M2.5 on Ø67mm circle | | RPLIDAR A1 | Ø70×41mm, 4× M2.5 on Ø67mm circle |
| Kill switch hole | Ø22mm panel mount | | Kill switch hole | Ø22mm panel mount |
@ -194,27 +160,19 @@ PLATFORM.md # Hardware platform reference
### Critical Lessons Learned (DON'T REPEAT THESE) ### Critical Lessons Learned (DON'T REPEAT THESE)
1. **SysTick_Handler with HAL_IncTick() is MANDATORY** — without it, HAL_Delay() and every HAL timeout hangs forever. This bricked us multiple times. 1. **SysTick_Handler with HAL_IncTick() is MANDATORY** — without it, HAL_Delay() and every HAL timeout hangs forever. This bricked us multiple times.
<<<<<<< HEAD 2. **DCache breaks SPI on STM32F7** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.
2. **DCache breaks SPI on ESP32** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.
=======
2. **DCache breaks SPI on ESP32-S3** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
3. **`-(int)0 == 0`** — checking `if (-result)` to detect errors doesn't work when result is 0 (success and failure look the same). Always use explicit error codes. 3. **`-(int)0 == 0`** — checking `if (-result)` to detect errors doesn't work when result is 0 (success and failure look the same). Always use explicit error codes.
4. **NEVER auto-run untested code on_boot** — we bricked the NSPanel 3x doing this. Test manually first. 4. **NEVER auto-run untested code on_boot** — we bricked the NSPanel 3x doing this. Test manually first.
5. **USB Serial (CH343) needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception. 5. **USB CDC needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception.
### DFU Reboot (Betaflight Method) ### DFU Reboot (Betaflight Method)
The firmware supports reboot-to-DFU via USB command: The firmware supports reboot-to-DFU via USB command:
1. Send `R` byte over USB Serial (CH343) 1. Send `R` byte over USB CDC
2. Firmware writes `0xDEADBEEF` to RTC backup register 0 2. Firmware writes `0xDEADBEEF` to RTC backup register 0
3. `NVIC_SystemReset()` — clean hardware reset 3. `NVIC_SystemReset()` — clean hardware reset
4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic 4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic
<<<<<<< HEAD 5. If magic found: clears it, remaps system memory, jumps to STM32 bootloader at `0x1FF00000`
5. If magic found: clears it, remaps system memory, jumps to ESP32 BALANCE bootloader at `0x1FF00000`
=======
5. If magic found: clears it, remaps system memory, jumps to ESP32-S3 bootloader at `0x1FF00000`
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
6. Board appears as DFU device, ready for `dfu-util` flash 6. Board appears as DFU device, ready for `dfu-util` flash
### Build & Flash ### Build & Flash
@ -240,14 +198,14 @@ Fallback: HSI 16MHz if HSE fails (PLL M=16)
## Current Status & Known Issues ## Current Status & Known Issues
### Working ### Working
- USB Serial (CH343) serial streaming (50Hz JSON: `{"ax":...,"ay":...,"az":...,"gx":...,"gy":...,"gz":...}`) - USB CDC serial streaming (50Hz JSON: `{"ax":...,"ay":...,"az":...,"gx":...,"gy":...,"gz":...}`)
- Clock config with HSE + HSI fallback - Clock config with HSE + HSI fallback
- Reboot-to-DFU via USB 'R' command - Reboot-to-DFU via USB 'R' command
- LED status patterns (status.c) - LED status patterns (status.c)
- Web UI with WebSerial + Three.js 3D visualization - Web UI with WebSerial + Three.js 3D visualization
### Broken / In Progress ### Broken / In Progress
- **QMI8658-P SPI reads return all zeros** — was the original IMU target, but SPI communication completely non-functional despite correct pin config. May be dead silicon. Switched to MPU6000 as primary. - **ICM-42688-P SPI reads return all zeros** — was the original IMU target, but SPI communication completely non-functional despite correct pin config. May be dead silicon. Switched to MPU6000 as primary.
- **MPU6000 driver** — header exists but implementation needs completion - **MPU6000 driver** — header exists but implementation needs completion
- **PID balance loop** — not yet implemented - **PID balance loop** — not yet implemented
- **Hoverboard ESC UART** — protocol defined, driver not written - **Hoverboard ESC UART** — protocol defined, driver not written
@ -285,7 +243,7 @@ T:12.3,P:45,L:100,R:-80,S:3\n
// T=tilt°, P=PID output, L/R=motor commands, S=state (0-3) // T=tilt°, P=PID output, L/R=motor commands, S=state (0-3)
``` ```
### FC → USB Serial (CH343) (50Hz JSON) ### FC → USB CDC (50Hz JSON)
```json ```json
{"ax":123,"ay":-456,"az":16384,"gx":10,"gy":-5,"gz":3,"t":250,"p":0,"bt":0} {"ax":123,"ay":-456,"az":16384,"gx":10,"gy":-5,"gz":3,"t":250,"p":0,"bt":0}
// Raw IMU values (int16), t=temp×10, p=pressure, bt=baro temp // Raw IMU values (int16), t=temp×10, p=pressure, bt=baro temp

View File

@ -1,10 +1,6 @@
# Face LCD Animation System (Issue #507) # Face LCD Animation System (Issue #507)
<<<<<<< HEAD Implements expressive face animations on an STM32 LCD display with 5 core emotions and smooth transitions.
Implements expressive face animations on an ESP32 LCD display with 5 core emotions and smooth transitions.
=======
Implements expressive face animations on an ESP32-S3 LCD display with 5 core emotions and smooth transitions.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Features ## Features
@ -86,11 +82,7 @@ STATUS → Echo current emotion + idle state
- Colors: Monochrome (1-bit) or RGB565 - Colors: Monochrome (1-bit) or RGB565
### Microcontroller ### Microcontroller
<<<<<<< HEAD - STM32F7xx (Mamba F722S)
- ESP32xx (ESP32 BALANCE)
=======
- ESP32-S3xx (ESP32-S3 BALANCE)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
- Available UART: USART3 (PB10=TX, PB11=RX) - Available UART: USART3 (PB10=TX, PB11=RX)
- Clock: 216 MHz - Clock: 216 MHz

View File

@ -81,7 +81,7 @@
│ │ │ │
│ [RealSense D435i] │ ← Front-facing, angled down ~10° │ [RealSense D435i] │ ← Front-facing, angled down ~10°
│ │ Height: ~400mm from ground │ │ Height: ~400mm from ground
│ [Jetson Orin Nano Super] │ ← Center, in ventilated enclosure │ [Jetson Nano] │ ← Center, in ventilated enclosure
│ [WiFi/4G module] │ Noctua fan draws air through │ [WiFi/4G module] │ Noctua fan draws air through
│ │ │ │
│ [Speaker] [LEDs] │ ← Rear: audio feedback + status │ [Speaker] [LEDs] │ ← Rear: audio feedback + status
@ -173,7 +173,7 @@ PACK1 ═╤═ PACK2 (parallel, XT60)
│ │ │ │
│ └── UART TX/RX ──→ Jetson GPIO │ └── UART TX/RX ──→ Jetson GPIO
├──→ DC-DC 36V→5V ──→ Jetson Orin Nano Super (barrel jack 5V/4A) ├──→ DC-DC 36V→5V ──→ Jetson Nano (barrel jack 5V/4A)
│ ──→ USB hub (sensors) │ ──→ USB hub (sensors)
├──→ DC-DC 36V→12V ──→ LED strips ├──→ DC-DC 36V→12V ──→ LED strips

View File

@ -33,7 +33,7 @@ Self-balancing two-wheeled indoor robot with AI brain.
| Component | Voltage | Current | Power (W) | Notes | | Component | Voltage | Current | Power (W) | Notes |
|-----------|---------|---------|-----------|-------| |-----------|---------|---------|-----------|-------|
| Jetson Orin Nano Super | 5V | 2-4A | 10-20W | AI inference mode: ~15W avg | | Jetson Nano | 5V | 2-4A | 10-20W | AI inference mode: ~15W avg |
| RealSense D435i | 5V (USB) | 0.7A | 3.5W | Depth + RGB streaming | | RealSense D435i | 5V (USB) | 0.7A | 3.5W | Depth + RGB streaming |
| RPLIDAR A1M8 | 5V | 0.5A | 2.5W | Spinning at 5.5Hz | | RPLIDAR A1M8 | 5V | 0.5A | 2.5W | Spinning at 5.5Hz |
| BNO055 IMU | 3.3V | 0.01A | 0.04W | Negligible | | BNO055 IMU | 3.3V | 0.01A | 0.04W | Negligible |
@ -80,7 +80,7 @@ Self-balancing two-wheeled indoor robot with AI brain.
| Battery pack (1x) | 2500 | Estimated, weigh to verify | | Battery pack (1x) | 2500 | Estimated, weigh to verify |
| 2x 8" hub motors | 2400 | ~1200g each with tire | | 2x 8" hub motors | 2400 | ~1200g each with tire |
| ESC board | 150 | Single board | | ESC board | 150 | Single board |
| Jetson Orin Nano Super + heatsink | 280 | With Noctua fan | | Jetson Nano + heatsink | 280 | With Noctua fan |
| RealSense D435i | 72 | Very light | | RealSense D435i | 72 | Very light |
| RPLIDAR A1M8 | 170 | With motor | | RPLIDAR A1M8 | 170 | With motor |
| BNO055 breakout | 5 | Tiny | | BNO055 breakout | 5 | Tiny |
@ -233,7 +233,7 @@ Self-balancing two-wheeled indoor robot with AI brain.
0mm — Base plate 0mm — Base plate
30mm — Battery shelf (holds pack on its side) 30mm — Battery shelf (holds pack on its side)
150mm — ESC + DC-DC shelf 150mm — ESC + DC-DC shelf
250mm — Jetson Orin Nano Super shelf 250mm — Jetson Nano shelf
300mm — BNO055 (attached to spine directly) 300mm — BNO055 (attached to spine directly)
370mm — RealSense bracket (front-facing arm) 370mm — RealSense bracket (front-facing arm)
420mm — LIDAR standoff begins 420mm — LIDAR standoff begins
@ -325,7 +325,7 @@ Self-balancing two-wheeled indoor robot with AI brain.
- [ ] Assemble spine onto base plate - [ ] Assemble spine onto base plate
- [ ] Mount battery to lowest shelf (velcro straps) - [ ] Mount battery to lowest shelf (velcro straps)
- [ ] Mount ESC + DC-DC converters - [ ] Mount ESC + DC-DC converters
- [ ] Mount Jetson Orin Nano Super on shelf, connect 5V power - [ ] Mount Jetson Nano on shelf, connect 5V power
- [ ] Wire Jetson UART → ESC UART - [ ] Wire Jetson UART → ESC UART
- [ ] Install JetPack 4.6 on Jetson (if not already) - [ ] Install JetPack 4.6 on Jetson (if not already)
- [ ] Write serial bridge: Jetson Python → ESC UART commands - [ ] Write serial bridge: Jetson Python → ESC UART commands

View File

@ -1,6 +1,6 @@
# SAUL-TEE — Self-Balancing Wagon Robot 🔬 # SaltyLab — Self-Balancing Indoor Bot 🔬
Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md` Two-wheeled, self-balancing robot for indoor AI/SLAM experiments.
## ⚠️ SAFETY — TOP PRIORITY ## ⚠️ SAFETY — TOP PRIORITY
@ -32,8 +32,8 @@ Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REF
|------|--------| |------|--------|
| 2x 8" pneumatic hub motors (36 PSI) | ✅ Have | | 2x 8" pneumatic hub motors (36 PSI) | ✅ Have |
| 1x hoverboard ESC (FOC firmware) | ✅ Have | | 1x hoverboard ESC (FOC firmware) | ✅ Have |
| 1x Drone FC (ESP32-S3 + QMI8658) | ✅ Have — balance brain | | 1x Drone FC (STM32F745 + MPU-6000) | ✅ Have — balance brain |
| 1x Jetson Orin Nano Super + Noctua fan | ✅ Have | | 1x Jetson Nano + Noctua fan | ✅ Have |
| 1x RealSense D435i | ✅ Have | | 1x RealSense D435i | ✅ Have |
| 1x RPLIDAR A1M8 | ✅ Have | | 1x RPLIDAR A1M8 | ✅ Have |
| 1x battery pack (36V) | ✅ Have | | 1x battery pack (36V) | ✅ Have |
@ -49,19 +49,20 @@ Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REF
| 1x BetaFPV ELRS 2.4GHz 1W TX module | ✅ Have — RC control + kill switch | | 1x BetaFPV ELRS 2.4GHz 1W TX module | ✅ Have — RC control + kill switch |
| 1x ELRS receiver (matching) | ✅ Have — mounts on FC UART | | 1x ELRS receiver (matching) | ✅ Have — mounts on FC UART |
### ESP32-S3 BALANCE Board Details — Waveshare ESP32-S3 Touch LCD 1.28 ### Drone FC Details — GEPRC GEP-F7 AIO
- **MCU:** ESP32-S3RET6 (Xtensa LX7 dual-core, 240MHz, 8MB Flash, 512KB SRAM) - **MCU:** STM32F722RET6 (216MHz Cortex-M7, 512KB flash, 256KB RAM)
- **IMU:** QMI8658 (6-axis, 32kHz gyro, ultra-low noise, SPI) ← the good one! - **IMU:** TDK ICM-42688-P (6-axis, 32kHz gyro, ultra-low noise, SPI) ← the good one!
- **Display:** 1.28" round LCD (GC9A01 driver, 240x240) - **Flash:** 8MB Winbond W25Q64 (blackbox, unused)
- **DFU mode:** Hold BOOT button while plugging USB - **OSD:** AT7456E (unused)
- **Firmware:** Custom balance firmware (ESP-IDF / Arduino-ESP32) - **4-in-1 ESC:** Built into AIO board (unused — we use hoverboard ESC)
- **USB:** USB Serial via CH343 chip - **DFU mode:** Hold yellow BOOT button while plugging USB
- **UART assignments:** - **Firmware:** Custom balance firmware (PlatformIO + STM32 HAL)
- UART0 → USB Serial (CH343) → debug/flash - **UART pads (confirmed from silkscreen):**
- UART1 → Jetson Orin Nano Super - T1/R1 (bottom) → USART1 (PA9/PA10) → Jetson
- UART2 → Hoverboard ESC - T2/R2 (right top) → USART2 (PA2/PA3) → Hoverboard ESC
- UART3 → ELRS receiver - T3/R3 (bottom) → USART3 (PB10/PB11) → ELRS receiver
- UART4/5 → spare - T4/R4 (bottom) → UART4 → spare
- T5/R5 (right bottom) → UART5 → spare
## Architecture ## Architecture
@ -73,7 +74,7 @@ Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REF
│ RealSense │ ← Forward-facing depth+RGB │ RealSense │ ← Forward-facing depth+RGB
│ D435i │ │ D435i │
├──────────────┤ ├──────────────┤
│ Jetson Orin Nano Super │ ← AI brain: navigation, person tracking │ Jetson Nano │ ← AI brain: navigation, person tracking
│ │ Sends velocity commands via UART │ │ Sends velocity commands via UART
├──────────────┤ ├──────────────┤
│ Drone FC │ ← Balance brain: IMU + PID @ 8kHz │ Drone FC │ ← Balance brain: IMU + PID @ 8kHz
@ -91,22 +92,145 @@ Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REF
└─────┘ └─────┘ └─────┘ └─────┘
``` ```
## Self-Balancing Control — ESP32-S3 BALANCE Board ## Self-Balancing Control — Custom Firmware on Drone FC
> For full system architecture, firmware details, and protocol specs, see ### Why a Drone FC?
> **docs/SAUL-TEE-SYSTEM-REFERENCE.md** The F745 board is just a premium STM32 dev board with a high-quality IMU (MPU-6000) already soldered on, proper voltage regulation, and multiple UARTs broken out. We write a lean custom balance firmware (~50 lines of C).
The balance controller runs on the Waveshare ESP32-S3 Touch LCD 1.28 board ### Architecture
(ESP32-S3 BALANCE). It reads the onboard QMI8658 IMU at 8kHz, runs a PID ```
balance loop, and drives the hoverboard ESC via UART. Jetson Orin Nano Super Jetson (speed+steer via UART1)
sends velocity commands over UART1. ELRS receiver on UART3 provides RC
override and kill-switch capability.
Drone FC (F745 + MPU-6000)
│ - Reads IMU @ 8kHz (SPI)
│ - Runs PID balance loop
│ - Mixes balance correction + Jetson commands
│ - Outputs speed+steer via UART2
Hoverboard ESC (FOC firmware)
│ - Receives UART commands
│ - Drives hub motors
Left + Right wheels
```
The legacy STM32 firmware (Mamba F722S era) has been archived to - **No motor outputs used** — FC talks UART directly to hoverboard ESC
======= - **Custom firmware only** — no third-party flight software
The legacy STM32 firmware (STM32 era) has been archived to - **Dead motor output irrelevant** — not using any PWM channels
`legacy/stm32/` and is no longer built or deployed.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only) ### Wiring
```
Jetson UART1 Drone FC (UART1)
──────────── ────────────────
TX (Pin 8) ──→ RX
RX (Pin 10) ──→ TX
GND ──→ GND
Drone FC (UART2) Hoverboard ESC
──────────────── ──────────────
TX ──→ RX (serial input)
GND ──→ GND
5V (BEC) ←── ESC 5V out (powers FC)
ELRS Receiver Drone FC (UART3)
───────────── ────────────────
TX ──→ RX
RX ←── TX (for telemetry/binding)
GND ──→ GND
5V ←── 5V
```
### Custom Firmware (STM32 C)
```c
// Core balance loop — runs in timer interrupt @ 1-8kHz
void balance_loop(void) {
// 1. Read pitch angle from MPU-6000 (complementary filter)
float pitch = get_pitch_angle(); // SPI read + filter
// 2. Get velocity command from Jetson (updated async via UART1 RX)
float target_speed = jetson_cmd.speed; // -1000 to 1000
float target_steer = jetson_cmd.steer; // -1000 to 1000
// 3. PID on pitch error
// Target angle shifts with speed command (lean forward = go forward)
float target_angle = target_speed * SPEED_TO_ANGLE_FACTOR;
float error = target_angle - pitch;
integral += error * dt;
integral = clamp(integral, -MAX_I, MAX_I); // anti-windup
float derivative = (error - prev_error) / dt;
prev_error = error;
float output = Kp * error + Ki * integral + Kd * derivative;
// 4. Mix balance + steering → hoverboard ESC UART command
int16_t left = clamp(output + target_steer, -1000, 1000);
int16_t right = clamp(output - target_steer, -1000, 1000);
// 5. Send to hoverboard ESC via UART2
send_hoverboard_cmd(left, right);
// 6. Safety: kill motors if tipped beyond recovery
if (fabs(pitch) > MAX_TILT_DEG) {
send_hoverboard_cmd(0, 0);
disarm();
}
// 7. Safety: RC kill switch (ELRS channel, checked every loop)
if (rc_channels.arm_switch == DISARMED) {
send_hoverboard_cmd(0, 0);
disarm();
}
// 8. Safety: kill if Jetson UART heartbeat lost
if (millis() - jetson_last_rx > JETSON_TIMEOUT_MS) {
send_hoverboard_cmd(0, 0);
disarm();
}
// 8. Safety: clamp output to max allowed speed
left = clamp(left, -max_speed_limit, max_speed_limit);
right = clamp(right, -max_speed_limit, max_speed_limit);
}
```
### Hoverboard ESC UART Protocol
```c
typedef struct {
uint16_t start; // 0xABCD
int16_t speed; // -1000 to 1000 (left)
int16_t steer; // -1000 to 1000 (right)
uint16_t checksum; // XOR of all bytes
} HoverboardCmd;
// 115200 baud, send at loop rate
```
### Jetson → FC Protocol (simple custom)
```c
typedef struct {
uint8_t header; // 0xAA
int16_t speed; // -1000 to 1000
int16_t steer; // -1000 to 1000
uint8_t mode; // 0=idle, 1=balance, 2=follow, 3=RC
uint8_t checksum;
} JetsonCmd;
// 115200 baud, ~50Hz from Jetson is plenty
```
### PID Tuning
| Param | Starting Value | Notes |
|-------|---------------|-------|
| Kp | 30-50 | Main balance response |
| Ki | 0.5-2 | Drift correction |
| Kd | 0.5-2 | Damping oscillation |
| Loop rate | 1-8 kHz | Start at 1kHz, increase if needed |
| Max tilt | ±25° | Beyond this = cut motors, require re-arm |
| JETSON_TIMEOUT_MS | 200 | Kill motors if Jetson stops talking |
| max_speed_limit | 100 | Start at 10% (100/1000), increase gradually |
| SPEED_TO_ANGLE_FACTOR | 0.01-0.05 | How much lean per speed unit |
## LED Subsystem (ESP32-C3) ## LED Subsystem (ESP32-C3)
@ -156,8 +280,8 @@ GND ──→ Common ground
``` ```
### Dev Tools ### Dev Tools
- **Flashing:** ESP32-S3CubeProgrammer via USB (DFU mode) or SWD - **Flashing:** STM32CubeProgrammer via USB (DFU mode) or SWD
- **IDE:** PlatformIO + ESP-IDF, or ESP32-S3CubeIDE - **IDE:** PlatformIO + STM32 HAL, or STM32CubeIDE
- **Debug:** SWD via ST-Link (or use FC's USB as virtual COM for printf debug) - **Debug:** SWD via ST-Link (or use FC's USB as virtual COM for printf debug)
## Physical Design ## Physical Design
@ -224,7 +348,7 @@ GND ──→ Common ground
## Software Stack ## Software Stack
### Jetson Orin Nano Super ### Jetson Nano
- **OS:** JetPack 4.6.1 (Ubuntu 18.04) - **OS:** JetPack 4.6.1 (Ubuntu 18.04)
- **ROS2 Humble** (or Foxy) for: - **ROS2 Humble** (or Foxy) for:
- `nav2` — navigation stack - `nav2` — navigation stack
@ -251,8 +375,8 @@ GND ──→ Common ground
- [ ] Install hardware kill switch inline with 36V battery (NC — press to kill) - [ ] Install hardware kill switch inline with 36V battery (NC — press to kill)
- [ ] Set up ceiling tether point above test area (rated for >15kg) - [ ] Set up ceiling tether point above test area (rated for >15kg)
- [ ] Clear test area: 3m radius, no loose items, shoes on - [ ] Clear test area: 3m radius, no loose items, shoes on
- [ ] Set up PlatformIO project for ESP32-S3 (ESP-IDF) - [ ] Set up PlatformIO project for STM32F745 (STM32 HAL)
- [ ] Write QMI8658 SPI driver (read gyro+accel, complementary filter) - [ ] Write MPU-6000 SPI driver (read gyro+accel, complementary filter)
- [ ] Write PID balance loop with ALL safety checks: - [ ] Write PID balance loop with ALL safety checks:
- ±25° tilt cutoff → disarm, require manual re-arm - ±25° tilt cutoff → disarm, require manual re-arm
- Watchdog timer (50ms hardware WDT) - Watchdog timer (50ms hardware WDT)

View File

@ -1,222 +0,0 @@
# SAUL-TEE System Reference — SaltyLab ESP32 Architecture
*Authoritative source of truth for hardware, pins, protocols, and CAN assignments.*
*Spec from hal@Orin, 2026-04-04.*
---
## Overview
| Board | Role | MCU | USB chip |
|-------|------|-----|----------|
| **ESP32-S3 BALANCE** | PID balance loop, CAN→VESCs, LCD display | ESP32-S3 | CH343 USB-serial |
| **ESP32-S3 IO** | RC input, motor drivers, sensors, LEDs, peripherals | ESP32-S3 | JTAG USB (native) |
**Robot form factor:** 4-wheel wagon — 870 × 510 × 550 mm, ~23 kg
**Power:** 36 V LiPo, DC-DC → 5 V and 12 V rails
**Orin connection:** CANable2 USB → 500 kbps CAN (same bus as VESCs)
---
## ESP32-S3 BALANCE
### Board
Waveshare ESP32-S3 Touch LCD 1.28
- GC9A01 round 240×240 LCD
- CST816S capacitive touch
- QMI8658 6-axis IMU (accel + gyro, SPI)
- CH343 USB-to-serial chip
### Pin Assignments
| Function | GPIO | Notes |
|----------|------|-------|
| **QMI8658 IMU (SPI)** | | |
| SCK | IO39 | |
| MOSI | IO38 | |
| MISO | IO40 | |
| CS | IO41 | |
| INT1 | IO42 | data-ready interrupt |
| **GC9A01 LCD (shares SPI bus)** | | |
| CS | IO12 | |
| DC | IO11 | |
| RST | IO10 | |
| BL | IO9 | PWM backlight |
| **CST816S Touch (I2C)** | | |
| SDA | IO4 | |
| SCL | IO5 | |
| INT | IO6 | |
| RST | IO7 | |
| **CAN — SN65HVD230 transceiver** | | 500 kbps |
| TX | IO43 | → SN65HVD230 TXD |
| RX | IO44 | ← SN65HVD230 RXD |
| **Inter-board UART (to IO board)** | | 460800 baud |
| TX | IO17 | |
| RX | IO18 | |
### Responsibilities
- Read QMI8658 @ 1 kHz (SPI, INT1-driven)
- Complementary filter → pitch angle
- PID balance loop (configurable Kp / Ki / Kd)
- Send VESC speed commands via CAN (ID 68 = left, ID 56 = right)
- Receive Orin velocity+mode commands via CAN (0x3000x303)
- Receive IO board status (arming, RC, faults) via UART protocol
- Drive GC9A01 LCD: pitch, speed, battery %, error state
- Enforce tilt cutoff at ±25°; IWDG 50 ms timeout
- Publish telemetry on CAN 0x4000x401 at 10 Hz
---
## ESP32-S3 IO
### Board
Bare ESP32-S3 devkit (JTAG USB)
### Pin Assignments
| Function | GPIO | Notes |
|----------|------|-------|
| **TBS Crossfire RC — UART0 (primary)** | | |
| RX | IO44 | CRSF frames from Crossfire RX |
| TX | IO43 | telemetry to Crossfire TX |
| **ELRS failover — UART2** | | active if CRSF absent >100 ms |
| RX | IO16 | |
| TX | IO17 | |
| **BTS7960 Motor Driver — Left** | | |
| RPWM | IO1 | forward PWM |
| LPWM | IO2 | reverse PWM |
| R_EN | IO3 | right enable |
| L_EN | IO4 | left enable |
| **BTS7960 Motor Driver — Right** | | |
| RPWM | IO5 | |
| LPWM | IO6 | |
| R_EN | IO7 | |
| L_EN | IO8 | |
| **I2C bus** | | |
| SDA | IO11 | |
| SCL | IO12 | |
| NFC (PN532 or similar) | I2C | |
| Barometer (BMP280/BMP388) | I2C | |
| ToF (VL53L0X/VL53L1X) | I2C | |
| **WS2812B LEDs** | | |
| Data | IO13 | |
| **Outputs** | | |
| Horn / buzzer | IO14 | PWM tone |
| Headlight | IO15 | PWM or digital |
| Fan | IO16 | (if ELRS not fitted on UART2) |
| **Inputs** | | |
| Arming button | IO9 | active-low, hold 3 s to arm |
| Kill switch sense | IO10 | hardware estop detect |
| **Inter-board UART (to BALANCE board)** | | 460800 baud |
| TX | IO18 | |
| RX | IO21 | |
### Responsibilities
- Parse CRSF frames (TBS Crossfire, primary)
- Parse ELRS frames (failover, activates if no CRSF for >100 ms)
- Drive BTS7960 left/right PWM motor drivers
- Read NFC, barometer, ToF via I2C
- Drive WS2812B LEDs (armed/fault/idle patterns)
- Control horn, headlight, fan, buzzer
- Manage arming: hold button 3 s while upright → send ARM to BALANCE
- Monitor kill switch input → immediate motor off + FAULT frame
- Forward RC + sensor data to BALANCE via binary UART protocol
- Report faults and RC-loss upstream
---
## Inter-Board Binary Protocol (UART @ 460800 baud)
```
[0xAA][LEN][TYPE][PAYLOAD × LEN bytes][CRC8]
```
- `0xAA` — start byte
- `LEN` — payload length in bytes (uint8)
- `TYPE` — message type (uint8)
- `CRC8` — CRC-8/MAXIM over TYPE + PAYLOAD bytes
### IO → BALANCE Messages
| TYPE | Name | Payload | Description |
|------|------|---------|-------------|
| 0x01 | RC_CMD | int16 throttle, int16 steer, uint8 flags | flags: bit0=armed, bit1=kill |
| 0x02 | SENSOR | uint16 tof_mm, int16 baro_delta_pa, uint8 nfc_present | |
| 0x03 | FAULT | uint8 fault_flags | bit0=rc_loss, bit1=motor_fault, bit2=estop |
### BALANCE → IO Messages
| TYPE | Name | Payload | Description |
|------|------|---------|-------------|
| 0x10 | STATE | int16 pitch_x100, int16 pid_out, uint8 error_state | |
| 0x11 | LED_CMD | uint8 pattern, uint8 r, uint8 g, uint8 b | |
| 0x12 | BUZZER | uint8 tone_id, uint16 duration_ms | |
---
## CAN Bus — 500 kbps
### Node Assignments
| Node | CAN ID | Role |
|------|--------|------|
| VESC Left motor | **68** | Receives speed/duty via VESC CAN protocol |
| VESC Right motor | **56** | Receives speed/duty via VESC CAN protocol |
| ESP32-S3 BALANCE | — | Sends VESC commands; publishes telemetry |
| Jetson Orin (CANable2) | — | Sends velocity commands; receives telemetry |
### Frame Table
| CAN ID | Direction | Description | Rate |
|--------|-----------|-------------|------|
| 0x300 | Orin → BALANCE | Velocity cmd: int16 speed_mmps, int16 steer_mrad | 20 Hz |
| 0x301 | Orin → BALANCE | PID tuning: float Kp, float Ki, float Kd (3×4B IEEE-754) | on demand |
| 0x302 | Orin → BALANCE | Mode: uint8 (0=off, 1=balance, 2=manual, 3=estop) | on demand |
| 0x303 | Orin → BALANCE | Config: uint16 tilt_limit_x100, uint16 max_speed_mmps | on demand |
| 0x400 | BALANCE → Orin | Telemetry A: int16 pitch_x100, int16 pid_out, int16 speed_mmps, uint8 state | 10 Hz |
| 0x401 | BALANCE → Orin | Telemetry B: int16 vesc_l_rpm, int16 vesc_r_rpm, uint16 battery_mv, uint8 faults | 10 Hz |
---
## RC Channel Mapping (TBS Crossfire / ELRS CRSF)
| CH | Function | Range (µs) | Notes |
|----|----------|------------|-------|
| 1 | Steer (Roll) | 9882012 | ±100% → ±max steer |
| 2 | Throttle (Pitch) | 9882012 | forward / back speed |
| 3 | Spare | 9882012 | |
| 4 | Spare | 9882012 | |
| 5 | ARM switch | <1500=disarm, >1500=arm | SB on TX |
| 6 | **ESTOP** | <1500=normal, >1500=kill | SC on TX — checked first every loop |
| 7 | Speed limit | 9882012 | maps to 10100% speed cap |
| 8 | Spare | | |
**RC loss:** No valid CRSF frame >100 ms → IO sends FAULT(rc_loss) → BALANCE cuts motors.
---
## Safety Invariants
1. **Motors NEVER spin on power-on** — 3 s button hold required while upright
2. **Tilt cutoff ±25°** — immediate motor zero, manual re-arm required
3. **IWDG 50 ms** — firmware hang → motors cut
4. **ESTOP RC channel** checked first in every loop iteration
5. **Orin CAN timeout 500 ms** → revert to RC-only mode
6. **Speed hard cap** — start at 10%, increase in 10% increments only after stable tethered testing
7. **Never untethered** until stable for 5+ continuous minutes tethered
---
## USB Debug Commands (both boards, serial console)
```
help list commands
status print pitch, PID state, CAN stats, UART stats
pid <Kp> <Ki> <Kd> set PID gains
arm arm (if upright and safe)
disarm disarm immediately
estop emergency stop (requires re-arm)
tilt_limit <deg> set tilt cutoff angle (default 25)
speed_limit <pct> set speed cap percentage (default 10)
can_stats CAN bus counters (tx/rx/errors/busoff)
uart_stats inter-board UART frame counters
reboot soft reboot
```

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>GEPRC GEP-F722-45A AIO — Board Layout (Legacy / Archived)</title> <title>GEPRC GEP-F722-45A AIO — Board Layout</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; color: #eee; font-family: 'Courier New', monospace; display: flex; flex-direction: column; align-items: center; padding: 20px; } body { background: #1a1a2e; color: #eee; font-family: 'Courier New', monospace; display: flex; flex-direction: column; align-items: center; padding: 20px; }
@ -112,13 +112,8 @@ h1 { color: #e94560; margin-bottom: 5px; font-size: 1.4em; }
</style> </style>
</head> </head>
<body> <body>
<<<<<<< HEAD
<h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout (Legacy / Archived)</h1>
<p class="subtitle">ESP32RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p>
=======
<h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout</h1> <h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout</h1>
<p class="subtitle">ESP32-S3RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p> <p class="subtitle">STM32F722RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p>
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
<div class="container"> <div class="container">
<div class="board-wrap"> <div class="board-wrap">
@ -130,11 +125,7 @@ h1 { color: #e94560; margin-bottom: 5px; font-size: 1.4em; }
<div class="mount br"></div> <div class="mount br"></div>
<!-- MCU --> <!-- MCU -->
<<<<<<< HEAD <div class="mcu"><div class="dot"></div>STM32<br>F722RET6<br>216MHz</div>
<div class="mcu"><div class="dot"></div>ESP32<br>(legacy:<br>F722RET6)</div>
=======
<div class="mcu"><div class="dot"></div>ESP32-S3<br>F722RET6<br>216MHz</div>
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
<!-- IMU --> <!-- IMU -->
<div class="imu">ICM<br>42688</div> <div class="imu">ICM<br>42688</div>
@ -215,7 +206,7 @@ h1 { color: #e94560; margin-bottom: 5px; font-size: 1.4em; }
<h2>🔌 UART Assignments</h2> <h2>🔌 UART Assignments</h2>
<div class="legend-item"> <div class="legend-item">
<div class="swatch" style="background:#2196F3"></div> <div class="swatch" style="background:#2196F3"></div>
<span><b>USART1</b> T1/R1 → Jetson Orin Nano Super</span> <span><b>USART1</b> T1/R1 → Jetson Nano</span>
</div> </div>
<div class="legend-item"> <div class="legend-item">
<div class="swatch" style="background:#FF9800"></div> <div class="swatch" style="background:#FF9800"></div>

View File

@ -1,155 +1,131 @@
# SaltyLab / SAUL-TEE Wiring Reference # SaltyLab Wiring Diagram
> ⚠️ **ARCHITECTURE CHANGE (2026-04-03):** Mamba F722S / STM32 retired. ## System Overview
> New stack: **ESP32-S3 BALANCE** + **ESP32-S3 IO** + VESCs on 500 kbps CAN.
> **Authoritative reference:** [`docs/SAUL-TEE-SYSTEM-REFERENCE.md`](SAUL-TEE-SYSTEM-REFERENCE.md)
> Historical STM32/Mamba wiring below is **obsolete** — retained for reference only.
---
## ~~System Overview~~ (OBSOLETE — see SAUL-TEE-SYSTEM-REFERENCE.md)
``` ```
┌─────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────┐
│ ORIN NANO SUPER │ │ ORIN NANO SUPER │
│ (Top Plate — 25W) │ │ (Top Plate — 25W) │
│ │ │ │
<<<<<<< HEAD │ USB-C ──── STM32 CDC (/dev/stm32-bridge, 921600 baud) │
│ USB-A ──── CANable2 USB-CAN adapter (slcan0, 500 kbps) │
│ USB-A ──── ESP32-S3 IO (/dev/esp32-io, 460800 baud) │
=======
│ USB-C ──── ESP32-S3 CDC (/dev/esp32-bridge, 921600 baud) │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ USB-A1 ─── RealSense D435i (USB 3.1) │ │ USB-A1 ─── RealSense D435i (USB 3.1) │
│ USB-A2 ─── RPLIDAR A1M8 (via CP2102 adapter, 115200) │ │ USB-A2 ─── RPLIDAR A1M8 (via CP2102 adapter, 115200) │
│ USB-C* ─── SIM7600A 4G/LTE modem (ttyUSB0-2, AT cmds + PPP) │ │ USB-C* ─── SIM7600A 4G/LTE modem (ttyUSB0-2, AT cmds + PPP) │
│ USB ─────── Leap Motion Controller (hand/gesture tracking) │ │ USB ─────── Leap Motion Controller (hand/gesture tracking) │
│ CSI-A ──── ArduCam adapter → 2x IMX219 (front + left) │ │ CSI-A ──── ArduCam adapter → 2× IMX219 (front + left) │
│ CSI-B ──── ArduCam adapter → 2x IMX219 (rear + right) │ │ CSI-B ──── ArduCam adapter → 2× IMX219 (rear + right) │
│ M.2 ───── 1TB NVMe SSD │ │ M.2 ───── 1TB NVMe SSD │
│ 40-pin ─── ReSpeaker 2-Mic HAT (I2S + I2C, WM8960 codec) │ │ 40-pin ─── ReSpeaker 2-Mic HAT (I2S + I2C, WM8960 codec) │
│ Pin 8 ──┐ │ │ Pin 8 ──┐ │
│ Pin 10 ─┤ UART fallback to ESP32-S3 BALANCE (ttyTHS0, 460800) │ Pin 10 ─┤ UART fallback to FC (ttyTHS0, 921600)
│ Pin 6 ──┘ GND │ │ Pin 6 ──┘ GND │
│ │ │ │
└─────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────┘
│ USB-A (CANable2) │ UART fallback (3 wires) │ USB-C (data only) │ UART fallback (3 wires)
│ SocketCAN slcan0 │ 460800 baud, 3.3V │ 921600 baud │ 921600 baud, 3.3V
│ 500 kbps │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────┐
<<<<<<< HEAD │ MAMBA F722S (FC) │
│ ESP32-S3 BALANCE │
│ (Waveshare Touch LCD 1.28, Middle Plate) │
=======
│ ESP32-S3 BALANCE (FC) │
│ (Middle Plate — foam mounted) │ │ (Middle Plate — foam mounted) │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │ │ │
│ CAN bus ──── CANable2 → Orin (primary link, ISO 11898) │ │ USB-C ──── Orin (CDC serial, primary link) │
│ UART0 ──── Orin UART fallback (460800 baud, 3.3V) │ │ │
│ UART1 ──── VESC Left (CAN ID 56) via UART/CAN bridge │ │ USART2 (PA2=TX, PA3=RX) ──── Hoverboard ESC (26400 baud) │
│ UART2 ──── VESC Right (CAN ID 68) via UART/CAN bridge │ │ UART4 (PA0=TX, PA1=RX) ──── ELRS RX (CRSF, 420000 baud) │
│ I2C ──── QMI8658 IMU (onboard, 6-DOF accel+gyro) │ │ USART6 (PC6=TX, PC7=RX) ──── Orin UART fallback │
│ SPI ──── GC9A01 LCD (onboard, 240x240 round display) │ │ UART5 (PC12=TX, PD2=RX) ─── Debug (optional) │
│ GPIO ──── WS2812B LED strip │ │ │
│ GPIO ──── Buzzer │ │ SPI1 ─── MPU6000 IMU (on-board, CW270) │
│ ADC ──── Battery voltage divider │ │ I2C1 ─── BMP280 baro (on-board, disabled) │
│ ADC ──── Battery voltage (PC1) + Current (PC3) │
│ PB3 ──── WS2812B LED strip │
│ PB2 ──── Buzzer │
│ │ │ │
└─────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────┘
│ CAN bus (ISO 11898) │ UART (460800 baud) │ USART2 │ UART4
│ 500 kbps │ │ PA2=TX → ESC RX │ PA0=TX → ELRS TX
│ PA3=RX ← ESC TX │ PA1=RX ← ELRS RX
│ GND ─── GND │ GND ─── GND
▼ ▼ ▼ ▼
┌────────────────────────┐ ┌──────────────────────────┐ ┌────────────────────────┐ ┌──────────────────────────┐
│ VESC Left (ID 56) │ │ VESC Right (ID 68) │ │ HOVERBOARD ESC │ │ ELRS 2.4GHz RX │
│ (Bottom Plate) │ │ (Bottom Plate) │ │ (Bottom Plate) │ │ (beside FC) │
│ │ │ │
│ BLDC hub motor │ │ BLDC hub motor │
│ CAN 500 kbps │ │ CAN 500 kbps │
│ FOC current control │ │ FOC current control │
│ VESC Status 1 (0x900) │ │ VESC Status 1 (0x910) │
│ │ │ │ │ │ │ │
│ 2× BLDC hub motors │ │ CRSF protocol │
│ 26400 baud UART │ │ 420000 baud │
│ Frame: [0xABCD] │ │ BetaFPV 1W TX → RX │
│ [steer][speed][csum] │ │ CH3=speed CH4=steer │
│ │ │ CH5=arm CH6=mode │
└────────────────────────┘ └──────────────────────────┘ └────────────────────────┘ └──────────────────────────┘
│ │
LEFT MOTOR RIGHT MOTOR ┌────┴────┐
``` ▼ ▼
🛞 LEFT RIGHT 🛞
MOTOR MOTOR
## Wire-by-Wire Connections ## Wire-by-Wire Connections
<<<<<<< HEAD ### 1. Orin ↔ FC (Primary: USB CDC)
### 1. Orin <-> ESP32-S3 BALANCE (Primary: CAN Bus via CANable2)
=======
### 1. Orin ↔ FC (Primary: USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| From | To | Wire | Notes | | From | To | Wire Color | Notes |
|------|----|------|-------| |------|----|-----------|-------|
| Orin USB-A | CANable2 USB | USB cable | SocketCAN slcan0 @ 500 kbps | | Orin USB-C port | FC USB-C port | USB cable | Data only, FC powered from 5V bus |
| CANable2 CAN-H | ESP32-S3 BALANCE CAN-H | twisted pair | ISO 11898 differential |
| CANable2 CAN-L | ESP32-S3 BALANCE CAN-L | twisted pair | ISO 11898 differential |
<<<<<<< HEAD - Device: `/dev/ttyACM0` → symlink `/dev/stm32-bridge`
- Interface: SocketCAN `slcan0`, 500 kbps
- Device node: `/dev/canable2` (via udev, symlink to ttyUSBx)
- Protocol: CAN frames --- ORIN_CMD_DRIVE (0x300), ORIN_CMD_MODE (0x301), ORIN_CMD_ESTOP (0x302)
- Telemetry: BALANCE_STATUS (0x400), BALANCE_VESC (0x401), BALANCE_IMU (0x402), BALANCE_BATTERY (0x403)
=======
- Device: `/dev/ttyACM0` → symlink `/dev/esp32-bridge`
- Baud: 921600, 8N1 - Baud: 921600, 8N1
- Protocol: JSON telemetry (FC→Orin), ASCII commands (Orin→FC) - Protocol: JSON telemetry (FC→Orin), ASCII commands (Orin→FC)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 2. Orin <-> ESP32-S3 BALANCE (Fallback: Hardware UART) ### 2. Orin ↔ FC (Fallback: Hardware UART)
| Orin Pin | Signal | ESP32-S3 Pin | Notes | | Orin Pin | Signal | FC Pin | FC Signal |
|----------|--------|--------------|-------| |----------|--------|--------|-----------|
| Pin 8 | TXD0 | GPIO17 (UART0 RX) | Orin TX -> BALANCE RX | | Pin 8 | TXD0 | PC7 | USART6 RX |
| Pin 10 | RXD0 | GPIO18 (UART0 TX) | Orin RX <- BALANCE TX | | Pin 10 | RXD0 | PC6 | USART6 TX |
| Pin 6 | GND | GND | Common ground | | Pin 6 | GND | GND | GND |
- Jetson device: `/dev/ttyTHS0` - Jetson device: `/dev/ttyTHS0`
- Baud: 460800, 8N1 - Baud: 921600, 8N1
- Voltage: 3.3V both sides (no level shifter needed) - Voltage: 3.3V both sides (no level shifter needed)
- Cross-connect: Orin TX -> BALANCE RX, Orin RX <- BALANCE TX - **Cross-connect:** Orin TX → FC RX, Orin RX ← FC TX
### 3. Orin <-> ESP32-S3 IO (USB Serial) ### 3. FC ↔ Hoverboard ESC
| From | To | Notes | | FC Pin | Signal | ESC Pin | Notes |
|------|----|-------| |--------|--------|---------|-------|
| Orin USB-A | ESP32-S3 IO USB-C | USB cable, /dev/esp32-io | | PA2 | USART2 TX | RX | FC sends speed/steer commands |
| PA3 | USART2 RX | TX | ESC sends feedback (optional) |
- Device node: `/dev/esp32-io` (udev symlink)
- Baud: 460800, 8N1
- Protocol: Binary frames `[0xAA][LEN][TYPE][PAYLOAD][CRC8]`
- Use: IO expansion, GPIO control, sensor polling
### 4. ESP32-S3 BALANCE <-> VESC Motors (CAN Bus)
| BALANCE Pin | Signal | VESC Pin | Notes |
|-------------|--------|----------|-------|
| GPIO21 | CAN-H | CAN-H | ISO 11898 differential pair |
| GPIO22 | CAN-L | CAN-L | ISO 11898 differential pair |
| GND | GND | GND | Common ground | | GND | GND | GND | Common ground |
- Baud: 500 kbps CAN - Baud: 26400, 8N1
- VESC Left: CAN ID 56, VESC Right: CAN ID 68 - Protocol: Binary frame — `[0xABCD][steer:int16][speed:int16][checksum:uint16]`
- Commands: COMM_SET_RPM, COMM_SET_CURRENT, COMM_SET_DUTY - Speed range: -1000 to +1000
- Telemetry: VESC Status 1 at 50 Hz (RPM, current, duty) - **Keep wires short and twisted** (EMI from ESC)
### 4. FC ↔ ELRS Receiver
| FC Pin | Signal | ELRS Pin | Notes |
|--------|--------|----------|-------|
| PA0 | UART4 TX | RX | Telemetry to TX (optional) |
| PA1 | UART4 RX | TX | CRSF frames from RX |
| GND | GND | GND | Common ground |
| 5V | — | VCC | Power ELRS from 5V bus |
- Baud: 420000 (CRSF protocol)
- Failsafe: disarm after 300ms without frame
### 5. Power Distribution ### 5. Power Distribution
``` ```
BATTERY (36V) ──┬── VESC Left (36V direct -> BLDC left motor) BATTERY (36V) ──┬── Hoverboard ESC (36V direct)
├── VESC Right (36V direct -> BLDC right motor)
├── 5V BEC/regulator ──┬── Orin (USB-C PD or barrel jack) ├── 5V BEC/regulator ──┬── Orin (USB-C PD or barrel jack)
│ ├── ESP32-S3 BALANCE (5V via USB-C) │ ├── FC (via USB or 5V pad)
│ ├── ESP32-S3 IO (5V via USB-C) │ ├── ELRS RX (5V)
│ ├── WS2812B LEDs (5V) │ ├── WS2812B LEDs (5V)
│ └── RPLIDAR (5V via USB) │ └── RPLIDAR (5V via USB)
└── Battery monitor ──── ESP32-S3 BALANCE ADC (voltage divider) └── Battery monitor ──── FC ADC (PC1=voltage, PC3=current)
``` ```
### 6. Sensors on Orin (USB/CSI) ### 6. Sensors on Orin (USB/CSI)
@ -160,39 +136,10 @@ BATTERY (36V) ──┬── VESC Left (36V direct -> BLDC left motor)
| RPLIDAR A1M8 | USB-UART | USB-A | `/dev/rplidar` | | RPLIDAR A1M8 | USB-UART | USB-A | `/dev/rplidar` |
| IMX219 front+left | MIPI CSI-2 | CSI-A (J5) | `/dev/video0,2` | | IMX219 front+left | MIPI CSI-2 | CSI-A (J5) | `/dev/video0,2` |
| IMX219 rear+right | MIPI CSI-2 | CSI-B (J8) | `/dev/video4,6` | | IMX219 rear+right | MIPI CSI-2 | CSI-B (J8) | `/dev/video4,6` |
| 1TB NVMe | PCIe Gen3 x4 | M.2 Key M | `/dev/nvme0n1` | | 1TB NVMe | PCIe Gen3 ×4 | M.2 Key M | `/dev/nvme0n1` |
| CANable2 | USB-CAN | USB-A | `/dev/canable2` -> `slcan0` |
<<<<<<< HEAD ## FC UART Summary (MAMBA F722S)
## FC UART Summary (MAMBA F722S — OBSOLETE)
| Interface | Pins | Baud/Rate | Assignment | Notes |
|-----------|------|-----------|------------|-------|
| UART0 | GPIO17=RX, GPIO18=TX | 460800 | Orin UART fallback | 3.3V, cross-connect |
| UART1 | GPIO19=RX, GPIO20=TX | 115200 | Debug serial | Optional |
| CAN (TWAI) | GPIO21=H, GPIO22=L | 500 kbps | CAN bus (VESCs + Orin) | SN65HVD230 transceiver |
| I2C | GPIO4=SDA, GPIO5=SCL | 400 kHz | QMI8658 IMU (addr 0x6B) | Onboard |
| SPI | GPIO36=MOSI, GPIO37=SCLK, GPIO35=CS | 40 MHz | GC9A01 LCD (onboard) | 240x240 round |
| USB CDC | USB-C | 460800 | Orin USB fallback | /dev/esp32-balance |
## CAN Frame ID Map
| CAN ID | Direction | Name | Contents |
|--------|-----------|------|----------|
| 0x300 | Orin -> BALANCE | ORIN_CMD_DRIVE | left_rpm_f32, right_rpm_f32 (8 bytes LE) |
| 0x301 | Orin -> BALANCE | ORIN_CMD_MODE | mode byte (0=IDLE, 1=DRIVE, 2=ESTOP) |
| 0x302 | Orin -> BALANCE | ORIN_CMD_ESTOP | flags byte (bit0=stop, bit1=clear) |
| 0x400 | BALANCE -> Orin | BALANCE_STATUS | pitch x10:i16, motor_cmd:u16, vbat_mv:u16, state:u8, flags:u8 |
| 0x401 | BALANCE -> Orin | BALANCE_VESC | l_rpm x10:i16, r_rpm x10:i16, l_cur x10:i16, r_cur x10:i16 |
| 0x402 | BALANCE -> Orin | BALANCE_IMU | pitch x100:i16, roll x100:i16, yaw x100:i16, ax x100:i16, ay x100:i16, az x100:i16 |
| 0x403 | BALANCE -> Orin | BALANCE_BATTERY | vbat_mv:u16, current_ma:i16, soc_pct:u8 |
| 0x900+ID | VESC Left -> | VESC_STATUS_1 | erpm:i32, current x10:i16, duty x1000:i16 |
| 0x910+ID | VESC Right -> | VESC_STATUS_1 | erpm:i32, current x10:i16, duty x1000:i16 |
VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
=======
## FC UART Summary (ESP32-S3 BALANCE)
| UART | Pins | Baud | Assignment | Notes | | UART | Pins | Baud | Assignment | Notes |
|------|------|------|------------|-------| |------|------|------|------------|-------|
@ -202,8 +149,7 @@ VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
| UART4 | PA0=TX, PA1=RX | 420000 | ELRS RX (CRSF) | RC control | | UART4 | PA0=TX, PA1=RX | 420000 | ELRS RX (CRSF) | RC control |
| UART5 | PC12=TX, PD2=RX | 115200 | Debug serial | Optional | | UART5 | PC12=TX, PD2=RX | 115200 | Debug serial | Optional |
| USART6 | PC6=TX, PC7=RX | 921600 | Jetson UART | Fallback link | | USART6 | PC6=TX, PC7=RX | 921600 | Jetson UART | Fallback link |
| USB Serial (CH343) | USB-C | 921600 | Jetson primary | `/dev/esp32-bridge` | | USB CDC | USB-C | 921600 | Jetson primary | `/dev/stm32-bridge` |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 7. ReSpeaker 2-Mic HAT (on Orin 40-pin header) ### 7. ReSpeaker 2-Mic HAT (on Orin 40-pin header)
@ -221,63 +167,57 @@ VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
| Pin 2, 4 | 5V | Power | | Pin 2, 4 | 5V | Power |
| Pin 6, 9 | GND | Ground | | Pin 6, 9 | GND | Ground |
- Codec: Wolfson WM8960 (I2C addr 0x1A) - **Codec:** Wolfson WM8960 (I2C addr 0x1A)
- Mics: 2x MEMS (left + right) --- basic stereo / sound localization - **Mics:** 2× MEMS (left + right) — basic stereo / sound localization
- Speaker: 3W class-D amp output (JST connector) - **Speaker:** 3W class-D amp output (JST connector)
- Headset: 3.5mm TRRS jack - **Headset:** 3.5mm TRRS jack
- Requires: WM8960 device tree overlay for Jetson (community port) - **Requires:** WM8960 device tree overlay for Jetson (community port)
- Use: Voice commands (faster-whisper), wake word (openWakeWord), audio feedback, status announcements - **Use:** Voice commands (faster-whisper), wake word (openWakeWord), audio feedback, status announcements
### 8. SIM7600A 4G/LTE HAT (via USB) ### 8. SIM7600A 4G/LTE HAT (via USB)
| Connection | Detail | | Connection | Detail |
|-----------|--------| |-----------|--------|
| Interface | USB (micro-B on HAT -> USB-A/C on Orin) | | Interface | USB (micro-B on HAT USB-A/C on Orin) |
| Device nodes | `/dev/ttyUSB0` (AT), `/dev/ttyUSB1` (PPP/data), `/dev/ttyUSB2` (GPS NMEA) | | Device nodes | `/dev/ttyUSB0` (AT), `/dev/ttyUSB1` (PPP/data), `/dev/ttyUSB2` (GPS NMEA) |
| Power | 5V from USB or separate 5V supply (peak 2A during TX) | | Power | 5V from USB or separate 5V supply (peak 2A during TX) |
| SIM | Nano-SIM slot on HAT | | SIM | Nano-SIM slot on HAT |
| Antenna | 4G LTE + GPS/GNSS (external SMA antennas --- mount high on chassis) | | Antenna | 4G LTE + GPS/GNSS (external SMA antennas mount high on chassis) |
- Data: PPP or QMI for internet connectivity - **Data:** PPP or QMI for internet connectivity
- GPS/GNSS: Built-in receiver, NMEA sentences on ttyUSB2 --- outdoor positioning - **GPS/GNSS:** Built-in receiver, NMEA sentences on ttyUSB2 — outdoor positioning
- AT commands: `AT+CGPS=1` (enable GPS), `AT+CGPSINFO` (get fix) - **AT commands:** `AT+CGPS=1` (enable GPS), `AT+CGPSINFO` (get fix)
- Connected via USB (not 40-pin) --- avoids UART conflict with BALANCE fallback, flexible antenna placement - **Connected via USB** (not 40-pin) — avoids UART conflict with FC fallback, flexible antenna placement
- Use: Remote telemetry, 4G connectivity outdoors, GPS positioning, remote SSH/control - **Use:** Remote telemetry, 4G connectivity outdoors, GPS positioning, remote SSH/control
### 9. Leap Motion Controller (USB) ### 10. Leap Motion Controller (USB)
| Connection | Detail | | Connection | Detail |
|-----------|--------| |-----------|--------|
| Interface | USB 3.0 (micro-B on controller -> USB-A on Orin) | | Interface | USB 3.0 (micro-B on controller USB-A on Orin) |
| Power | ~0.5W | | Power | ~0.5W |
| Range | ~80cm, 150 deg FOV | | Range | ~80cm, 150° FOV |
| SDK | Ultraleap Gemini V5+ (Linux ARM64 support) | | SDK | Ultraleap Gemini V5+ (Linux ARM64 support) |
| ROS2 | `leap_motion_ros2` wrapper available | | ROS2 | `leap_motion_ros2` wrapper available |
- 2x IR cameras + 3x IR LEDs --- tracks all 10 fingers in 3D, sub-mm precision - **2× IR cameras + 3× IR LEDs** tracks all 10 fingers in 3D, sub-mm precision
- Mount: Forward-facing on sensor tower or upward on Orin plate - **Mount:** Forward-facing on sensor tower or upward on Orin plate
- Use: Gesture control (palm=stop, point=go, fist=arm), hand-following mode, demos - **Use:** Gesture control (palm=stop, point=go, fist=arm), hand-following mode, demos
- Combined with ReSpeaker: Voice + gesture control with zero hardware in hand - **Combined with ReSpeaker:** Voice + gesture control with zero hardware in hand
### 10. Power Budget (USB) ### 11. Power Budget (USB)
| Device | Interface | Power Draw | | Device | Interface | Power Draw |
|--------|-----------|------------| |--------|-----------|------------|
<<<<<<< HEAD | STM32 FC (CDC) | USB-C | ~0.5W (data only, FC on 5V bus) |
| CANable2 USB-CAN | USB-A | ~0.5W |
| ESP32-S3 BALANCE | USB-C | ~0.8W (WiFi off) |
| ESP32-S3 IO | USB-C | ~0.5W |
=======
| ESP32-S3 FC (CDC) | USB-C | ~0.5W (data only, FC on 5V bus) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| RealSense D435i | USB-A | ~1.5W (3.5W peak) | | RealSense D435i | USB-A | ~1.5W (3.5W peak) |
| RPLIDAR A1M8 | USB-A | ~2.6W (motor on) | | RPLIDAR A1M8 | USB-A | ~2.6W (motor on) |
| SIM7600A | USB | ~1W idle, 3W TX peak | | SIM7600A | USB | ~1W idle, 3W TX peak |
| Leap Motion | USB-A | ~0.5W | | Leap Motion | USB | ~0.5W |
| ReSpeaker HAT | 40-pin | ~0.5W | | ReSpeaker HAT | 40-pin | ~0.5W |
| **Total USB** | | **~7.9W typical, ~11W peak** | | **Total USB** | | **~6.5W typical, ~10.5W peak** |
Orin Nano Super delivers up to 25W --- USB peripherals are well within budget. Orin Nano Super delivers up to 25W USB peripherals are well within budget.
--- ---
@ -285,46 +225,38 @@ Orin Nano Super delivers up to 25W --- USB peripherals are well within budget.
``` ```
┌──────────────┐ ┌──────────────┐
RC TX │ (in your hand) ELRS TX │ (in your hand)
│ (2.4GHz) │ │ (2.4GHz) │
└──────┬───────┘ └──────┬───────┘
│ radio │ radio
┌──────▼───────┐ ┌──────▼───────┐
RC RX │ CRSF 420kbaud (future) ELRS RX │ CRSF 420kbaud
└──────┬───────┘ └──────┬───────┘
│ UART │ UART4
┌────────────▼────────────┐ ┌────────────▼────────────┐
<<<<<<< HEAD │ MAMBA F722S │
│ ESP32-S3 BALANCE │
│ (Waveshare LCD 1.28) │
=======
│ ESP32-S3 BALANCE │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │ │ │
QMI8658 -> Balance PID │ MPU6000 → Balance PID │
RC -> Mode Manager │ CRSF → Mode Manager │
│ Safety Monitor │ │ Safety Monitor │
│ │ │ │
└──┬──────────┬───────────┘ └──┬──────────┬───────────┘
<<<<<<< HEAD USART2 ─────┘ └───── USB CDC / USART6
CAN 500kbps─┘ └───── CAN bus / UART fallback
=======
USART2 ─────┘ └───── USB Serial (CH343) / USART6
26400 baud 921600 baud 26400 baud 921600 baud
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │ │ │
┌────┴────────────┐ ▼ ▼
│ CAN bus (500k) │ ┌───────────────────┐ ┌────────────────┐ ┌───────────────────┐
├─ VESC Left 56 │ │ Orin Nano Super │ │ Hoverboard ESC │ │ Orin Nano Super │
└─ VESC Right 68 │ │ │ │ │ │ │
│ │ │ SLAM / Nav2 / AI │ │ L motor R motor│ │ SLAM / Nav2 / AI │
▼ ▼ │ Person following │ │ 🛞 🛞 │ │ Person following │
LEFT RIGHT │ Voice commands │ └────────────────┘ │ Voice commands │
MOTOR MOTOR │ 4G telemetry │ │ 4G telemetry │
└──┬──────────┬───────┘ └──┬──────────┬───────┘
│ │ │ │
┌──────────▼─┐ ┌────▼──────────┐ ┌──────────▼─┐ ┌────▼──────────┐
│ ReSpeaker │ │ SIM7600A │ │ ReSpeaker │ │ SIM7600A │
│ 2-Mic HAT │ │ 4G/LTE + GPS │ │ 2-Mic HAT │ │ 4G/LTE + GPS │
│ 🎤 🔊 │ │ 📡 🛰️ │
└────────────┘ └───────────────┘ └────────────┘ └───────────────┘
``` ```

View File

@ -1,3 +0,0 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32s3_balance)

View File

@ -1,22 +0,0 @@
idf_component_register(
SRCS
"main.c"
"orin_serial.c"
"vesc_can.c"
"gitea_ota.c"
"ota_self.c"
"uart_ota.c"
"ota_display.c"
INCLUDE_DIRS "."
REQUIRES
esp_wifi
esp_http_client
esp_https_ota
nvs_flash
app_update
mbedtls
cJSON
driver
freertos
esp_timer
)

View File

@ -1,42 +0,0 @@
#pragma once
/* ── ESP32-S3 BALANCE board — bd-66hx pin/config definitions ───────────────
*
* Hardware change from pre-bd-66hx design:
* Previously: IO43/IO44 = CAN SN65HVD230 (shared Orin+VESC bus via CANable2)
* After bd-66hx: IO43/IO44 = CH343 UART0 (Orin serial comms)
* IO2/IO1 = CAN SN65HVD230 rewired (VESC-only bus)
*
* The SN65HVD230 transceiver physical wiring must be updated from IO43/44
* to IO2/IO1 when deploying this firmware. See docs/SAUL-TEE-SYSTEM-REFERENCE.md.
*/
/* ── Orin serial (CH343 USB-to-UART, 1a86:55d3 on Orin side) ── */
#define ORIN_UART_PORT UART_NUM_0
#define ORIN_UART_BAUD 460800
#define ORIN_UART_TX_GPIO 43 /* ESP32→CH343 RXD */
#define ORIN_UART_RX_GPIO 44 /* CH343 TXD→ESP32 */
#define ORIN_UART_RX_BUF 1024
#define ORIN_TX_QUEUE_DEPTH 16
/* ── VESC CAN TWAI (SN65HVD230 transceiver, rewired for bd-66hx) ── */
#define VESC_CAN_TX_GPIO 2 /* ESP32 TWAI TX → SN65HVD230 TXD */
#define VESC_CAN_RX_GPIO 1 /* SN65HVD230 RXD → ESP32 TWAI RX */
#define VESC_CAN_RX_QUEUE 32
/* VESC node IDs — matched to bd-wim1 TELEM_VESC_LEFT/RIGHT mapping */
#define VESC_ID_A 56u /* TELEM_VESC_LEFT (0x81) */
#define VESC_ID_B 68u /* TELEM_VESC_RIGHT (0x82) */
/* ── Safety / timing ── */
#define HB_TIMEOUT_MS 500u /* heartbeat watchdog: disarm if exceeded */
#define DRIVE_TIMEOUT_MS 500u /* drive command staleness timeout */
#define TELEM_STATUS_PERIOD_MS 100u /* 10 Hz status telemetry to Orin */
#define TELEM_VESC_PERIOD_MS 100u /* 10 Hz VESC telemetry to Orin */
/* ── Drive → VESC RPM scaling ── */
#define RPM_PER_SPEED_UNIT 5 /* speed_units=1000 → 5000 ERPM */
#define RPM_PER_STEER_UNIT 3 /* steer differential scale */
/* ── Tilt cutoff ── */
#define TILT_CUTOFF_DEG 25.0f

View File

@ -1,285 +0,0 @@
/* gitea_ota.c — Gitea version checker (bd-3hte)
*
* Uses esp_http_client + cJSON to query:
* GET /api/v1/repos/{repo}/releases?limit=10
* Filters releases by tag prefix, extracts version and download URLs.
*/
#include "gitea_ota.h"
#include "version.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_http_client.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "cJSON.h"
#include <string.h>
#include <stdio.h>
static const char *TAG = "gitea_ota";
ota_update_info_t g_balance_update = {0};
ota_update_info_t g_io_update = {0};
/* ── WiFi connection ── */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
#define WIFI_MAX_RETRIES 5
/* Compile-time WiFi fallback (override in NVS "wifi"/"ssid","pass") */
#define DEFAULT_WIFI_SSID "saltylab"
#define DEFAULT_WIFI_PASS ""
static EventGroupHandle_t s_wifi_eg;
static int s_wifi_retries = 0;
static void wifi_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_wifi_retries < WIFI_MAX_RETRIES) {
esp_wifi_connect();
s_wifi_retries++;
} else {
xEventGroupSetBits(s_wifi_eg, WIFI_FAIL_BIT);
}
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
s_wifi_retries = 0;
xEventGroupSetBits(s_wifi_eg, WIFI_CONNECTED_BIT);
}
}
static bool wifi_connect(void)
{
char ssid[64] = DEFAULT_WIFI_SSID;
char pass[64] = DEFAULT_WIFI_PASS;
/* Try to read credentials from NVS */
nvs_handle_t nvs;
if (nvs_open("wifi", NVS_READONLY, &nvs) == ESP_OK) {
size_t sz = sizeof(ssid);
nvs_get_str(nvs, "ssid", ssid, &sz);
sz = sizeof(pass);
nvs_get_str(nvs, "pass", pass, &sz);
nvs_close(nvs);
}
s_wifi_eg = xEventGroupCreate();
s_wifi_retries = 0;
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t h1, h2;
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, &h1));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, &h2));
wifi_config_t wcfg = {0};
strlcpy((char *)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid));
strlcpy((char *)wcfg.sta.password, pass, sizeof(wcfg.sta.password));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wcfg));
ESP_ERROR_CHECK(esp_wifi_start());
EventBits_t bits = xEventGroupWaitBits(s_wifi_eg,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE,
pdMS_TO_TICKS(15000));
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, h2);
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, h1);
vEventGroupDelete(s_wifi_eg);
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "WiFi connected SSID=%s", ssid);
return true;
}
ESP_LOGW(TAG, "WiFi connect failed SSID=%s", ssid);
return false;
}
/* ── HTTP fetch into a heap buffer ── */
#define HTTP_RESP_MAX (8 * 1024)
typedef struct { char *buf; int len; int cap; } http_buf_t;
static esp_err_t http_event_cb(esp_http_client_event_t *evt)
{
http_buf_t *b = (http_buf_t *)evt->user_data;
if (evt->event_id == HTTP_EVENT_ON_DATA && b) {
if (b->len + evt->data_len < b->cap) {
memcpy(b->buf + b->len, evt->data, evt->data_len);
b->len += evt->data_len;
}
}
return ESP_OK;
}
static char *http_get(const char *url)
{
char *buf = malloc(HTTP_RESP_MAX);
if (!buf) return NULL;
http_buf_t b = {.buf = buf, .len = 0, .cap = HTTP_RESP_MAX};
buf[0] = '\0';
esp_http_client_config_t cfg = {
.url = url,
.event_handler = http_event_cb,
.user_data = &b,
.timeout_ms = GITEA_API_TIMEOUT_MS,
.skip_cert_common_name_check = true,
};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
esp_err_t err = esp_http_client_perform(client);
int status = esp_http_client_get_status_code(client);
esp_http_client_cleanup(client);
if (err != ESP_OK || status != 200) {
ESP_LOGW(TAG, "HTTP GET %s → err=%d status=%d", url, err, status);
free(buf);
return NULL;
}
buf[b.len] = '\0';
return buf;
}
/* ── Version comparison: returns true if remote > local ── */
static bool version_newer(const char *local, const char *remote)
{
int la=0,lb=0,lc=0, ra=0,rb=0,rc=0;
sscanf(local, "%d.%d.%d", &la, &lb, &lc);
sscanf(remote, "%d.%d.%d", &ra, &rb, &rc);
if (ra != la) return ra > la;
if (rb != lb) return rb > lb;
return rc > lc;
}
/* ── Parse releases JSON array, fill ota_update_info_t ── */
static void parse_releases(const char *json, const char *tag_prefix,
const char *bin_asset, const char *sha_asset,
const char *local_version,
ota_update_info_t *out)
{
cJSON *arr = cJSON_Parse(json);
if (!arr || !cJSON_IsArray(arr)) {
ESP_LOGW(TAG, "JSON parse failed");
cJSON_Delete(arr);
return;
}
cJSON *rel;
cJSON_ArrayForEach(rel, arr) {
cJSON *tag_j = cJSON_GetObjectItem(rel, "tag_name");
if (!cJSON_IsString(tag_j)) continue;
const char *tag = tag_j->valuestring;
if (strncmp(tag, tag_prefix, strlen(tag_prefix)) != 0) continue;
/* Extract version after prefix */
const char *ver = tag + strlen(tag_prefix);
if (*ver == 'v') ver++; /* strip leading 'v' */
if (!version_newer(local_version, ver)) continue;
/* Found a newer release — extract asset URLs */
cJSON *assets = cJSON_GetObjectItem(rel, "assets");
if (!cJSON_IsArray(assets)) continue;
out->available = false;
out->download_url[0] = '\0';
out->sha256[0] = '\0';
strlcpy(out->version, ver, sizeof(out->version));
cJSON *asset;
cJSON_ArrayForEach(asset, assets) {
cJSON *name_j = cJSON_GetObjectItem(asset, "name");
cJSON *url_j = cJSON_GetObjectItem(asset, "browser_download_url");
if (!cJSON_IsString(name_j) || !cJSON_IsString(url_j)) continue;
if (strcmp(name_j->valuestring, bin_asset) == 0) {
strlcpy(out->download_url, url_j->valuestring,
sizeof(out->download_url));
out->available = true;
} else if (strcmp(name_j->valuestring, sha_asset) == 0) {
/* Download the SHA256 asset inline */
char *sha = http_get(url_j->valuestring);
if (sha) {
/* sha file is just hex+newline */
size_t n = strspn(sha, "0123456789abcdefABCDEF");
if (n == 64) {
memcpy(out->sha256, sha, 64);
out->sha256[64] = '\0';
}
free(sha);
}
}
}
if (out->available) {
ESP_LOGI(TAG, "update: tag=%s ver=%s", tag, out->version);
}
break; /* use first matching release */
}
cJSON_Delete(arr);
}
/* ── Main check ── */
void gitea_ota_check_now(void)
{
char url[512];
snprintf(url, sizeof(url),
"%s/api/v1/repos/%s/releases?limit=10",
GITEA_BASE_URL, GITEA_REPO);
char *json = http_get(url);
if (!json) {
ESP_LOGW(TAG, "releases fetch failed");
return;
}
parse_releases(json, BALANCE_TAG_PREFIX, BALANCE_BIN_ASSET,
BALANCE_SHA256_ASSET, BALANCE_FW_VERSION, &g_balance_update);
parse_releases(json, IO_TAG_PREFIX, IO_BIN_ASSET,
IO_SHA256_ASSET, IO_FW_VERSION, &g_io_update);
free(json);
}
/* ── Background task ── */
static void version_check_task(void *arg)
{
/* Initial check immediately after WiFi up */
vTaskDelay(pdMS_TO_TICKS(2000));
gitea_ota_check_now();
for (;;) {
vTaskDelay(pdMS_TO_TICKS(VERSION_CHECK_PERIOD_MS));
gitea_ota_check_now();
}
}
void gitea_ota_init(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
if (!wifi_connect()) {
ESP_LOGW(TAG, "WiFi unavailable — version checks disabled");
return;
}
xTaskCreate(version_check_task, "ver_check", 6144, NULL, 3, NULL);
ESP_LOGI(TAG, "version check task started");
}

View File

@ -1,42 +0,0 @@
#pragma once
/* gitea_ota.h — Gitea release version checker (bd-3hte)
*
* WiFi task: on boot and every 30 min, queries Gitea releases API,
* compares tag version against embedded FW_VERSION, stores update info.
*
* WiFi credentials read from NVS namespace "wifi" keys "ssid"/"pass".
* Fall back to compile-time defaults if NVS is empty.
*/
#include <stdint.h>
#include <stdbool.h>
/* Gitea instance */
#define GITEA_BASE_URL "http://gitea.vayrette.com"
#define GITEA_REPO "seb/saltylab-firmware"
#define GITEA_API_TIMEOUT_MS 10000
/* Version check interval */
#define VERSION_CHECK_PERIOD_MS (30u * 60u * 1000u) /* 30 minutes */
/* Max URL/version string lengths */
#define OTA_URL_MAX 384
#define OTA_VER_MAX 32
#define OTA_SHA256_MAX 65
typedef struct {
bool available;
char version[OTA_VER_MAX]; /* remote version string, e.g. "1.2.3" */
char download_url[OTA_URL_MAX]; /* direct download URL for .bin */
char sha256[OTA_SHA256_MAX]; /* hex SHA256 (from .sha256 asset), or "" */
} ota_update_info_t;
/* Shared state — written by gitea_ota_check_task, read by display/OTA tasks */
extern ota_update_info_t g_balance_update;
extern ota_update_info_t g_io_update;
/* Initialize WiFi and start version check task */
void gitea_ota_init(void);
/* One-shot sync check (can be called from any task) */
void gitea_ota_check_now(void);

View File

@ -1,114 +0,0 @@
/* main.c — ESP32-S3 BALANCE app_main (bd-66hx + OTA beads) */
#include "orin_serial.h"
#include "vesc_can.h"
#include "gitea_ota.h"
#include "ota_self.h"
#include "uart_ota.h"
#include "ota_display.h"
#include "config.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_timer.h"
#include <string.h>
static const char *TAG = "main";
static QueueHandle_t s_orin_tx_q;
/* ── Telemetry task: sends TELEM_STATUS to Orin at 10 Hz ── */
static void telem_task(void *arg)
{
for (;;) {
vTaskDelay(pdMS_TO_TICKS(TELEM_STATUS_PERIOD_MS));
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000LL);
bool hb_timeout = (now_ms - g_orin_ctrl.hb_last_ms) > HB_TIMEOUT_MS;
/* Determine balance state for telemetry */
bal_state_t state;
if (g_orin_ctrl.estop) {
state = BAL_ESTOP;
} else if (!g_orin_ctrl.armed) {
state = BAL_DISARMED;
} else {
state = BAL_ARMED;
}
/* flags: bit0=estop_active, bit1=heartbeat_timeout */
uint8_t flags = (g_orin_ctrl.estop ? 0x01u : 0x00u) |
(hb_timeout ? 0x02u : 0x00u);
/* Battery voltage from VESC_ID_A STATUS_5 (V×10 → mV) */
uint16_t vbat_mv = (uint16_t)((int32_t)g_vesc[0].voltage_x10 * 100);
orin_send_status(s_orin_tx_q,
0, /* pitch_x10: stub — full IMU in future bead */
0, /* motor_cmd: stub */
vbat_mv,
state,
flags);
}
}
/* ── Drive task: applies Orin drive commands to VESCs @ 50 Hz ── */
static void drive_task(void *arg)
{
for (;;) {
vTaskDelay(pdMS_TO_TICKS(20)); /* 50 Hz */
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000LL);
bool hb_timeout = (now_ms - g_orin_ctrl.hb_last_ms) > HB_TIMEOUT_MS;
bool drive_stale = (now_ms - g_orin_drive.updated_ms) > DRIVE_TIMEOUT_MS;
int32_t left_erpm = 0;
int32_t right_erpm = 0;
if (g_orin_ctrl.armed && !g_orin_ctrl.estop &&
!hb_timeout && !drive_stale) {
int32_t spd = (int32_t)g_orin_drive.speed * RPM_PER_SPEED_UNIT;
int32_t str = (int32_t)g_orin_drive.steer * RPM_PER_STEER_UNIT;
left_erpm = spd + str;
right_erpm = spd - str;
}
/* VESC_ID_A (56) = LEFT, VESC_ID_B (68) = RIGHT per bd-wim1 protocol */
vesc_can_send_rpm(VESC_ID_A, left_erpm);
vesc_can_send_rpm(VESC_ID_B, right_erpm);
}
}
void app_main(void)
{
ESP_LOGI(TAG, "ESP32-S3 BALANCE starting");
/* OTA rollback health check — must be called within OTA_ROLLBACK_WINDOW_S */
ota_self_health_check();
/* Init peripherals */
orin_serial_init();
vesc_can_init();
/* TX queue for outbound serial frames */
s_orin_tx_q = xQueueCreate(ORIN_TX_QUEUE_DEPTH, sizeof(orin_tx_frame_t));
configASSERT(s_orin_tx_q);
/* Seed heartbeat timer so we don't immediately timeout */
g_orin_ctrl.hb_last_ms = (uint32_t)(esp_timer_get_time() / 1000LL);
/* Create tasks */
xTaskCreate(orin_serial_rx_task, "orin_rx", 4096, s_orin_tx_q, 10, NULL);
xTaskCreate(orin_serial_tx_task, "orin_tx", 2048, s_orin_tx_q, 9, NULL);
xTaskCreate(vesc_can_rx_task, "vesc_rx", 4096, s_orin_tx_q, 10, NULL);
xTaskCreate(telem_task, "telem", 2048, NULL, 5, NULL);
xTaskCreate(drive_task, "drive", 2048, NULL, 8, NULL);
/* OTA subsystem — WiFi version checker + display overlay */
gitea_ota_init();
ota_display_init();
ESP_LOGI(TAG, "all tasks started");
/* app_main returns — FreeRTOS scheduler continues */
}

View File

@ -1,354 +0,0 @@
/* orin_serial.c — Orin↔ESP32-S3 serial protocol (bd-66hx + bd-1s1s OTA cmds) */
#include "orin_serial.h"
#include "config.h"
#include "gitea_ota.h"
#include "ota_self.h"
#include "uart_ota.h"
#include "version.h"
#include "driver/uart.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <string.h>
#include <stdio.h>
static const char *TAG = "orin";
/* ── Shared state ── */
orin_drive_t g_orin_drive = {0};
orin_pid_t g_orin_pid = {0};
orin_control_t g_orin_ctrl = {.armed = false, .estop = false, .hb_last_ms = 0};
/* ── CRC8-SMBUS (poly=0x07, init=0x00) ── */
static uint8_t crc8(const uint8_t *data, uint8_t len)
{
uint8_t crc = 0x00u;
for (uint8_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t b = 0; b < 8u; b++) {
crc = (crc & 0x80u) ? (uint8_t)((crc << 1u) ^ 0x07u) : (uint8_t)(crc << 1u);
}
}
return crc;
}
/* ── Frame builder ── */
static void build_frame(orin_tx_frame_t *f, uint8_t out[/* ORIN_MAX_PAYLOAD + 4 */], uint8_t *out_len)
{
/* [SYNC][LEN][TYPE][PAYLOAD...][CRC] */
uint8_t crc_buf[2u + ORIN_MAX_PAYLOAD];
crc_buf[0] = f->len;
crc_buf[1] = f->type;
memcpy(&crc_buf[2], f->payload, f->len);
uint8_t crc = crc8(crc_buf, (uint8_t)(2u + f->len));
out[0] = ORIN_SYNC;
out[1] = f->len;
out[2] = f->type;
memcpy(&out[3], f->payload, f->len);
out[3u + f->len] = crc;
*out_len = (uint8_t)(4u + f->len);
}
/* ── Enqueue helpers ── */
static void enqueue(QueueHandle_t q, uint8_t type, const uint8_t *payload, uint8_t len)
{
orin_tx_frame_t f = {.type = type, .len = len};
if (len > 0u && payload) {
memcpy(f.payload, payload, len);
}
if (xQueueSend(q, &f, 0) != pdTRUE) {
ESP_LOGW(TAG, "tx queue full, dropped type=0x%02x", type);
}
}
void orin_send_ack(QueueHandle_t q, uint8_t cmd_type)
{
enqueue(q, RESP_ACK, &cmd_type, 1u);
}
void orin_send_nack(QueueHandle_t q, uint8_t cmd_type, uint8_t err)
{
uint8_t p[2] = {cmd_type, err};
enqueue(q, RESP_NACK, p, 2u);
}
void orin_send_status(QueueHandle_t q,
int16_t pitch_x10, int16_t motor_cmd,
uint16_t vbat_mv, bal_state_t state, uint8_t flags)
{
/* int16 pitch_x10, int16 motor_cmd, uint16 vbat_mv, uint8 state, uint8 flags — BE */
uint8_t p[8];
p[0] = (uint8_t)((uint16_t)pitch_x10 >> 8u);
p[1] = (uint8_t)((uint16_t)pitch_x10);
p[2] = (uint8_t)((uint16_t)motor_cmd >> 8u);
p[3] = (uint8_t)((uint16_t)motor_cmd);
p[4] = (uint8_t)(vbat_mv >> 8u);
p[5] = (uint8_t)(vbat_mv);
p[6] = (uint8_t)state;
p[7] = flags;
enqueue(q, TELEM_STATUS, p, 8u);
}
void orin_send_vesc(QueueHandle_t q, uint8_t telem_type,
int32_t erpm, uint16_t voltage_mv,
int16_t current_ma, uint16_t temp_c_x10)
{
/* int32 erpm, uint16 voltage_mv, int16 current_ma, uint16 temp_c_x10 — BE */
uint8_t p[10];
uint32_t u = (uint32_t)erpm;
p[0] = (uint8_t)(u >> 24u);
p[1] = (uint8_t)(u >> 16u);
p[2] = (uint8_t)(u >> 8u);
p[3] = (uint8_t)(u);
p[4] = (uint8_t)(voltage_mv >> 8u);
p[5] = (uint8_t)(voltage_mv);
p[6] = (uint8_t)((uint16_t)current_ma >> 8u);
p[7] = (uint8_t)((uint16_t)current_ma);
p[8] = (uint8_t)(temp_c_x10 >> 8u);
p[9] = (uint8_t)(temp_c_x10);
enqueue(q, telem_type, p, 10u);
}
/* ── UART init ── */
void orin_serial_init(void)
{
uart_config_t cfg = {
.baud_rate = ORIN_UART_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
ESP_ERROR_CHECK(uart_param_config(ORIN_UART_PORT, &cfg));
ESP_ERROR_CHECK(uart_set_pin(ORIN_UART_PORT,
ORIN_UART_TX_GPIO, ORIN_UART_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(ORIN_UART_PORT, ORIN_UART_RX_BUF, 0,
0, NULL, 0));
ESP_LOGI(TAG, "UART%d init OK: tx=%d rx=%d baud=%d",
ORIN_UART_PORT, ORIN_UART_TX_GPIO, ORIN_UART_RX_GPIO, ORIN_UART_BAUD);
}
/* ── RX parser state machine ── */
typedef enum {
WAIT_SYNC,
WAIT_LEN,
WAIT_TYPE,
WAIT_PAYLOAD,
WAIT_CRC,
} rx_state_t;
static void dispatch_cmd(uint8_t type, const uint8_t *payload, uint8_t len,
QueueHandle_t tx_q)
{
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000LL);
switch (type) {
case CMD_HEARTBEAT:
g_orin_ctrl.hb_last_ms = now_ms;
orin_send_ack(tx_q, type);
break;
case CMD_DRIVE:
if (len < 4u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
if (g_orin_ctrl.estop) { orin_send_nack(tx_q, type, ERR_ESTOP_ACTIVE); break; }
if (!g_orin_ctrl.armed) { orin_send_nack(tx_q, type, ERR_DISARMED); break; }
g_orin_drive.speed = (int16_t)(((uint16_t)payload[0] << 8u) | payload[1]);
g_orin_drive.steer = (int16_t)(((uint16_t)payload[2] << 8u) | payload[3]);
g_orin_drive.updated_ms = now_ms;
g_orin_ctrl.hb_last_ms = now_ms; /* drive counts as heartbeat */
orin_send_ack(tx_q, type);
break;
case CMD_ESTOP:
if (len < 1u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
g_orin_ctrl.estop = (payload[0] != 0u);
if (g_orin_ctrl.estop) {
g_orin_drive.speed = 0;
g_orin_drive.steer = 0;
}
orin_send_ack(tx_q, type);
break;
case CMD_ARM:
if (len < 1u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
if (g_orin_ctrl.estop && payload[0] != 0u) {
/* cannot arm while estop is active */
orin_send_nack(tx_q, type, ERR_ESTOP_ACTIVE);
break;
}
g_orin_ctrl.armed = (payload[0] != 0u);
if (!g_orin_ctrl.armed) {
g_orin_drive.speed = 0;
g_orin_drive.steer = 0;
}
orin_send_ack(tx_q, type);
break;
case CMD_PID:
if (len < 12u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
/* float32 big-endian: copy and swap bytes */
{
uint32_t raw;
raw = ((uint32_t)payload[0] << 24u) | ((uint32_t)payload[1] << 16u) |
((uint32_t)payload[2] << 8u) | (uint32_t)payload[3];
memcpy((void*)&g_orin_pid.kp, &raw, 4u);
raw = ((uint32_t)payload[4] << 24u) | ((uint32_t)payload[5] << 16u) |
((uint32_t)payload[6] << 8u) | (uint32_t)payload[7];
memcpy((void*)&g_orin_pid.ki, &raw, 4u);
raw = ((uint32_t)payload[8] << 24u) | ((uint32_t)payload[9] << 16u) |
((uint32_t)payload[10] << 8u) | (uint32_t)payload[11];
memcpy((void*)&g_orin_pid.kd, &raw, 4u);
g_orin_pid.updated = true;
}
orin_send_ack(tx_q, type);
break;
case CMD_OTA_CHECK:
/* Trigger an immediate Gitea version check */
gitea_ota_check_now();
orin_send_version_info(tx_q, OTA_TARGET_BALANCE,
BALANCE_FW_VERSION,
g_balance_update.available
? g_balance_update.version : "");
orin_send_version_info(tx_q, OTA_TARGET_IO,
IO_FW_VERSION,
g_io_update.available
? g_io_update.version : "");
orin_send_ack(tx_q, type);
break;
case CMD_OTA_UPDATE:
if (len < 1u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
{
uint8_t target = payload[0];
bool triggered = false;
if (target == OTA_TARGET_IO || target == OTA_TARGET_BOTH) {
if (!uart_ota_trigger()) {
orin_send_nack(tx_q, type,
g_io_update.available ? ERR_OTA_BUSY : ERR_OTA_NO_UPDATE);
break;
}
triggered = true;
}
if (target == OTA_TARGET_BALANCE || target == OTA_TARGET_BOTH) {
if (!ota_self_trigger()) {
if (!triggered) {
orin_send_nack(tx_q, type,
g_balance_update.available ? ERR_OTA_BUSY : ERR_OTA_NO_UPDATE);
break;
}
}
}
orin_send_ack(tx_q, type);
}
break;
default:
ESP_LOGW(TAG, "unknown cmd type=0x%02x", type);
break;
}
}
void orin_serial_rx_task(void *arg)
{
QueueHandle_t tx_q = (QueueHandle_t)arg;
rx_state_t state = WAIT_SYNC;
uint8_t rx_len = 0;
uint8_t rx_type = 0;
uint8_t payload[ORIN_MAX_PAYLOAD];
uint8_t pay_idx = 0;
uint8_t byte;
for (;;) {
int r = uart_read_bytes(ORIN_UART_PORT, &byte, 1, pdMS_TO_TICKS(10));
if (r <= 0) {
continue;
}
switch (state) {
case WAIT_SYNC:
if (byte == ORIN_SYNC) { state = WAIT_LEN; }
break;
case WAIT_LEN:
if (byte > ORIN_MAX_PAYLOAD) {
/* oversize — send NACK and reset */
orin_send_nack(tx_q, 0x00u, ERR_BAD_LEN);
state = WAIT_SYNC;
} else {
rx_len = byte;
state = WAIT_TYPE;
}
break;
case WAIT_TYPE:
rx_type = byte;
pay_idx = 0u;
state = (rx_len == 0u) ? WAIT_CRC : WAIT_PAYLOAD;
break;
case WAIT_PAYLOAD:
payload[pay_idx++] = byte;
if (pay_idx == rx_len) { state = WAIT_CRC; }
break;
case WAIT_CRC: {
/* Verify CRC over [LEN, TYPE, PAYLOAD] */
uint8_t crc_buf[2u + ORIN_MAX_PAYLOAD];
crc_buf[0] = rx_len;
crc_buf[1] = rx_type;
memcpy(&crc_buf[2], payload, rx_len);
uint8_t expected = crc8(crc_buf, (uint8_t)(2u + rx_len));
if (byte != expected) {
ESP_LOGW(TAG, "CRC fail type=0x%02x got=0x%02x exp=0x%02x",
rx_type, byte, expected);
orin_send_nack(tx_q, rx_type, ERR_BAD_CRC);
} else {
dispatch_cmd(rx_type, payload, rx_len, tx_q);
}
state = WAIT_SYNC;
break;
}
}
}
}
void orin_serial_tx_task(void *arg)
{
QueueHandle_t tx_q = (QueueHandle_t)arg;
orin_tx_frame_t f;
uint8_t wire[4u + ORIN_MAX_PAYLOAD];
uint8_t wire_len;
for (;;) {
if (xQueueReceive(tx_q, &f, portMAX_DELAY) == pdTRUE) {
build_frame(&f, wire, &wire_len);
uart_write_bytes(ORIN_UART_PORT, (const char *)wire, wire_len);
}
}
}
/* ── OTA telemetry helpers (bd-1s1s) ── */
void orin_send_ota_status(QueueHandle_t q, uint8_t target,
uint8_t state, uint8_t progress, uint8_t err)
{
/* TELEM_OTA_STATUS: uint8 target, uint8 state, uint8 progress, uint8 err */
uint8_t p[4] = {target, state, progress, err};
enqueue(q, TELEM_OTA_STATUS, p, 4u);
}
void orin_send_version_info(QueueHandle_t q, uint8_t target,
const char *current, const char *available)
{
/* TELEM_VERSION_INFO: uint8 target, char current[16], char available[16] */
uint8_t p[33];
p[0] = target;
strncpy((char *)&p[1], current, 16); p[16] = '\0';
strncpy((char *)&p[17], available ? available : "", 16); p[32] = '\0';
enqueue(q, TELEM_VERSION_INFO, p, 33u);
}

View File

@ -1,113 +0,0 @@
#pragma once
/* orin_serial.h — Orin↔ESP32-S3 BALANCE USB/UART serial protocol (bd-66hx)
*
* Frame layout (matches bd-wim1 esp32_balance_protocol.py exactly):
* [0xAA][LEN][TYPE][PAYLOAD × LEN bytes][CRC8-SMBUS]
* CRC covers LEN + TYPE + PAYLOAD bytes.
* All multi-byte payload fields are big-endian.
*
* Physical: UART0 CH343 USB-serial Orin /dev/esp32-balance @ 460800 baud
*/
#include <stdint.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
/* ── Frame constants ── */
#define ORIN_SYNC 0xAAu
#define ORIN_MAX_PAYLOAD 62u
/* ── Command types: Orin → ESP32 ── */
#define CMD_HEARTBEAT 0x01u
#define CMD_DRIVE 0x02u /* int16 speed + int16 steer, BE */
#define CMD_ESTOP 0x03u /* uint8: 1=assert, 0=clear */
#define CMD_ARM 0x04u /* uint8: 1=arm, 0=disarm */
#define CMD_PID 0x05u /* float32 kp, ki, kd, BE */
/* ── Telemetry types: ESP32 → Orin ── */
#define TELEM_STATUS 0x80u /* status @ 10 Hz */
#define TELEM_VESC_LEFT 0x81u /* VESC ID 56 telemetry @ 10 Hz */
#define TELEM_VESC_RIGHT 0x82u /* VESC ID 68 telemetry @ 10 Hz */
#define TELEM_OTA_STATUS 0x83u /* OTA state + progress (bd-1s1s) */
#define TELEM_VERSION_INFO 0x84u /* firmware version report (bd-1s1s) */
#define RESP_ACK 0xA0u
#define RESP_NACK 0xA1u
/* ── OTA commands (Orin → ESP32, bd-1s1s) ── */
#define CMD_OTA_CHECK 0x10u /* no payload: trigger Gitea version check */
#define CMD_OTA_UPDATE 0x11u /* uint8 target: 0=balance, 1=io, 2=both */
/* ── OTA target constants ── */
#define OTA_TARGET_BALANCE 0x00u
#define OTA_TARGET_IO 0x01u
#define OTA_TARGET_BOTH 0x02u
/* ── NACK error codes ── */
#define ERR_BAD_CRC 0x01u
#define ERR_BAD_LEN 0x02u
#define ERR_ESTOP_ACTIVE 0x03u
#define ERR_DISARMED 0x04u
#define ERR_OTA_BUSY 0x05u
#define ERR_OTA_NO_UPDATE 0x06u
/* ── Balance state (mirrored from TELEM_STATUS.balance_state) ── */
typedef enum {
BAL_DISARMED = 0,
BAL_ARMED = 1,
BAL_TILT_FAULT = 2,
BAL_ESTOP = 3,
} bal_state_t;
/* ── Shared state written by RX task, consumed by main/vesc tasks ── */
typedef struct {
volatile int16_t speed; /* -1000..+1000 */
volatile int16_t steer; /* -1000..+1000 */
volatile uint32_t updated_ms; /* esp_timer tick at last CMD_DRIVE */
} orin_drive_t;
typedef struct {
volatile float kp, ki, kd;
volatile bool updated;
} orin_pid_t;
typedef struct {
volatile bool armed;
volatile bool estop;
volatile uint32_t hb_last_ms; /* esp_timer tick at last CMD_HEARTBEAT/CMD_DRIVE */
} orin_control_t;
/* ── TX frame queue item ── */
typedef struct {
uint8_t type;
uint8_t len;
uint8_t payload[ORIN_MAX_PAYLOAD];
} orin_tx_frame_t;
/* ── Globals (defined in orin_serial.c, extern here) ── */
extern orin_drive_t g_orin_drive;
extern orin_pid_t g_orin_pid;
extern orin_control_t g_orin_ctrl;
/* ── API ── */
void orin_serial_init(void);
/* Tasks — pass tx_queue as arg to both */
void orin_serial_rx_task(void *arg); /* arg = QueueHandle_t tx_queue */
void orin_serial_tx_task(void *arg); /* arg = QueueHandle_t tx_queue */
/* Enqueue outbound frames */
void orin_send_status(QueueHandle_t q,
int16_t pitch_x10, int16_t motor_cmd,
uint16_t vbat_mv, bal_state_t state, uint8_t flags);
void orin_send_vesc(QueueHandle_t q, uint8_t telem_type,
int32_t erpm, uint16_t voltage_mv,
int16_t current_ma, uint16_t temp_c_x10);
void orin_send_ack(QueueHandle_t q, uint8_t cmd_type);
void orin_send_nack(QueueHandle_t q, uint8_t cmd_type, uint8_t err);
/* OTA telemetry helpers (bd-1s1s) */
void orin_send_ota_status(QueueHandle_t q, uint8_t target,
uint8_t state, uint8_t progress, uint8_t err);
void orin_send_version_info(QueueHandle_t q, uint8_t target,
const char *current, const char *available);

View File

@ -1,150 +0,0 @@
/* ota_display.c — OTA notification/progress UI on GC9A01 (bd-1yr8)
*
* Renders OTA state overlaid on the 240×240 round HUD display:
* - BADGE: small dot on top-right when update available (idle state)
* - UPDATE SCREEN: version compare, Update Balance / Update IO / Update All
* - PROGRESS: arc around display perimeter + % + status text
* - ERROR: red banner + "RETRY" prompt
*
* The display_draw_* primitives must be provided by the GC9A01 driver.
* Actual SPI driver implementation is in a separate driver bead.
*/
#include "ota_display.h"
#include "gitea_ota.h"
#include "version.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <stdio.h>
#include <string.h>
static const char *TAG = "ota_disp";
/* Display centre and radius for the 240×240 GC9A01 */
#define CX 120
#define CY 120
#define RAD 110
/* ── Availability badge: 8×8 dot at top-right of display ── */
static void draw_badge(bool balance_avail, bool io_avail)
{
uint16_t col = (balance_avail || io_avail) ? COL_ORANGE : COL_BG;
display_fill_rect(200, 15, 12, 12, col);
}
/* ── Progress arc: sweeps 0→360° proportional to progress% ── */
static void draw_progress_arc(uint8_t pct, uint16_t color)
{
int end_deg = (int)(360 * pct / 100);
display_draw_arc(CX, CY, RAD, 0, end_deg, 6, color);
}
/* ── Status banner: 2 lines of text centred on display ── */
static void draw_status(const char *line1, const char *line2,
uint16_t fg, uint16_t bg)
{
display_fill_rect(20, 90, 200, 60, bg);
if (line1 && line1[0])
display_draw_string(CX - (int)(strlen(line1) * 6 / 2), 96,
line1, fg, bg);
if (line2 && line2[0])
display_draw_string(CX - (int)(strlen(line2) * 6 / 2), 116,
line2, fg, bg);
}
/* ── Main render logic ── */
void ota_display_update(void)
{
/* Determine dominant OTA state */
ota_self_state_t self = g_ota_self_state;
uart_ota_send_state_t io_s = g_uart_ota_state;
switch (self) {
case OTA_SELF_DOWNLOADING:
case OTA_SELF_VERIFYING:
case OTA_SELF_APPLYING: {
/* Balance self-update in progress */
char pct_str[16];
snprintf(pct_str, sizeof(pct_str), "%d%%", g_ota_self_progress);
const char *phase = (self == OTA_SELF_VERIFYING) ? "Verifying..." :
(self == OTA_SELF_APPLYING) ? "Applying..." :
"Downloading...";
draw_progress_arc(g_ota_self_progress, COL_BLUE);
draw_status("Updating Balance", pct_str, COL_WHITE, COL_BG);
ESP_LOGD(TAG, "balance OTA %s %d%%", phase, g_ota_self_progress);
return;
}
case OTA_SELF_REBOOTING:
draw_status("Update complete", "Rebooting...", COL_GREEN, COL_BG);
return;
case OTA_SELF_FAILED:
draw_progress_arc(0, COL_RED);
draw_status("Balance update", "FAILED RETRY?", COL_RED, COL_BG);
return;
default:
break;
}
switch (io_s) {
case UART_OTA_S_DOWNLOADING:
draw_progress_arc(g_uart_ota_progress, COL_YELLOW);
draw_status("Downloading IO", "firmware...", COL_WHITE, COL_BG);
return;
case UART_OTA_S_SENDING: {
char pct_str[16];
snprintf(pct_str, sizeof(pct_str), "%d%%", g_uart_ota_progress);
draw_progress_arc(g_uart_ota_progress, COL_YELLOW);
draw_status("Updating IO", pct_str, COL_WHITE, COL_BG);
return;
}
case UART_OTA_S_DONE:
draw_status("IO update done", "", COL_GREEN, COL_BG);
return;
case UART_OTA_S_FAILED:
draw_progress_arc(0, COL_RED);
draw_status("IO update", "FAILED RETRY?", COL_RED, COL_BG);
return;
default:
break;
}
/* Idle — show badge if update available */
bool bal_avail = g_balance_update.available;
bool io_avail = g_io_update.available;
draw_badge(bal_avail, io_avail);
if (bal_avail || io_avail) {
/* Show available versions on display when idle */
char verline[32];
if (bal_avail) {
snprintf(verline, sizeof(verline), "Bal v%s rdy",
g_balance_update.version);
draw_status(verline, io_avail ? "IO update rdy" : "",
COL_ORANGE, COL_BG);
} else if (io_avail) {
snprintf(verline, sizeof(verline), "IO v%s rdy",
g_io_update.version);
draw_status(verline, "", COL_ORANGE, COL_BG);
}
} else {
/* Clear OTA overlay area */
display_fill_rect(20, 90, 200, 60, COL_BG);
draw_badge(false, false);
}
}
/* ── Background display task (5 Hz) ── */
static void ota_display_task(void *arg)
{
for (;;) {
vTaskDelay(pdMS_TO_TICKS(200));
ota_display_update();
}
}
void ota_display_init(void)
{
xTaskCreate(ota_display_task, "ota_disp", 2048, NULL, 3, NULL);
ESP_LOGI(TAG, "OTA display task started");
}

View File

@ -1,33 +0,0 @@
#pragma once
/* ota_display.h — OTA notification UI on GC9A01 round LCD (bd-1yr8)
*
* GC9A01 240×240 round display via SPI (IO12 CS, IO11 DC, IO10 RST, IO9 BL).
* Calls into display_draw_* primitives (provided by display driver layer).
* This module owns the "OTA notification overlay" rendered over the HUD.
*/
#include <stdint.h>
#include <stdbool.h>
#include "ota_self.h"
#include "uart_ota.h"
/* ── Display primitives API (must be provided by display driver) ── */
void display_fill_rect(int x, int y, int w, int h, uint16_t rgb565);
void display_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg);
void display_draw_arc(int cx, int cy, int r, int start_deg, int end_deg,
int thickness, uint16_t color);
/* ── Colour palette (RGB565) ── */
#define COL_BG 0x0000u /* black */
#define COL_WHITE 0xFFFFu
#define COL_GREEN 0x07E0u
#define COL_YELLOW 0xFFE0u
#define COL_RED 0xF800u
#define COL_BLUE 0x001Fu
#define COL_ORANGE 0xFD20u
/* ── OTA display task: runs at 5 Hz, overlays OTA state on HUD ── */
void ota_display_init(void);
/* Called from main loop or display task to render the OTA overlay */
void ota_display_update(void);

View File

@ -1,183 +0,0 @@
/* ota_self.c — Balance self-OTA (bd-18nb)
*
* Uses esp_https_ota / esp_ota_ops to download from Gitea release URL,
* stream-verify SHA256 with mbedTLS, set new boot partition, and reboot.
* CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE in sdkconfig allows auto-rollback
* if the new image doesn't call esp_ota_mark_app_valid_cancel_rollback()
* within OTA_ROLLBACK_WINDOW_S seconds.
*/
#include "ota_self.h"
#include "gitea_ota.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/sha256.h"
#include <string.h>
#include <stdio.h>
static const char *TAG = "ota_self";
volatile ota_self_state_t g_ota_self_state = OTA_SELF_IDLE;
volatile uint8_t g_ota_self_progress = 0;
#define OTA_CHUNK_SIZE 4096
/* ── SHA256 verify helper ── */
static bool sha256_matches(const uint8_t *digest, const char *expected_hex)
{
if (!expected_hex || expected_hex[0] == '\0') {
ESP_LOGW(TAG, "no SHA256 to verify — skipping");
return true;
}
char got[65] = {0};
for (int i = 0; i < 32; i++) {
snprintf(&got[i*2], 3, "%02x", digest[i]);
}
bool ok = (strncasecmp(got, expected_hex, 64) == 0);
if (!ok) {
ESP_LOGE(TAG, "SHA256 mismatch: got=%s exp=%s", got, expected_hex);
}
return ok;
}
/* ── OTA download + flash task ── */
static void ota_self_task(void *arg)
{
const char *url = g_balance_update.download_url;
const char *sha256 = g_balance_update.sha256;
g_ota_self_state = OTA_SELF_DOWNLOADING;
g_ota_self_progress = 0;
ESP_LOGI(TAG, "OTA start: %s", url);
esp_ota_handle_t handle = 0;
const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL);
if (!ota_part) {
ESP_LOGE(TAG, "no OTA partition");
g_ota_self_state = OTA_SELF_FAILED;
vTaskDelete(NULL);
return;
}
esp_err_t err = esp_ota_begin(ota_part, OTA_WITH_SEQUENTIAL_WRITES, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "ota_begin: %s", esp_err_to_name(err));
g_ota_self_state = OTA_SELF_FAILED;
vTaskDelete(NULL);
return;
}
/* Setup HTTP client */
esp_http_client_config_t hcfg = {
.url = url,
.timeout_ms = 30000,
.buffer_size = OTA_CHUNK_SIZE,
.skip_cert_common_name_check = true,
};
esp_http_client_handle_t client = esp_http_client_init(&hcfg);
err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "http_open: %s", esp_err_to_name(err));
esp_ota_abort(handle);
esp_http_client_cleanup(client);
g_ota_self_state = OTA_SELF_FAILED;
vTaskDelete(NULL);
return;
}
int content_len = esp_http_client_fetch_headers(client);
ESP_LOGI(TAG, "content-length: %d", content_len);
mbedtls_sha256_context sha_ctx;
mbedtls_sha256_init(&sha_ctx);
mbedtls_sha256_starts(&sha_ctx, 0); /* 0 = SHA-256 */
static uint8_t buf[OTA_CHUNK_SIZE];
int total = 0;
int rd;
while ((rd = esp_http_client_read(client, (char *)buf, sizeof(buf))) > 0) {
mbedtls_sha256_update(&sha_ctx, buf, rd);
err = esp_ota_write(handle, buf, rd);
if (err != ESP_OK) {
ESP_LOGE(TAG, "ota_write: %s", esp_err_to_name(err));
esp_ota_abort(handle);
goto cleanup;
}
total += rd;
if (content_len > 0) {
g_ota_self_progress = (uint8_t)((total * 100) / content_len);
}
}
esp_http_client_close(client);
/* Verify SHA256 */
g_ota_self_state = OTA_SELF_VERIFYING;
uint8_t digest[32];
mbedtls_sha256_finish(&sha_ctx, digest);
if (!sha256_matches(digest, sha256)) {
ESP_LOGE(TAG, "SHA256 verification failed");
esp_ota_abort(handle);
g_ota_self_state = OTA_SELF_FAILED;
goto cleanup;
}
/* Finalize + set boot partition */
g_ota_self_state = OTA_SELF_APPLYING;
err = esp_ota_end(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "ota_end: %s", esp_err_to_name(err));
g_ota_self_state = OTA_SELF_FAILED;
goto cleanup;
}
err = esp_ota_set_boot_partition(ota_part);
if (err != ESP_OK) {
ESP_LOGE(TAG, "set_boot_partition: %s", esp_err_to_name(err));
g_ota_self_state = OTA_SELF_FAILED;
goto cleanup;
}
g_ota_self_state = OTA_SELF_REBOOTING;
g_ota_self_progress = 100;
ESP_LOGI(TAG, "OTA success — rebooting");
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
cleanup:
mbedtls_sha256_free(&sha_ctx);
esp_http_client_cleanup(client);
handle = 0;
vTaskDelete(NULL);
}
bool ota_self_trigger(void)
{
if (!g_balance_update.available) {
ESP_LOGW(TAG, "no update available");
return false;
}
if (g_ota_self_state != OTA_SELF_IDLE) {
ESP_LOGW(TAG, "OTA already in progress (state=%d)", g_ota_self_state);
return false;
}
xTaskCreate(ota_self_task, "ota_self", 8192, NULL, 5, NULL);
return true;
}
void ota_self_health_check(void)
{
/* Mark running image as valid — prevents rollback */
esp_err_t err = esp_ota_mark_app_valid_cancel_rollback();
if (err == ESP_OK) {
ESP_LOGI(TAG, "image marked valid");
} else if (err == ESP_ERR_NOT_SUPPORTED) {
/* Not an OTA image (e.g., flashed via JTAG) — ignore */
} else {
ESP_LOGW(TAG, "mark_valid: %s", esp_err_to_name(err));
}
}

View File

@ -1,34 +0,0 @@
#pragma once
/* ota_self.h — Balance self-OTA (bd-18nb)
*
* Downloads balance-firmware.bin from Gitea release URL to the inactive
* OTA partition, verifies SHA256, sets boot partition, reboots.
* Auto-rollback if health check not called within ROLLBACK_WINDOW_S seconds.
*/
#include <stdint.h>
#include <stdbool.h>
#define OTA_ROLLBACK_WINDOW_S 30
typedef enum {
OTA_SELF_IDLE = 0,
OTA_SELF_CHECKING, /* (unused — gitea_ota handles this) */
OTA_SELF_DOWNLOADING,
OTA_SELF_VERIFYING,
OTA_SELF_APPLYING,
OTA_SELF_REBOOTING,
OTA_SELF_FAILED,
} ota_self_state_t;
extern volatile ota_self_state_t g_ota_self_state;
extern volatile uint8_t g_ota_self_progress; /* 0-100 % */
/* Trigger a Balance self-update.
* Uses g_balance_update (from gitea_ota). Non-blocking: starts in a task.
* Returns false if no update available or OTA already in progress. */
bool ota_self_trigger(void);
/* Called from app_main after boot to mark the running image as valid.
* Must be called within OTA_ROLLBACK_WINDOW_S after boot or rollback fires. */
void ota_self_health_check(void);

View File

@ -1,241 +0,0 @@
/* uart_ota.c — UART OTA sender: Balance→IO board (bd-21hv)
*
* Downloads io-firmware.bin from Gitea, then sends to IO board via UART1.
* IO board must update itself BEFORE Balance self-update (per spec).
*/
#include "uart_ota.h"
#include "gitea_ota.h"
#include "esp_log.h"
#include "esp_http_client.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/sha256.h"
#include <string.h>
#include <stdio.h>
static const char *TAG = "uart_ota";
volatile uart_ota_send_state_t g_uart_ota_state = UART_OTA_S_IDLE;
volatile uint8_t g_uart_ota_progress = 0;
/* ── CRC8-SMBUS ── */
static uint8_t crc8(const uint8_t *d, uint16_t len)
{
uint8_t crc = 0;
for (uint16_t i = 0; i < len; i++) {
crc ^= d[i];
for (uint8_t b = 0; b < 8; b++)
crc = (crc & 0x80u) ? (uint8_t)((crc << 1u) ^ 0x07u) : (uint8_t)(crc << 1u);
}
return crc;
}
/* ── Build and send one UART OTA frame ── */
static void send_frame(uint8_t type, uint16_t seq,
const uint8_t *payload, uint16_t plen)
{
/* [TYPE:1][SEQ:2 BE][LEN:2 BE][PAYLOAD][CRC8:1] */
uint8_t hdr[5];
hdr[0] = type;
hdr[1] = (uint8_t)(seq >> 8u);
hdr[2] = (uint8_t)(seq);
hdr[3] = (uint8_t)(plen >> 8u);
hdr[4] = (uint8_t)(plen);
/* CRC over hdr + payload */
uint8_t crc_buf[5 + OTA_UART_CHUNK_SIZE];
memcpy(crc_buf, hdr, 5);
if (plen > 0 && payload) memcpy(crc_buf + 5, payload, plen);
uint8_t crc = crc8(crc_buf, (uint16_t)(5 + plen));
uart_write_bytes(UART_OTA_PORT, (char *)hdr, 5);
if (plen > 0 && payload)
uart_write_bytes(UART_OTA_PORT, (char *)payload, plen);
uart_write_bytes(UART_OTA_PORT, (char *)&crc, 1);
}
/* ── Wait for ACK/NACK from IO board ── */
static bool wait_ack(uint16_t expected_seq)
{
/* Response frame: [TYPE:1][SEQ:2][LEN:2][PAYLOAD][CRC:1] */
uint8_t buf[16];
int timeout = OTA_UART_ACK_TIMEOUT_MS;
int got = 0;
while (timeout > 0 && got < 6) {
int r = uart_read_bytes(UART_OTA_PORT, buf + got, 1, pdMS_TO_TICKS(50));
if (r > 0) got++;
else timeout -= 50;
}
if (got < 3) return false;
uint8_t type = buf[0];
uint16_t seq = (uint16_t)((buf[1] << 8u) | buf[2]);
if (type == UART_OTA_ACK && seq == expected_seq) return true;
if (type == UART_OTA_NACK) {
uint8_t err = (got >= 6) ? buf[5] : 0;
ESP_LOGW(TAG, "NACK seq=%u err=%u", seq, err);
}
return false;
}
/* ── Download firmware to RAM buffer (max 1.75 MB) ── */
static uint8_t *download_io_firmware(uint32_t *out_size)
{
const char *url = g_io_update.download_url;
ESP_LOGI(TAG, "downloading IO fw: %s", url);
esp_http_client_config_t cfg = {
.url = url, .timeout_ms = 30000,
.skip_cert_common_name_check = true,
};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
if (esp_http_client_open(client, 0) != ESP_OK) {
esp_http_client_cleanup(client);
return NULL;
}
int content_len = esp_http_client_fetch_headers(client);
if (content_len <= 0 || content_len > (int)(0x1B0000)) {
ESP_LOGE(TAG, "bad content-length: %d", content_len);
esp_http_client_cleanup(client);
return NULL;
}
uint8_t *buf = malloc(content_len);
if (!buf) {
ESP_LOGE(TAG, "malloc %d failed", content_len);
esp_http_client_cleanup(client);
return NULL;
}
int total = 0, rd;
while ((rd = esp_http_client_read(client, (char *)buf + total,
content_len - total)) > 0) {
total += rd;
g_uart_ota_progress = (uint8_t)((total * 50) / content_len); /* 0-50% for download */
}
esp_http_client_cleanup(client);
if (total != content_len) {
free(buf);
return NULL;
}
*out_size = (uint32_t)total;
return buf;
}
/* ── UART OTA send task ── */
static void uart_ota_task(void *arg)
{
g_uart_ota_state = UART_OTA_S_DOWNLOADING;
g_uart_ota_progress = 0;
uint32_t fw_size = 0;
uint8_t *fw = download_io_firmware(&fw_size);
if (!fw) {
ESP_LOGE(TAG, "download failed");
g_uart_ota_state = UART_OTA_S_FAILED;
vTaskDelete(NULL);
return;
}
/* Compute SHA256 of downloaded firmware */
uint8_t digest[32];
mbedtls_sha256_context sha;
mbedtls_sha256_init(&sha);
mbedtls_sha256_starts(&sha, 0);
mbedtls_sha256_update(&sha, fw, fw_size);
mbedtls_sha256_finish(&sha, digest);
mbedtls_sha256_free(&sha);
g_uart_ota_state = UART_OTA_S_SENDING;
/* Send OTA_BEGIN: uint32 size + uint8[32] sha256 */
uint8_t begin_payload[36];
begin_payload[0] = (uint8_t)(fw_size >> 24u);
begin_payload[1] = (uint8_t)(fw_size >> 16u);
begin_payload[2] = (uint8_t)(fw_size >> 8u);
begin_payload[3] = (uint8_t)(fw_size);
memcpy(&begin_payload[4], digest, 32);
for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) {
send_frame(UART_OTA_BEGIN, 0, begin_payload, 36);
if (wait_ack(0)) goto send_data;
ESP_LOGW(TAG, "BEGIN retry %d", retry);
}
ESP_LOGE(TAG, "BEGIN failed");
free(fw);
g_uart_ota_state = UART_OTA_S_FAILED;
vTaskDelete(NULL);
return;
send_data: {
uint32_t offset = 0;
uint16_t seq = 1;
while (offset < fw_size) {
uint16_t chunk = (uint16_t)((fw_size - offset) < OTA_UART_CHUNK_SIZE
? (fw_size - offset) : OTA_UART_CHUNK_SIZE);
bool acked = false;
for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) {
send_frame(UART_OTA_DATA, seq, fw + offset, chunk);
if (wait_ack(seq)) { acked = true; break; }
ESP_LOGW(TAG, "DATA seq=%u retry=%d", seq, retry);
}
if (!acked) {
ESP_LOGE(TAG, "DATA seq=%u failed", seq);
send_frame(UART_OTA_ABORT, seq, NULL, 0);
free(fw);
g_uart_ota_state = UART_OTA_S_FAILED;
vTaskDelete(NULL);
return;
}
offset += chunk;
seq++;
/* 50-100% for sending phase */
g_uart_ota_progress = (uint8_t)(50u + (offset * 50u) / fw_size);
}
/* Send OTA_END */
for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) {
send_frame(UART_OTA_END, seq, NULL, 0);
if (wait_ack(seq)) break;
}
}
free(fw);
g_uart_ota_progress = 100;
g_uart_ota_state = UART_OTA_S_DONE;
ESP_LOGI(TAG, "IO OTA complete — %lu bytes sent", (unsigned long)fw_size);
vTaskDelete(NULL);
}
bool uart_ota_trigger(void)
{
if (!g_io_update.available) {
ESP_LOGW(TAG, "no IO update available");
return false;
}
if (g_uart_ota_state != UART_OTA_S_IDLE) {
ESP_LOGW(TAG, "UART OTA busy (state=%d)", g_uart_ota_state);
return false;
}
/* Init UART1 for OTA */
uart_config_t ucfg = {
.baud_rate = UART_OTA_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
uart_param_config(UART_OTA_PORT, &ucfg);
uart_set_pin(UART_OTA_PORT, UART_OTA_TX_GPIO, UART_OTA_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_OTA_PORT, 2048, 0, 0, NULL, 0);
xTaskCreate(uart_ota_task, "uart_ota", 16384, NULL, 4, NULL);
return true;
}

View File

@ -1,64 +0,0 @@
#pragma once
/* uart_ota.h — UART OTA protocol for Balance→IO firmware update (bd-21hv)
*
* Balance downloads io-firmware.bin from Gitea, then streams it to the IO
* board over UART1 (GPIO17/18, 460800 baud) in 1 KB chunks with ACK.
*
* Protocol frame format (both directions):
* [TYPE:1][SEQ:2 BE][LEN:2 BE][PAYLOAD:LEN][CRC8:1]
* CRC8-SMBUS over TYPE+SEQ+LEN+PAYLOAD.
*
* BalanceIO:
* OTA_BEGIN (0xC0) payload: uint32 total_size BE + uint8[32] sha256
* OTA_DATA (0xC1) payload: uint8[] chunk (up to 1024 bytes)
* OTA_END (0xC2) no payload
* OTA_ABORT (0xC3) no payload
*
* IOBalance:
* OTA_ACK (0xC4) payload: uint16 acked_seq BE
* OTA_NACK (0xC5) payload: uint16 failed_seq BE + uint8 err_code
* OTA_STATUS (0xC6) payload: uint8 state + uint8 progress%
*/
#include <stdint.h>
#include <stdbool.h>
/* UART for Balance→IO OTA */
#include "driver/uart.h"
#define UART_OTA_PORT UART_NUM_1
#define UART_OTA_BAUD 460800
#define UART_OTA_TX_GPIO 17
#define UART_OTA_RX_GPIO 18
#define OTA_UART_CHUNK_SIZE 1024
#define OTA_UART_ACK_TIMEOUT_MS 3000
#define OTA_UART_MAX_RETRIES 3
/* Frame type bytes */
#define UART_OTA_BEGIN 0xC0u
#define UART_OTA_DATA 0xC1u
#define UART_OTA_END 0xC2u
#define UART_OTA_ABORT 0xC3u
#define UART_OTA_ACK 0xC4u
#define UART_OTA_NACK 0xC5u
#define UART_OTA_STATUS 0xC6u
/* NACK error codes */
#define OTA_ERR_BAD_CRC 0x01u
#define OTA_ERR_WRITE 0x02u
#define OTA_ERR_SIZE 0x03u
typedef enum {
UART_OTA_S_IDLE = 0,
UART_OTA_S_DOWNLOADING, /* downloading from Gitea */
UART_OTA_S_SENDING, /* sending to IO board */
UART_OTA_S_DONE,
UART_OTA_S_FAILED,
} uart_ota_send_state_t;
extern volatile uart_ota_send_state_t g_uart_ota_state;
extern volatile uint8_t g_uart_ota_progress;
/* Trigger IO firmware update. Uses g_io_update (from gitea_ota).
* Downloads bin, then streams via UART. Returns false if busy or no update. */
bool uart_ota_trigger(void);

View File

@ -1,14 +0,0 @@
#pragma once
/* Embedded firmware version — bump on each release */
#define BALANCE_FW_VERSION "1.0.0"
#define IO_FW_VERSION "1.0.0"
/* Gitea release tag prefixes */
#define BALANCE_TAG_PREFIX "esp32-balance/"
#define IO_TAG_PREFIX "esp32-io/"
/* Gitea release asset filenames */
#define BALANCE_BIN_ASSET "balance-firmware.bin"
#define IO_BIN_ASSET "io-firmware.bin"
#define BALANCE_SHA256_ASSET "balance-firmware.sha256"
#define IO_SHA256_ASSET "io-firmware.sha256"

View File

@ -1,119 +0,0 @@
/* vesc_can.c — VESC CAN TWAI driver (bd-66hx)
*
* Receives VESC STATUS/4/5 frames via TWAI, proxies to Orin over serial.
* Transmits SET_RPM commands from Orin drive requests.
*/
#include "vesc_can.h"
#include "orin_serial.h"
#include "config.h"
#include "driver/twai.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
static const char *TAG = "vesc_can";
vesc_state_t g_vesc[2] = {0};
/* Index for a given VESC node ID: 0=VESC_ID_A, 1=VESC_ID_B */
static int vesc_idx(uint8_t id)
{
if (id == VESC_ID_A) return 0;
if (id == VESC_ID_B) return 1;
return -1;
}
void vesc_can_init(void)
{
twai_general_config_t gcfg = TWAI_GENERAL_CONFIG_DEFAULT(
(gpio_num_t)VESC_CAN_TX_GPIO,
(gpio_num_t)VESC_CAN_RX_GPIO,
TWAI_MODE_NORMAL);
gcfg.rx_queue_len = VESC_CAN_RX_QUEUE;
twai_timing_config_t tcfg = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t fcfg = TWAI_FILTER_CONFIG_ACCEPT_ALL();
ESP_ERROR_CHECK(twai_driver_install(&gcfg, &tcfg, &fcfg));
ESP_ERROR_CHECK(twai_start());
ESP_LOGI(TAG, "TWAI init OK: tx=%d rx=%d 500kbps", VESC_CAN_TX_GPIO, VESC_CAN_RX_GPIO);
}
void vesc_can_send_rpm(uint8_t vesc_id, int32_t erpm)
{
uint32_t ext_id = ((uint32_t)VESC_PKT_SET_RPM << 8u) | vesc_id;
twai_message_t msg = {
.extd = 1,
.identifier = ext_id,
.data_length_code = 4,
};
uint32_t u = (uint32_t)erpm;
msg.data[0] = (uint8_t)(u >> 24u);
msg.data[1] = (uint8_t)(u >> 16u);
msg.data[2] = (uint8_t)(u >> 8u);
msg.data[3] = (uint8_t)(u);
twai_transmit(&msg, pdMS_TO_TICKS(5));
}
void vesc_can_rx_task(void *arg)
{
QueueHandle_t tx_q = (QueueHandle_t)arg;
twai_message_t msg;
for (;;) {
if (twai_receive(&msg, pdMS_TO_TICKS(50)) != ESP_OK) {
continue;
}
if (!msg.extd) {
continue; /* ignore standard frames */
}
uint8_t pkt_type = (uint8_t)(msg.identifier >> 8u);
uint8_t vesc_id = (uint8_t)(msg.identifier & 0xFFu);
int idx = vesc_idx(vesc_id);
if (idx < 0) {
continue; /* not our VESC */
}
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000LL);
vesc_state_t *s = &g_vesc[idx];
switch (pkt_type) {
case VESC_PKT_STATUS:
if (msg.data_length_code < 8u) { break; }
s->erpm = (int32_t)(
((uint32_t)msg.data[0] << 24u) | ((uint32_t)msg.data[1] << 16u) |
((uint32_t)msg.data[2] << 8u) | (uint32_t)msg.data[3]);
s->current_x10 = (int16_t)(((uint16_t)msg.data[4] << 8u) | msg.data[5]);
s->last_rx_ms = now_ms;
/* Proxy to Orin: voltage from STATUS_5 (may be zero until received) */
{
uint8_t ttype = (vesc_id == VESC_ID_A) ? TELEM_VESC_LEFT : TELEM_VESC_RIGHT;
/* voltage_mv: V×10 → mV (/10 * 1000 = *100); current_ma: A×10 → mA (*100) */
uint16_t vmv = (uint16_t)((int32_t)s->voltage_x10 * 100);
int16_t ima = (int16_t)((int32_t)s->current_x10 * 100);
orin_send_vesc(tx_q, ttype, s->erpm, vmv, ima,
(uint16_t)s->temp_mot_x10);
}
break;
case VESC_PKT_STATUS_4:
if (msg.data_length_code < 6u) { break; }
/* T_fet×10, T_mot×10, I_in×10 */
s->temp_mot_x10 = (int16_t)(((uint16_t)msg.data[2] << 8u) | msg.data[3]);
break;
case VESC_PKT_STATUS_5:
if (msg.data_length_code < 6u) { break; }
/* int32 tacho (ignored), int16 V_in×10 */
s->voltage_x10 = (int16_t)(((uint16_t)msg.data[4] << 8u) | msg.data[5]);
break;
default:
break;
}
}
}

View File

@ -1,36 +0,0 @@
#pragma once
/* vesc_can.h — VESC CAN TWAI driver for ESP32-S3 BALANCE (bd-66hx)
*
* VESC extended CAN ID: (packet_type << 8) | vesc_node_id
* Physical layer: TWAI peripheral SN65HVD230 500 kbps shared bus
*/
#include <stdint.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
/* ── VESC packet types ── */
#define VESC_PKT_SET_RPM 3u
#define VESC_PKT_STATUS 9u /* int32 erpm, int16 I×10, int16 duty×1000 */
#define VESC_PKT_STATUS_4 16u /* int16 T_fet×10, T_mot×10, I_in×10 */
#define VESC_PKT_STATUS_5 27u /* int32 tacho, int16 V_in×10 */
/* ── VESC telemetry snapshot ── */
typedef struct {
int32_t erpm; /* electrical RPM (STATUS) */
int16_t current_x10; /* phase current A×10 (STATUS) */
int16_t voltage_x10; /* bus voltage V×10 (STATUS_5) */
int16_t temp_mot_x10; /* motor temp °C×10 (STATUS_4) */
uint32_t last_rx_ms; /* esp_timer ms of last STATUS frame */
} vesc_state_t;
/* ── Globals (two VESC nodes: index 0 = VESC_ID_A=56, 1 = VESC_ID_B=68) ── */
extern vesc_state_t g_vesc[2];
/* ── API ── */
void vesc_can_init(void);
void vesc_can_send_rpm(uint8_t vesc_id, int32_t erpm);
/* RX task — pass tx_queue as arg; forwards STATUS frames to Orin over serial */
void vesc_can_rx_task(void *arg); /* arg = QueueHandle_t orin_tx_queue */

View File

@ -1,7 +0,0 @@
# ESP32-S3 BALANCE — 4 MB flash, dual OTA partitions
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1B0000,
app1, app, ota_1, 0x1C0000, 0x1B0000,
nvs_user, data, nvs, 0x370000, 0x50000,
1 # ESP32-S3 BALANCE — 4 MB flash, dual OTA partitions
2 # Name, Type, SubType, Offset, Size
3 nvs, data, nvs, 0x9000, 0x5000,
4 otadata, data, ota, 0xe000, 0x2000,
5 app0, app, ota_0, 0x10000, 0x1B0000,
6 app1, app, ota_1, 0x1C0000, 0x1B0000,
7 nvs_user, data, nvs, 0x370000, 0x50000,

View File

@ -1,19 +0,0 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_TASK_WDT_EN=y
CONFIG_ESP_TASK_WDT_TIMEOUT_S=5
CONFIG_TWAI_ISR_IN_IRAM=y
CONFIG_UART_ISR_IN_IRAM=y
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
CONFIG_ESP_CONSOLE_UART_NUM=0
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# OTA — bd-3gwo: dual OTA partitions + rollback
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
CONFIG_OTA_ALLOW_HTTP=y
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y

View File

@ -1,3 +0,0 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32s3_io)

View File

@ -1,10 +0,0 @@
idf_component_register(
SRCS "main.c" "uart_ota_recv.c"
INCLUDE_DIRS "."
REQUIRES
app_update
mbedtls
driver
freertos
esp_timer
)

View File

@ -1,35 +0,0 @@
#pragma once
/* ESP32-S3 IO board — pin assignments (SAUL-TEE-SYSTEM-REFERENCE.md) */
/* ── Inter-board UART (to/from BALANCE board) ── */
#define IO_UART_PORT UART_NUM_0
#define IO_UART_BAUD 460800
#define IO_UART_TX_GPIO 43 /* IO board UART0_TXD → BALANCE RX */
#define IO_UART_RX_GPIO 44 /* IO board UART0_RXD ← BALANCE TX */
/* Note: SAUL-TEE spec says IO TX=IO18, RX=IO21; BALANCE TX=IO17, RX=IO18.
* This is UART0 on the IO devkit (GPIO43/44). Adjust to match actual wiring. */
/* ── BTS7960 Left motor driver ── */
#define MOTOR_L_RPWM 1
#define MOTOR_L_LPWM 2
#define MOTOR_L_EN_R 3
#define MOTOR_L_EN_L 4
/* ── BTS7960 Right motor driver ── */
#define MOTOR_R_RPWM 5
#define MOTOR_R_LPWM 6
#define MOTOR_R_EN_R 7
#define MOTOR_R_EN_L 8
/* ── Arming button / kill switch ── */
#define ARM_BTN_GPIO 9
#define KILL_GPIO 10
/* ── WS2812B LED strip ── */
#define LED_DATA_GPIO 13
/* ── OTA UART — receives firmware from BALANCE (bd-21hv) ── */
/* Uses same IO_UART_PORT since Balance drives OTA over the inter-board link */
/* ── Firmware version ── */
#define IO_FW_VERSION "1.0.0"

View File

@ -1,42 +0,0 @@
/* main.c — ESP32-S3 IO board app_main */
#include "uart_ota_recv.h"
#include "config.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "io_main";
static void uart_init(void)
{
uart_config_t cfg = {
.baud_rate = IO_UART_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
uart_param_config(IO_UART_PORT, &cfg);
uart_set_pin(IO_UART_PORT, IO_UART_TX_GPIO, IO_UART_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(IO_UART_PORT, 4096, 0, 0, NULL, 0);
}
void app_main(void)
{
ESP_LOGI(TAG, "ESP32-S3 IO v%s starting", IO_FW_VERSION);
/* Mark running image valid (OTA rollback support) */
esp_ota_mark_app_valid_cancel_rollback();
uart_init();
uart_ota_recv_init();
/* IO board main loop placeholder — RC/motor/sensor tasks added in later beads */
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

View File

@ -1,210 +0,0 @@
/* uart_ota_recv.c — IO board OTA receiver (bd-21hv)
*
* Listens on UART0 for OTA frames from Balance board.
* Writes incoming chunks to the inactive OTA partition, verifies SHA256,
* then reboots into new firmware.
*/
#include "uart_ota_recv.h"
#include "config.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/sha256.h"
#include <string.h>
static const char *TAG = "io_ota";
volatile io_ota_state_t g_io_ota_state = IO_OTA_IDLE;
volatile uint8_t g_io_ota_progress = 0;
/* Frame type bytes (same as uart_ota.h sender side) */
#define OTA_BEGIN 0xC0u
#define OTA_DATA 0xC1u
#define OTA_END 0xC2u
#define OTA_ABORT 0xC3u
#define OTA_ACK 0xC4u
#define OTA_NACK 0xC5u
#define CHUNK_MAX 1024
static uint8_t crc8(const uint8_t *d, uint16_t len)
{
uint8_t crc = 0;
for (uint16_t i = 0; i < len; i++) {
crc ^= d[i];
for (uint8_t b = 0; b < 8; b++)
crc = (crc & 0x80u) ? (uint8_t)((crc << 1u) ^ 0x07u) : (uint8_t)(crc << 1u);
}
return crc;
}
static void send_ack(uint16_t seq)
{
uint8_t frame[6];
frame[0] = OTA_ACK;
frame[1] = (uint8_t)(seq >> 8u);
frame[2] = (uint8_t)(seq);
frame[3] = 0; frame[4] = 0; /* LEN=0 */
uint8_t crc = crc8(frame, 5);
frame[5] = crc;
uart_write_bytes(IO_UART_PORT, (char *)frame, 6);
}
static void send_nack(uint16_t seq, uint8_t err)
{
uint8_t frame[8];
frame[0] = OTA_NACK;
frame[1] = (uint8_t)(seq >> 8u);
frame[2] = (uint8_t)(seq);
frame[3] = 0; frame[4] = 1; /* LEN=1 */
frame[5] = err;
uint8_t crc = crc8(frame, 6);
frame[6] = crc;
uart_write_bytes(IO_UART_PORT, (char *)frame, 7);
}
/* Read exact n bytes with timeout */
static bool uart_read_exact(uint8_t *buf, int n, int timeout_ms)
{
int got = 0;
while (got < n && timeout_ms > 0) {
int r = uart_read_bytes(IO_UART_PORT, buf + got, n - got,
pdMS_TO_TICKS(50));
if (r > 0) got += r;
else timeout_ms -= 50;
}
return got == n;
}
static void ota_recv_task(void *arg)
{
esp_ota_handle_t handle = 0;
const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL);
mbedtls_sha256_context sha;
mbedtls_sha256_init(&sha);
uint32_t expected_size = 0;
uint8_t expected_digest[32] = {0};
uint32_t received = 0;
bool ota_started = false;
static uint8_t payload[CHUNK_MAX];
for (;;) {
/* Read frame header: TYPE(1) + SEQ(2) + LEN(2) = 5 bytes */
uint8_t hdr[5];
if (!uart_read_exact(hdr, 5, 5000)) continue;
uint8_t type = hdr[0];
uint16_t seq = (uint16_t)((hdr[1] << 8u) | hdr[2]);
uint16_t plen = (uint16_t)((hdr[3] << 8u) | hdr[4]);
if (plen > CHUNK_MAX + 36) {
ESP_LOGW(TAG, "oversized frame plen=%u", plen);
continue;
}
/* Read payload + CRC */
if (plen > 0 && !uart_read_exact(payload, plen, 2000)) continue;
uint8_t crc_rx;
if (!uart_read_exact(&crc_rx, 1, 500)) continue;
/* Verify CRC over hdr+payload */
uint8_t crc_buf[5 + CHUNK_MAX + 36];
memcpy(crc_buf, hdr, 5);
if (plen > 0) memcpy(crc_buf + 5, payload, plen);
uint8_t expected_crc = crc8(crc_buf, (uint16_t)(5 + plen));
if (crc_rx != expected_crc) {
ESP_LOGW(TAG, "CRC fail seq=%u", seq);
send_nack(seq, 0x01u); /* OTA_ERR_BAD_CRC */
continue;
}
switch (type) {
case OTA_BEGIN:
if (plen < 36) { send_nack(seq, 0x03u); break; }
expected_size = ((uint32_t)payload[0] << 24u) |
((uint32_t)payload[1] << 16u) |
((uint32_t)payload[2] << 8u) |
(uint32_t)payload[3];
memcpy(expected_digest, &payload[4], 32);
if (!ota_part || esp_ota_begin(ota_part, OTA_WITH_SEQUENTIAL_WRITES,
&handle) != ESP_OK) {
send_nack(seq, 0x02u);
break;
}
mbedtls_sha256_starts(&sha, 0);
received = 0;
ota_started = true;
g_io_ota_state = IO_OTA_RECEIVING;
g_io_ota_progress = 0;
ESP_LOGI(TAG, "OTA begin: %lu bytes", (unsigned long)expected_size);
send_ack(seq);
break;
case OTA_DATA:
if (!ota_started) { send_nack(seq, 0x02u); break; }
if (esp_ota_write(handle, payload, plen) != ESP_OK) {
send_nack(seq, 0x02u);
esp_ota_abort(handle);
ota_started = false;
g_io_ota_state = IO_OTA_FAILED;
break;
}
mbedtls_sha256_update(&sha, payload, plen);
received += plen;
if (expected_size > 0)
g_io_ota_progress = (uint8_t)((received * 100u) / expected_size);
send_ack(seq);
break;
case OTA_END: {
if (!ota_started) { send_nack(seq, 0x02u); break; }
g_io_ota_state = IO_OTA_VERIFYING;
uint8_t digest[32];
mbedtls_sha256_finish(&sha, digest);
if (memcmp(digest, expected_digest, 32) != 0) {
ESP_LOGE(TAG, "SHA256 mismatch");
esp_ota_abort(handle);
send_nack(seq, 0x01u);
g_io_ota_state = IO_OTA_FAILED;
break;
}
if (esp_ota_end(handle) != ESP_OK ||
esp_ota_set_boot_partition(ota_part) != ESP_OK) {
send_nack(seq, 0x02u);
g_io_ota_state = IO_OTA_FAILED;
break;
}
g_io_ota_state = IO_OTA_REBOOTING;
g_io_ota_progress = 100;
ESP_LOGI(TAG, "OTA done — rebooting");
send_ack(seq);
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
break;
}
case OTA_ABORT:
if (ota_started) { esp_ota_abort(handle); ota_started = false; }
g_io_ota_state = IO_OTA_IDLE;
ESP_LOGW(TAG, "OTA aborted");
break;
default:
break;
}
}
}
void uart_ota_recv_init(void)
{
/* UART0 already initialized for inter-board comms; just create the task */
xTaskCreate(ota_recv_task, "io_ota_recv", 8192, NULL, 6, NULL);
ESP_LOGI(TAG, "OTA receiver task started");
}

View File

@ -1,20 +0,0 @@
#pragma once
/* uart_ota_recv.h — IO board: receives OTA firmware from Balance (bd-21hv) */
#include <stdint.h>
#include <stdbool.h>
typedef enum {
IO_OTA_IDLE = 0,
IO_OTA_RECEIVING,
IO_OTA_VERIFYING,
IO_OTA_APPLYING,
IO_OTA_REBOOTING,
IO_OTA_FAILED,
} io_ota_state_t;
extern volatile io_ota_state_t g_io_ota_state;
extern volatile uint8_t g_io_ota_progress;
/* Start listening for OTA frames on UART0 */
void uart_ota_recv_init(void);

View File

@ -1,7 +0,0 @@
# ESP32-S3 IO — 4 MB flash, dual OTA partitions
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1B0000,
app1, app, ota_1, 0x1C0000, 0x1B0000,
nvs_user, data, nvs, 0x370000, 0x50000,
1 # ESP32-S3 IO — 4 MB flash, dual OTA partitions
2 # Name, Type, SubType, Offset, Size
3 nvs, data, nvs, 0x9000, 0x5000,
4 otadata, data, ota, 0xe000, 0x2000,
5 app0, app, ota_0, 0x10000, 0x1B0000,
6 app1, app, ota_1, 0x1C0000, 0x1B0000,
7 nvs_user, data, nvs, 0x370000, 0x50000,

View File

@ -1,13 +0,0 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_TASK_WDT_EN=y
CONFIG_ESP_TASK_WDT_TIMEOUT_S=5
CONFIG_UART_ISR_IN_IRAM=y
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# OTA — bd-3gwo: dual OTA partitions + rollback
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y

View File

@ -15,9 +15,8 @@
typedef enum { typedef enum {
BALANCE_DISARMED = 0, /* Motors off, waiting for arm command */ BALANCE_DISARMED = 0, /* Motors off, waiting for arm command */
BALANCE_ARMED = 1, /* Active balancing */ BALANCE_ARMED, /* Active balancing */
BALANCE_TILT_FAULT = 2, /* Tilt exceeded limit, motors killed */ BALANCE_TILT_FAULT, /* Tilt exceeded limit, motors killed */
BALANCE_PARKED = 3, /* PID frozen, motors off — quick re-arm via button (Issue #682) */
} balance_state_t; } balance_state_t;
typedef struct { typedef struct {
@ -47,7 +46,5 @@ void balance_init(balance_t *b);
void balance_update(balance_t *b, const IMUData *imu, float dt); void balance_update(balance_t *b, const IMUData *imu, float dt);
void balance_arm(balance_t *b); void balance_arm(balance_t *b);
void balance_disarm(balance_t *b); void balance_disarm(balance_t *b);
void balance_park(balance_t *b); /* ARMED -> PARKED: freeze PID, zero motors (Issue #682) */
void balance_unpark(balance_t *b); /* PARKED -> ARMED if pitch < 20 deg (Issue #682) */
#endif #endif

81
include/can_driver.h Normal file
View File

@ -0,0 +1,81 @@
#ifndef CAN_DRIVER_H
#define CAN_DRIVER_H
#include <stdint.h>
#include <stdbool.h>
/* CAN bus driver for BLDC motor controllers (Issue #597)
* CAN2 on PB12 (RX, AF9) / PB13 (TX, AF9) at 500 kbps
* APB1 = 54 MHz: PSC=6, BS1=13tq, BS2=4tq, SJW=1tq 18 tq/bit = 500 kbps
*/
/* Node IDs */
#define CAN_NUM_MOTORS 2u
#define CAN_NODE_LEFT 0u
#define CAN_NODE_RIGHT 1u
/* CAN frame IDs */
#define CAN_ID_VEL_CMD_BASE 0x100u /* TX: 0x100 + node_id — velocity/torque command */
#define CAN_ID_ENABLE_CMD_BASE 0x110u /* TX: 0x110 + node_id — enable/disable */
#define CAN_ID_FEEDBACK_BASE 0x200u /* RX: 0x200 + node_id — position/velocity/current */
/* Filter: accept standard IDs 0x2000x21F */
#define CAN_FILTER_STDID 0x200u
#define CAN_FILTER_MASK 0x7E0u
/* Bit timing (500 kbps @ 54 MHz APB1) */
#define CAN_PRESCALER 6u
/* TX rate */
#define CAN_TX_RATE_HZ 100u
/* Node alive timeout */
#define CAN_NODE_TIMEOUT_MS 100u
/* TX command frame (8 bytes payload, DLC=4 for vel cmd) */
typedef struct {
int16_t velocity_rpm; /* target RPM (+/- = fwd/rev) */
int16_t torque_x100; /* torque limit × 100 (0 = unlimited) */
} can_cmd_t;
/* RX feedback frame (DLC=8) */
typedef struct {
int16_t velocity_rpm; /* actual RPM */
int16_t current_ma; /* phase current in mA */
int16_t position_x100; /* position × 100 (degrees or encoder counts) */
int8_t temperature_c; /* controller temperature °C */
uint8_t fault; /* fault flags (0 = healthy) */
uint32_t last_rx_ms; /* HAL_GetTick() at last valid frame */
} can_feedback_t;
/* Bus statistics */
typedef struct {
uint32_t tx_count; /* frames transmitted */
uint32_t rx_count; /* frames received */
uint16_t err_count; /* HAL-level errors */
uint8_t bus_off; /* 1 = bus-off state */
uint8_t _pad;
} can_stats_t;
/* Initialise CAN2 peripheral, GPIO, and filter bank 14 */
void can_driver_init(void);
/* Send velocity+torque command to one node */
void can_driver_send_cmd(uint8_t node_id, const can_cmd_t *cmd);
/* Send enable/disable command to one node */
void can_driver_send_enable(uint8_t node_id, bool enable);
/* Copy latest feedback snapshot (returns false if node never heard from) */
bool can_driver_get_feedback(uint8_t node_id, can_feedback_t *out);
/* Returns true if node has been heard within CAN_NODE_TIMEOUT_MS */
bool can_driver_is_alive(uint8_t node_id, uint32_t now_ms);
/* Copy bus statistics snapshot */
void can_driver_get_stats(can_stats_t *out);
/* Drain RX FIFO0; call every main-loop tick */
void can_driver_process(void);
#endif /* CAN_DRIVER_H */

View File

@ -257,52 +257,9 @@
#define GIMBAL_PAN_LIMIT_DEG 180.0f // pan soft limit (deg each side) #define GIMBAL_PAN_LIMIT_DEG 180.0f // pan soft limit (deg each side)
#define GIMBAL_TILT_LIMIT_DEG 90.0f // tilt soft limit (deg each side) #define GIMBAL_TILT_LIMIT_DEG 90.0f // tilt soft limit (deg each side)
// --- CAN Bus Driver (Issue #597, remapped Issue #676) --- // --- CAN Bus Driver (Issue #597) ---
// CAN1 on PB8 (RX, AF9) / PB9 (TX, AF9) — SCL/SDA pads on Mamba F722S MK2 // CAN2 on PB12 (RX, AF9) / PB13 (TX, AF9) — repurposes SPI2/OSD pins (unused on balance bot)
// I2C1 freed: BME280 moved to I2C2 (PB10/PB11); PB8/PB9 repurposed for CAN1
#define CAN_RPM_SCALE 10 // motor_cmd to RPM: 1 cmd count = 10 RPM #define CAN_RPM_SCALE 10 // motor_cmd to RPM: 1 cmd count = 10 RPM
#define CAN_TLM_HZ 1u // JLINK_TLM_CAN_STATS transmit rate (Hz) #define CAN_TLM_HZ 1u // JLINK_TLM_CAN_STATS transmit rate (Hz)
// --- LVC: Low Voltage Cutoff (Issue #613) ---
// 3-stage undervoltage protection; voltages in mV
#define LVC_WARNING_MV 21000u // 21.0 V -- buzzer alert, full power
#define LVC_CRITICAL_MV 19800u // 19.8 V -- 50% motor power reduction
#define LVC_CUTOFF_MV 18600u // 18.6 V -- motors disabled, latch until reboot
#define LVC_HYSTERESIS_MV 200u // recovery hysteresis to prevent threshold chatter
#define LVC_TLM_HZ 1u // JLINK_TLM_LVC transmit rate (Hz)
// --- UART Command Protocol (Issue #629) ---
// Jetson-STM32 binary command protocol on UART5 (PC12/PD2)
// NOTE: Spec requested USART1 @ 115200; USART1 is occupied by JLink @ 921600.
#define UART_PROT_BAUD 115200u // baud rate for UART5 Jetson protocol
#define UART_PROT_HB_TIMEOUT_MS 500u // heartbeat timeout: Jetson considered lost after 500 ms
// --- Encoder Odometry (Issue #632) ---
// Left encoder: TIM2 (32-bit), CH1=PA15 (AF1), CH2=PB3 (AF1)
// Right encoder: TIM3 (16-bit), CH1=PC6 (AF2), CH2=PC7 (AF2)
// Encoder mode 3: count on both A and B edges (x4 resolution)
#define ENC_LEFT_TIM TIM2
#define ENC_LEFT_CH1_PORT GPIOA
#define ENC_LEFT_CH1_PIN GPIO_PIN_15 // TIM2_CH1, AF1
#define ENC_LEFT_CH2_PORT GPIOB
#define ENC_LEFT_CH2_PIN GPIO_PIN_3 // TIM2_CH2, AF1
#define ENC_LEFT_AF GPIO_AF1_TIM2
#define ENC_RIGHT_TIM TIM3
#define ENC_RIGHT_CH1_PORT GPIOC
#define ENC_RIGHT_CH1_PIN GPIO_PIN_6 // TIM3_CH1, AF2
#define ENC_RIGHT_CH2_PORT GPIOC
#define ENC_RIGHT_CH2_PIN GPIO_PIN_7 // TIM3_CH2, AF2
#define ENC_RIGHT_AF GPIO_AF2_TIM3
// --- Hardware Button (Issue #682) ---
// Active-low push button on PC2 (internal pull-up)
#define BTN_PORT GPIOC
#define BTN_PIN GPIO_PIN_2
#define BTN_DEBOUNCE_MS 20u // ms debounce window
#define BTN_LONG_MIN_MS 1500u // ms threshold: LONG press
#define BTN_COMMIT_MS 500u // ms quiet after lone SHORT -> PARK event
#define BTN_SEQ_TIMEOUT_MS 3000u // ms: sequence window; expired buffer abandoned
#endif // CONFIG_H #endif // CONFIG_H

View File

@ -50,9 +50,6 @@
* 0x87 FAULT_LOG - jlink_tlm_fault_log_t (20 bytes), sent on boot + FAULT_LOG_GET (Issue #565) * 0x87 FAULT_LOG - jlink_tlm_fault_log_t (20 bytes), sent on boot + FAULT_LOG_GET (Issue #565)
* 0x88 SLOPE - jlink_tlm_slope_t (4 bytes), sent at SLOPE_TLM_HZ (Issue #600) * 0x88 SLOPE - jlink_tlm_slope_t (4 bytes), sent at SLOPE_TLM_HZ (Issue #600)
* 0x89 CAN_STATS - jlink_tlm_can_stats_t (16 bytes), sent at CAN_TLM_HZ + CAN_STATS_GET (Issue #597) * 0x89 CAN_STATS - jlink_tlm_can_stats_t (16 bytes), sent at CAN_TLM_HZ + CAN_STATS_GET (Issue #597)
* 0x8A STEERING - jlink_tlm_steering_t (8 bytes), sent at STEER_TLM_HZ (Issue #616)
* 0x8B LVC - jlink_tlm_lvc_t (4 bytes), sent at LVC_TLM_HZ (Issue #613)
* 0x8C ODOM - jlink_tlm_odom_t (16 bytes), sent at ENC_TLM_HZ (Issue #632)
* *
* Priority: CRSF RC always takes precedence. Jetson steer/speed only applied * Priority: CRSF RC always takes precedence. Jetson steer/speed only applied
* when mode_manager_active() == MODE_AUTONOMOUS (CH6 high). In RC_MANUAL and * when mode_manager_active() == MODE_AUTONOMOUS (CH6 high). In RC_MANUAL and
@ -96,12 +93,6 @@
#define JLINK_TLM_FAULT_LOG 0x87u /* jlink_tlm_fault_log_t (20 bytes, Issue #565) */ #define JLINK_TLM_FAULT_LOG 0x87u /* jlink_tlm_fault_log_t (20 bytes, Issue #565) */
#define JLINK_TLM_SLOPE 0x88u /* jlink_tlm_slope_t (4 bytes, Issue #600) */ #define JLINK_TLM_SLOPE 0x88u /* jlink_tlm_slope_t (4 bytes, Issue #600) */
#define JLINK_TLM_CAN_STATS 0x89u /* jlink_tlm_can_stats_t (16 bytes, Issue #597) */ #define JLINK_TLM_CAN_STATS 0x89u /* jlink_tlm_can_stats_t (16 bytes, Issue #597) */
#define JLINK_TLM_STEERING 0x8Au /* jlink_tlm_steering_t (8 bytes, Issue #616) */
#define JLINK_TLM_LVC 0x8Bu /* jlink_tlm_lvc_t (4 bytes, Issue #613) */
#define JLINK_TLM_ODOM 0x8Cu /* jlink_tlm_odom_t (16 bytes, Issue #632) */
#define JLINK_TLM_BARO 0x8Du /* jlink_tlm_baro_t (12 bytes, Issue #672) */
#define JLINK_TLM_VESC_STATE 0x8Eu /* jlink_tlm_vesc_state_t (22 bytes, Issue #674) */
#define JLINK_TLM_CAN_WDOG 0x8Fu /* jlink_tlm_can_wdog_t (16 bytes, Issue #694) */
/* ---- Telemetry STATUS payload (20 bytes, packed) ---- */ /* ---- Telemetry STATUS payload (20 bytes, packed) ---- */
typedef struct __attribute__((packed)) { typedef struct __attribute__((packed)) {
@ -213,73 +204,6 @@ typedef struct __attribute__((packed)) {
int16_t vel1_rpm; /* node 1 current velocity (RPM) */ int16_t vel1_rpm; /* node 1 current velocity (RPM) */
} jlink_tlm_can_stats_t; /* 16 bytes */ } jlink_tlm_can_stats_t; /* 16 bytes */
/* ---- Telemetry STEERING payload (8 bytes, packed) Issue #616 ---- */
/* Published at STEER_TLM_HZ (10 Hz); reports yaw-rate PID state. */
typedef struct __attribute__((packed)) {
int16_t target_x10; /* target yaw rate, deg/s × 10 (0.1 deg/s resolution) */
int16_t actual_x10; /* measured yaw rate, deg/s × 10 */
int16_t output; /* differential motor output (-STEER_OUTPUT_MAX..+MAX) */
uint8_t enabled; /* 1 = PID active */
uint8_t _pad; /* reserved */
} jlink_tlm_steering_t; /* 8 bytes */
/* ---- Telemetry LVC payload (4 bytes, packed) Issue #613 ---- */
/* Sent at LVC_TLM_HZ (1 Hz); reports battery voltage and LVC protection state. */
typedef struct __attribute__((packed)) {
uint16_t voltage_mv; /* battery voltage (mV) */
uint8_t percent; /* 0-100: fuel gauge within CUTOFF..WARNING; 255=unknown */
uint8_t protection_state; /* LvcState: 0=NORMAL,1=WARNING,2=CRITICAL,3=CUTOFF */
} jlink_tlm_lvc_t; /* 4 bytes */
/* ---- Telemetry ODOM payload (16 bytes, packed) Issue #632 ---- */
/* Sent at ENC_TLM_HZ (50 Hz); wheel RPM, pose, and linear speed. */
typedef struct __attribute__((packed)) {
int16_t rpm_left; /* left wheel RPM (signed; + = forward) */
int16_t rpm_right; /* right wheel RPM (signed) */
int32_t x_mm; /* forward displacement (mm, int32) */
int32_t y_mm; /* lateral displacement (mm, int32) */
int16_t theta_cdeg; /* heading in centidegrees (0.01 deg steps) */
int16_t speed_mmps; /* linear speed of centre point (mm/s) */
} jlink_tlm_odom_t; /* 16 bytes */
/* ---- Telemetry BARO payload (12 bytes, packed) Issue #672 ---- */
/* Sent at BARO_TLM_HZ (1 Hz); reports ambient pressure, temperature, altitude. */
typedef struct __attribute__((packed)) {
int32_t pressure_pa; /* barometric pressure (Pa) */
int16_t temp_x10; /* ambient temperature (°C × 10; e.g. 235 = 23.5 °C) */
int32_t alt_cm; /* pressure altitude above ISA sea level (cm) */
int16_t humidity_pct_x10; /* %RH × 10 (BME280 only); -1 = BMP280/absent */
} jlink_tlm_baro_t; /* 12 bytes */
/* ---- Telemetry CAN_WDOG payload (16 bytes, packed) Issue #694 ---- */
/* Sent at 1 Hz; reports CAN bus-error severity and restart history. */
typedef struct __attribute__((packed)) {
uint32_t restart_count; /* SW bus-off restarts since boot */
uint32_t busoff_count; /* lifetime bus-off entry events */
uint16_t errpassive_count; /* error-passive transitions */
uint16_t errwarn_count; /* error-warning transitions */
uint8_t error_state; /* can_error_state_t: 0=OK,1=WARN,2=EP,3=BOFF */
uint8_t tec; /* transmit error counter (ESR[23:16]) */
uint8_t rec; /* receive error counter (ESR[31:24]) */
uint8_t _pad; /* reserved */
} jlink_tlm_can_wdog_t; /* 16 bytes */
/* ---- Telemetry VESC_STATE payload (22 bytes, packed) Issue #674 ---- */
/* Sent at VESC_TLM_HZ (1 Hz) by vesc_can_send_tlm(). */
typedef struct __attribute__((packed)) {
int32_t left_rpm; /* left VESC actual RPM */
int32_t right_rpm; /* right VESC actual RPM */
int16_t left_current_x10; /* left phase current (A × 10) */
int16_t right_current_x10; /* right phase current (A × 10) */
int16_t left_temp_x10; /* left FET temperature (°C × 10) */
int16_t right_temp_x10; /* right FET temperature (°C × 10) */
int16_t voltage_x10; /* input voltage (V × 10; from STATUS_5) */
uint8_t left_fault; /* left VESC fault code (0 = none) */
uint8_t right_fault; /* right VESC fault code (0 = none) */
uint8_t left_alive; /* 1 = left VESC alive (STATUS within 1 s) */
uint8_t right_alive; /* 1 = right VESC alive (STATUS within 1 s) */
} jlink_tlm_vesc_state_t; /* 22 bytes */
/* ---- Volatile state (read from main loop) ---- */ /* ---- Volatile state (read from main loop) ---- */
typedef struct { typedef struct {
/* Drive command - updated on JLINK_CMD_DRIVE */ /* Drive command - updated on JLINK_CMD_DRIVE */
@ -398,44 +322,4 @@ void jlink_send_slope_tlm(const jlink_tlm_slope_t *tlm);
*/ */
void jlink_send_can_stats(const jlink_tlm_can_stats_t *tlm); void jlink_send_can_stats(const jlink_tlm_can_stats_t *tlm);
/*
* jlink_send_steering_tlm(tlm) - transmit JLINK_TLM_STEERING (0x8A) frame
* (14 bytes total) to Jetson. Issue #616.
* Rate-limiting is handled by steering_pid_send_tlm(); call from there only.
*/
void jlink_send_steering_tlm(const jlink_tlm_steering_t *tlm);
/*
* jlink_send_lvc_tlm(tlm) - transmit JLINK_TLM_LVC (0x8B) frame
* (10 bytes total) at LVC_TLM_HZ (1 Hz). Issue #613.
*/
void jlink_send_lvc_tlm(const jlink_tlm_lvc_t *tlm);
/*
* jlink_send_odom_tlm(tlm) - transmit JLINK_TLM_ODOM (0x8C) frame
* (22 bytes total) at ENC_TLM_HZ (50 Hz). Issue #632.
* Rate-limiting handled by encoder_odom_send_tlm(); call from there only.
*/
void jlink_send_odom_tlm(const jlink_tlm_odom_t *tlm);
/*
* jlink_send_baro_tlm(tlm) - transmit JLINK_TLM_BARO (0x8D) frame
* (18 bytes total) at BARO_TLM_HZ (1 Hz). Issue #672.
* Rate-limiting handled by baro_tick(); call from there only.
*/
void jlink_send_baro_tlm(const jlink_tlm_baro_t *tlm);
/*
* jlink_send_vesc_state_tlm(tlm) - transmit JLINK_TLM_VESC_STATE (0x8E) frame
* (28 bytes total) at VESC_TLM_HZ (1 Hz). Issue #674.
* Rate-limiting handled by vesc_can_send_tlm(); call from there only.
*/
void jlink_send_vesc_state_tlm(const jlink_tlm_vesc_state_t *tlm);
/*
* jlink_send_can_wdog_tlm(tlm) - transmit JLINK_TLM_CAN_WDOG (0x8F) frame
* (22 bytes total) at 1 Hz. Issue #694.
*/
void jlink_send_can_wdog_tlm(const jlink_tlm_can_wdog_t *tlm);
#endif /* JLINK_H */ #endif /* JLINK_H */

View File

@ -9,7 +9,6 @@ typedef struct {
float pitch_rate; // degrees/sec (raw gyro pitch axis) float pitch_rate; // degrees/sec (raw gyro pitch axis)
float roll; // degrees, filtered (complementary filter) float roll; // degrees, filtered (complementary filter)
float yaw; // degrees, gyro-integrated (drifts — no magnetometer) float yaw; // degrees, gyro-integrated (drifts — no magnetometer)
float yaw_rate; // degrees/sec (raw gyro Z / board_gz, Issue #616)
float accel_x; // g float accel_x; // g
float accel_z; // g float accel_z; // g
} IMUData; } IMUData;
@ -29,15 +28,4 @@ bool mpu6000_is_calibrated(void);
void mpu6000_read(IMUData *data); void mpu6000_read(IMUData *data);
/*
* mpu6000_set_mount_offset(pitch_deg, roll_deg) set mount angle offsets.
* These are subtracted from the pitch and roll outputs in mpu6000_read().
* Load via imu_cal_flash_load() on boot; update after 'O' CDC command.
* Issue #680.
*/
void mpu6000_set_mount_offset(float pitch_deg, float roll_deg);
/* Returns true if non-zero mount offsets have been applied (Issue #680). */
bool mpu6000_has_mount_offset(void);
#endif #endif

Some files were not shown because too many files have changed in this diff Show More