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

20 KiB
Raw Blame History

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
  2. Paint Dispensing Hardware
  3. Control Electronics
  4. Positioning Strategy
  5. Software Architecture
  6. Parking Lot Templates
  7. Bill of Materials
  8. Safety
  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:

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)

{
  "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

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

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:

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