saltylab-firmware/docs/PARKING-MARKING-SPEC.md
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

535 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.*