Compare commits

...

1 Commits

Author SHA1 Message Date
mark
ffda15e3ec feat: Add parking lot line marking system spec (Serkan use case)
Comprehensive hardware + software spec for SAUL-TEE autonomous parking
lot striping system. Covers paint dispensing hardware, control electronics
(solenoid/relay/GPIO), RTK GPS positioning strategy, ROS2 node architecture,
MQTT topics, and parking lot templates (ADA, standard, angled, symbols).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 10:24:34 -04:00

View File

@ -0,0 +1,534 @@
# Parking Lot Line Marking System — Hardware + Software Spec
**Platform:** SAUL-TEE Robot
**Use case:** Serkan — Autonomous parking lot striping
**Author:** mark (parking marking agent)
**Date:** 2026-04-07
**Status:** Draft v1.0
---
## Table of Contents
1. [System Overview](#1-system-overview)
2. [Paint Dispensing Hardware](#2-paint-dispensing-hardware)
3. [Control Electronics](#3-control-electronics)
4. [Positioning Strategy](#4-positioning-strategy)
5. [Software Architecture](#5-software-architecture)
6. [Parking Lot Templates](#6-parking-lot-templates)
7. [Bill of Materials](#7-bill-of-materials)
8. [Safety](#8-safety)
9. [Open Items](#9-open-items)
---
## 1. System Overview
SAUL-TEE autonomously traverses a parking lot surface while a rear-mounted paint dispensing system sprays crisp lines. The Jetson Orin drives RTK-corrected path following via differential VESC drive. The ESP32 IO board triggers a solenoid valve via relay on demand. Lines are generated from a layout file (SVG or JSON) and executed as a sequence of waypoints with paint-on/off events.
```
[Layout File] → [Layout Planner node]
[Path Follower node] ← RTK/UWB pose
[Paint Controller node] → GPIO → relay → solenoid → spray
```
---
## 2. Paint Dispensing Hardware
### 2.1 Spray Mechanism — Selected: Pressurized Airless Canister
Three options evaluated:
| Option | Pros | Cons | Verdict |
|--------|------|------|---------|
| **Pressurized aerosol canister** | Cheap, no pump, self-contained, easy swap | ~500 mL, limited volume, cold-sensitive | **Selected for v1** |
| Airless diaphragm pump + reservoir | Large capacity, refillable, consistent | Pump adds complexity, power draw ~3 A | v2 option |
| Gravity-fed dip tube | Simple, zero power | Requires height differential, drips on stop | Rejected |
**v1 approach:** Modified professional marking paint canister (e.g., Rustoleum Marking Chalk, inverted-spray capable) + normally-closed solenoid valve inline with canister outlet tube. Canister pressure (~50 PSI) propels paint through the solenoid to a fan nozzle aimed at the ground.
### 2.2 Nozzle Selection
| Line Width | Nozzle | Tip Orifice | Height from Ground | Notes |
|------------|--------|-------------|-------------------|-------|
| 4" standard parking line | Flat fan 40° | 0.017" | 810 cm | Covers standard stripe width at 50 PSI |
| 2" detail / curb line | Flat fan 20° | 0.013" | 68 cm | Narrower pattern |
| 6" fire lane | Flat fan 65° | 0.021" | 1012 cm | Wide spray, lower pressure preferred |
**Recommended nozzle:** Graco RAC X 515 (flat fan, 10" fan width at 12" height) for 4" lines. Reversible tip for unclogging.
**Critical:** Nozzle tip height must be fixed relative to ground. Spring-loaded mount or rigid bracket set to calibrated height.
### 2.3 Solenoid Valve
- **Type:** Normally-closed (NC) 12 V DC solenoid valve
- **Body:** Brass or stainless, 1/4" NPT female ports
- **Pressure rating:** ≥ 100 PSI (canister runs at ~50 PSI)
- **Response time:** < 20 ms open/close (critical for line start/stop accuracy)
- **Current draw:** ~500 mA @ 12 V when open
- **Recommended part:** U.S. Solid USS-MSV00006 1/4" NC 12 V (≈ $18) or equivalent
**Flyback diode:** 1N4007 across solenoid coil terminals (cathode to +12 V).
### 2.4 Paint Reservoir / Canister Mount
**v1 — Inverted aerosol canister:**
- Standard 17 oz / 500 mL marking paint canister (inverted nozzle type)
- Custom bracket: 3 mm aluminum plate, two 3" U-bolt clamps for canister body
- Mount behind rear castor, centered on robot longitudinal axis
- Canister outlet → 1/4" OD PTFE tube → solenoid → nozzle
**v2 — Refillable reservoir:**
- 2 L HDPE pressure pot (Harbor Freight or similar, ~$35)
- External 12 V mini air compressor regulating to 3060 PSI
- Float-level sensor (reed switch) for paint level monitoring
### 2.5 Mounting Bracket
```
Top rail (80/20 T-slot or robot frame)
├─── Canister bracket (rear, vertical, above castor)
│ U-bolts × 2 clamping canister body
│ QD fitting for tube swap
└─── Nozzle arm (extends aft ~200 mm, 50 mm above ground target height)
Adjustable height slot (bolt + lock nut)
Solenoid mounted inline, protected by splash shield
```
**Key dimension:** Nozzle center is 150 mm behind rear axle center. This offset is compensated in software — spray-on is triggered when the nozzle position (not robot center) reaches the line start point.
### 2.6 Power Requirements
| Component | Voltage | Current | Source |
|-----------|---------|---------|--------|
| Solenoid valve | 12 V | 0.5 A peak (intermittent) | Robot 12 V rail |
| Relay module | 5 V logic, 12 V switched | < 50 mA control | 5 V rail via IO GPIO |
| RTK GPS module | 5 V | 100200 mA | 5 V rail |
| (v2) Mini compressor | 12 V | 35 A | Direct from 36 V via 12 V DC-DC |
Total paint system power budget: **< 1 A continuous** (solenoid duty cycle ~20%).
---
## 3. Control Electronics
### 3.1 Signal Chain: Orin → Spray
```
Jetson Orin (ROS2 node)
│ publishes /paint/cmd (Bool)
[CAN or MQTT] ← preferred: direct GPIO on Orin Jetson 40-pin header
Orin GPIO pin (3.3 V logic)
Level shifter (3.3 V → 5 V) [optional, if relay module needs 5 V]
5 V relay module (optocoupler isolated, SPDT, 10 A contacts)
│ NO contact → +12 V
│ COM → solenoid +
│ solenoid → GND
Solenoid valve (NC, 12 V)
Spray nozzle
```
### 3.2 Jetson Orin GPIO
The Jetson Orin Nano / AGX Orin 40-pin expansion header exposes GPIOs via `Jetson.GPIO` Python library or `/sys/class/gpio`.
| Signal | Orin Pin | GPIO # | Notes |
|--------|----------|--------|-------|
| SPRAY_EN | Pin 11 | GPIO09 (Tegra SOC) | Active high → relay ON → solenoid opens |
| ESTOP_IN | Pin 12 | GPIO08 | Monitor hardware kill switch state |
Use `Jetson.GPIO` in ROS2 node:
```python
import Jetson.GPIO as GPIO
SPRAY_PIN = 11
GPIO.setmode(GPIO.BOARD)
GPIO.setup(SPRAY_PIN, GPIO.OUT, initial=GPIO.LOW)
GPIO.output(SPRAY_PIN, GPIO.HIGH) # spray on
GPIO.output(SPRAY_PIN, GPIO.LOW) # spray off
```
### 3.3 ESP32 IO Board — Alternative / Backup Path
If direct Orin GPIO is not preferred, route through the IO board.
**Available IO pins (from SAUL-TEE-SYSTEM-REFERENCE.md):**
| GPIO | Current Use | Available? |
|------|-------------|-----------|
| IO14 | Horn/buzzer | Yes (shared, low duty) |
| IO15 | Headlight | Yes (can multiplex) |
| **IO16** | Fan (if ELRS not fitted) | **Yes — recommended for spray relay** |
**Recommended:** IO16 on ESP32 IO board → relay → solenoid.
Add new UART message type for spray control:
```
IO Board extension — TYPE 0x04: SPRAY_CMD
Payload: uint8 enable (0=off, 1=on), uint16 duration_ms (0=indefinite)
```
Orin sends via MQTT → ESP32 MQTT bridge → UART to IO board → GPIO16.
### 3.4 Safety Interlocks
| Condition | Action |
|-----------|--------|
| E-STOP (RC CH6 > 1500 µs) | IO board immediately pulls IO16 LOW (spray off) before forwarding FAULT to BALANCE |
| Tilt > ±25° | BALANCE sends FAULT → Orin paint controller kills spray |
| Robot velocity = 0 (stationary) | paint_controller node holds spray off (prevents puddles) |
| RC loss > 100 ms | FAULT propagated → spray off |
| MQTT disconnected > 5 s | paint_controller enters SAFE mode, spray off |
**IO board firmware addition:** In the FAULT frame handler (TYPE 0x03), ensure IO16 is driven LOW before sending any FAULT frame upward. Zero-latency interlock at MCU level.
### 3.5 Flow Rate Sensor (Optional — v2)
- **Type:** YF-S201 Hall-effect flow sensor, 1/2" inline
- **Signal:** Pulse output, 7.5 pulses per mL
- **Connected to:** IO board I2C bus interrupt pin or free GPIO
- **Purpose:** Detect clog (rate drops to zero during spray command), estimate paint consumed
---
## 4. Positioning Strategy
### 4.1 Accuracy Requirements
| Parameter | Requirement | Rationale |
|-----------|-------------|-----------|
| Lateral position error | < ±2 cm | 4" (10 cm) line — 2 cm error leaves 3 cm margin |
| Heading error | < ±0.5° | Over 20 m line, 0.5° heading error = 17 cm drift — marginal but acceptable with frequent corrections |
| Longitudinal (along-line) error | < ±5 cm | Controls spray on/off timing |
| Update rate | ≥ 5 Hz | At 0.3 m/s robot speed, 5 Hz gives 6 cm per update |
**Conclusion:** Phone GPS (±5 m) alone is **insufficient**. RTK GPS (±2 cm) is required for production use.
### 4.2 RTK GPS Module
**Recommended:** u-blox ZED-F9P (standalone RTK receiver)
| Spec | Value |
|------|-------|
| RTK accuracy (fixed) | 1 cm + 1 ppm horizontal |
| Heading (dual antenna) | 0.3° RMS |
| Update rate | 10 Hz (RTK), 20 Hz raw |
| Interface | UART (NMEA + UBX), USB |
| Power | 5 V, ~150 mA |
| Module board | SparkFun GPS-RTK2 (ZED-F9P) ~$250, or ArduSimple simpleRTK2B ~$200 |
**NTRIP correction source:** Use a local CORS network or set up a second ZED-F9P base station at a known point on the parking lot boundary. Base → Orin NTRIP client → RTK corrections to rover.
**Antenna placement:** Center of robot, clear sky view. Keep > 30 cm from metal frame edges.
### 4.3 Dual Antenna Heading (Moving Baseline RTK)
Use two ZED-F9P modules (or one ZED-F9P + ZED-F9H) with antennas at robot front and rear, separated by ≥ 50 cm. This gives direct heading from GPS — no magnetometer drift issues.
| Antenna separation | Heading accuracy |
|-------------------|-----------------|
| 50 cm | ~1.1° RMS |
| 100 cm | ~0.57° RMS |
| 150 cm | ~0.38° RMS |
**Recommended:** 100 cm baseline along robot longitudinal axis → heading accuracy 0.57° RMS.
### 4.4 Sensor Fusion Architecture
```
[ZED-F9P RTK rover] ── UART ──→ robot_localization EKF
[Phone GPS MQTT bridge] ──────→ (fallback, coarse)
[UWB tag] ────────────────────→ (near base station, ±2 cm augment)
[IMU - QMI8658 via CAN 0x400]──→ dead reckoning between GPS updates
/saltybot/pose (geometry_msgs/PoseWithCovarianceStamped)
```
- **robot_localization** (ROS2 EKF node) fuses GPS + IMU
- RTK GPS is primary when fixed; covariance set high when float/no fix
- UWB active near parking lot entry/exit points (anchor infrastructure needed)
- Phone GPS used for coarse positioning only, filtered out when RTK fixed
### 4.5 Wheel Odometry
At 0.3 m/s nominal painting speed, the VESC RPM telemetry (CAN 0x401) provides ~1 cm odometry resolution. Combine with IMU heading for dead-reckoning between RTK updates.
---
## 5. Software Architecture
### 5.1 ROS2 Node Graph
```
/layout_planner
Reads: layout JSON file
Publishes: /paint/path (nav_msgs/Path), /paint/zones (PaintZone[])
/path_follower
Subscribes: /paint/path, /saltybot/pose
Publishes: /cmd_vel (geometry_msgs/Twist) → CAN 0x300 bridge
Algorithm: Pure Pursuit with adaptive lookahead (0.3 m at low speed)
/paint_controller
Subscribes: /saltybot/pose, /paint/zones
Publishes: /paint/spray_cmd (std_msgs/Bool) → GPIO
Logic: nozzle-offset compensation, edge start/stop, velocity interlock
/rtk_bridge
Reads: ZED-F9P UART (NMEA GGA, UBX-NAV-PVT)
Publishes: /gps/fix (sensor_msgs/NavSatFix), /gps/heading (Float64)
/paint_monitor (MQTT bridge)
Bridges: /paint/status → MQTT saltybot/mark/status
Bridges: MQTT saltybot/mark/cmd → /paint/cmd
```
### 5.2 MQTT Topics
| Topic | Direction | Payload | Description |
|-------|-----------|---------|-------------|
| `saltybot/mark/status` | Orin → broker | JSON | Current state: pose, spray_state, progress_pct, paint_remaining |
| `saltybot/mark/cmd` | broker → Orin | JSON | Commands: start, stop, pause, load_layout |
| `saltybot/mark/layout` | broker → Orin | JSON | Full layout upload |
| `saltybot/mark/fault` | Orin → broker | JSON | Fault events: e-stop, gps_lost, clog_detected |
| `saltybot/mark/spray` | Orin → ESP32 IO | `{"en":1}` | Direct spray relay command (fallback path) |
### 5.3 Layout File Format (JSON)
```json
{
"version": 1,
"origin": {"lat": 37.4219983, "lon": -122.084},
"lines": [
{
"id": "row_A_space_1_left",
"type": "stripe",
"width_mm": 100,
"start": {"x_m": 0.0, "y_m": 0.0},
"end": {"x_m": 5.5, "y_m": 0.0}
}
],
"symbols": [
{
"id": "hc_symbol_1",
"type": "handicap_iso",
"center": {"x_m": 12.0, "y_m": 2.3},
"heading_deg": 0
}
]
}
```
Coordinates are in a local ENU (East-North-Up) frame anchored at `origin`.
### 5.4 path_follower — Pure Pursuit
```python
class PathFollower(Node):
LOOKAHEAD = 0.35 # m — tunable
MAX_SPEED = 0.3 # m/s during painting
NOZZLE_OFFSET = -0.15 # m aft of rear axle
def compute_cmd_vel(self, pose, path):
# Find lookahead point on path
# Compute curvature κ = 2*sin(α) / L
# angular_vel = κ * linear_vel
...
```
**Speed:** 0.3 m/s nominal (≈ 1 ft/s). At this speed, 100 m line ≈ 5.5 min.
### 5.5 paint_controller — Spray Logic
```python
NOZZLE_OFFSET_M = -0.15 # nozzle is 15 cm behind robot center (rear)
def nozzle_pose(robot_pose):
# Project robot pose backward by offset along heading
...
def on_pose_update(self, pose):
nozzle = nozzle_pose(pose)
# Check if nozzle is within any active paint zone
in_zone = any(zone.contains(nozzle) for zone in self.active_zones)
# Velocity interlock: must be moving > 0.05 m/s
moving = self.current_speed > 0.05
spray = in_zone and moving and not self.fault
self.set_spray(spray)
```
### 5.6 Integration with Existing Nodes
| Existing node | Integration point |
|---------------|------------------|
| `ios_gps_bridge` (Issue #681) | Provides fallback /gps/fix when RTK not fixed |
| VESC CAN bridge | path_follower publishes to /cmd_vel → existing bridge forwards to CAN 0x300 |
| `saul_tee_driver` | Subscribes /cmd_vel — no changes needed |
| UWB tag node | robot_localization already fuses UWB range — paint system uses same /pose topic |
---
## 6. Parking Lot Templates
All templates follow **MUTCD (Manual on Uniform Traffic Control Devices)** and **ADA** standards.
### 6.1 Standard Dimensions
| Space Type | Width | Depth | Line Width | Notes |
|------------|-------|-------|-----------|-------|
| Standard | 9 ft (2.74 m) | 18 ft (5.49 m) | 4 in (102 mm) | Most common |
| Compact | 8 ft (2.44 m) | 16 ft (4.88 m) | 4 in | Must be labeled |
| ADA accessible | 13 ft (3.96 m) | 18 ft | 4 in | Includes 5' access aisle |
| Van accessible | 16 ft (4.88 m) | 18 ft | 4 in | 8' space + 8' aisle |
| Fire lane | varies | — | 6 in (152 mm) | Red or yellow, MUTCD |
### 6.2 Stall Angle Templates
| Angle | Module width | Drive aisle |
|-------|-------------|------------|
| 90° (perpendicular) | 9.0 ft stall + 9.0 ft module | 24 ft two-way |
| 60° angled | 10.4 ft module | 18 ft one-way |
| 45° angled | 12.7 ft module | 13 ft one-way |
| Parallel | 23 ft length | 12 ft one-way |
### 6.3 Symbol Templates
#### Handicap Symbol (ISA — International Symbol of Access)
Standard 60" × 60" (1.52 m × 1.52 m) wheelchair figure, blue paint. Consists of:
- Head circle: 6" diameter
- Body and wheel segments: 46" strokes
Robot approach: rasterize ISA symbol into a grid of parallel stripe passes at 2" nozzle width, ~30 passes per symbol.
#### Arrow Templates
| Type | Dimensions | Use |
|------|-----------|-----|
| Straight arrow | 6" wide × 36" long shaft + 18" head | One-way lane |
| Curved arrow | 6" wide, 10 ft radius | Turn lane |
| Double-headed | 6" wide | Two-way reminder |
#### Fire Lane Markings
- "FIRE LANE — NO PARKING" text: 12" tall letters, 4" stroke
- Red curb paint: continuous stripe, 6" wide on curb face (robot with angle-mounted nozzle)
- MUTCD crosshatch: 6" lines at 45°, 18" spacing
### 6.4 Layout Design Tool (CLI)
A Python script `tools/parking_layout.py` will generate a layout JSON from simple parameters:
```bash
# Generate a 10-space 90° lot section
python tools/parking_layout.py \
--type 90deg \
--spaces 10 \
--rows 2 \
--origin 37.4219983,-122.084 \
--output lot_A.json
# Preview as SVG
python tools/parking_layout.py --input lot_A.json --svg lot_A.svg
```
---
## 7. Bill of Materials
### 7.1 Paint Dispensing System
| Item | Part / Source | Qty | Unit Cost | Total |
|------|--------------|-----|-----------|-------|
| Inverted marking paint canister (Rustoleum Professional) | Home Depot / Rustoleum | 4 | $8 | $32 |
| NC solenoid valve 12 V 1/4" NPT | U.S. Solid / Amazon | 1 | $18 | $18 |
| 5 V single-channel relay module | Amazon | 1 | $5 | $5 |
| Graco RAC X 515 reversible tip | Paint supply | 1 | $25 | $25 |
| 1/4" OD PTFE tubing (1 m) | Amazon | 1 | $8 | $8 |
| 1/4" NPT push-to-connect fittings | Amazon | 4 | $3 | $12 |
| 1N4007 flyback diode | electronics | 2 | $0.10 | $1 |
| 3 mm aluminum plate (bracket stock) | Metal supermarket | 1 | $15 | $15 |
| M5 hardware (bolts, nuts, standoffs) | — | lot | $5 | $5 |
| 80/20 T-slot bracket 10-series | 80/20 Inc. | 2 | $8 | $16 |
| PTFE thread tape | — | 1 | $2 | $2 |
| Splash shield (acrylic 3 mm) | — | 1 | $5 | $5 |
| **Subtotal — paint system** | | | | **$144** |
### 7.2 Positioning (RTK GPS)
| Item | Part / Source | Qty | Unit Cost | Total |
|------|--------------|-----|-----------|-------|
| u-blox ZED-F9P RTK board (rover) | SparkFun GPS-RTK2 | 1 | $250 | $250 |
| u-blox ZED-F9P RTK board (base) | SparkFun GPS-RTK2 | 1 | $250 | $250 |
| Survey GNSS antenna (L1/L2) | SparkFun / u-blox | 2 | $65 | $130 |
| SMA cable (3 m) | Amazon | 2 | $12 | $24 |
| USB-A to USB-C cable (Orin) | — | 1 | $8 | $8 |
| **Subtotal — RTK GPS** | | | | **$662** |
### 7.3 Software / Labor
All software is open-source / in-house. No licensing cost.
### 7.4 Total Estimated BOM
| Category | Cost |
|----------|------|
| Paint dispensing system | $144 |
| RTK GPS system | $662 |
| Contingency (10%) | $81 |
| **Total** | **$887** |
---
## 8. Safety
### 8.1 Paint Hazard
- Aerosol paint is flammable. Keep robot battery terminals isolated from paint canister.
- Ensure ventilation if operating in enclosed areas.
- Overspray: operate only in calm wind conditions (< 10 mph / 16 km/h).
### 8.2 Electrical Safety
- Relay module must be rated for 12 V / 1 A minimum (solenoid inrush).
- Flyback diode mandatory across solenoid coil — unclamped kickback can damage relay contacts and IO GPIO.
- Fuse solenoid 12 V line with a 2 A polyfuse.
### 8.3 Operational Safety
- Never enable autonomous movement unless lot is clear.
- Use RC ESTOP (CH6) as immediate abort — confirm IO board firmware pulls spray pin LOW in FAULT handler.
- Run at max 0.5 m/s with human observer for initial field trials.
- Paint-on while stationary is locked out in software (puddle prevention).
---
## 9. Open Items
| # | Item | Owner | Priority |
|---|------|-------|----------|
| 1 | Confirm Orin GPIO pin accessibility (40-pin header on Jetson Orin Nano vs AGX) | mark / hal | High |
| 2 | Source RTK CORS network credentials for target parking lot area | Serkan | High |
| 3 | Determine parking lot GPS coordinates / anchor point for origin | Serkan | High |
| 4 | Fabricate aluminum canister bracket + nozzle arm | Serkan / mech | Medium |
| 5 | Write IO board firmware extension (TYPE 0x04 SPRAY_CMD) | mark | Medium |
| 6 | Implement `tools/parking_layout.py` layout designer | mark | Medium |
| 7 | Field test RTK fix acquisition time in target lot (tree/building multipath) | mark | Medium |
| 8 | Calibrate nozzle offset constant (15 cm assumed — measure actual) | mark | Low |
| 9 | Evaluate YF-S201 flow sensor for clog detection (v2) | mark | Low |
---
*Spec written by mark — parking lot marking agent.*
*Questions/corrections → MQTT: `saltybot/mark/status` or open GitHub issue in saltylab-firmware.*