# 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" | 8–10 cm | Covers standard stripe width at 50 PSI | | 2" detail / curb line | Flat fan 20° | 0.013" | 6–8 cm | Narrower pattern | | 6" fire lane | Flat fan 65° | 0.021" | 10–12 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 30–60 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 | 100–200 mA | 5 V rail | | (v2) Mini compressor | 12 V | 3–5 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: 4–6" 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.*