Project Info
A wearable haptic vision system for blind and low-vision users
Inspiration
Blind and low-vision pedestrians navigate with tools that tell them almost nothing about the space around them. A white cane finds obstacles at ground level. A guide dog handles traffic. Neither gives turn-by-turn directions, and neither warns about a person stepping into your path from the side. We wanted to fuse navigation and obstacle awareness into a single, always-on sense, delivered through touch so the wearer's ears stay free for the street. What It Does Wander is a haptic belt worn around the torso. A chest-mounted iPhone reads Google Maps walking directions, monitors its own LiDAR depth sensor, and watches the camera for people and obstacles. Every heartbeat it compresses all of that into one cue and fires it to four servos arranged as a cross around the body: front, back, left, and right. The belt points the way to go, not at the hazard. A tap on the left motor means turn left, a tap on the right means turn right, and the front motor means you are on course. When something is in your path, the belt steers you clear instead of buzzing the thing itself. An obstacle on your left taps Right to send you toward the open side, and a person stepping in dead ahead taps Back, telling you to stop and step back. Closeness rides on the strength of the tap. The wearer sets a destination by speaking it, and Wander resolves the place, builds the route, and starts walking them there. The safety logic runs at 10 Hz with a strict priority stack: a person in your path beats a LiDAR obstacle, which beats an early-warning looming cue, which beats a navigation turn cue. The belt never confuses "turn here" with "stop, something is in front of you." How We Built It The phone is the entire sensing stack. We dropped the planned Coral accelerator once on-device LiDAR and CoreML carried the safety story, so there is no external compute to wear. iOS app (Swift 6, strict concurrency): AppModel runs a 10 Hz decide loop that arbitrates cues from four sources and fires a single LC2 packet over UDP to the belt. LiDAR obstacle detection: ARKit depth frames, sampled in three lateral bands to decide whether to steer left, steer right, or stop and reorient. Person and object detection: YOLOv8n CoreML model, on-device at the LiDAR frame rate. Depth-crop fusion confirms distance. 21 COCO navigation classes (person, bicycle, car, bus, stop sign, and more). Early warning: A BearingTracker watches for centered, looming objects before LiDAR has a return and fires a soft front tap as a heads-up. Navigation: Google Maps SDK + Directions API. The wearer speaks a destination, PlaceResolver finds it with MKLocalSearch, and the app drives the route off live GPS. Voice layer: Deepgram Voice Agent for speech in and out, with client-side function calling for commands like set_destination, where_am_i, describe_surroundings, and read_sign. Claude reasoning (off the safety path): the describe path runs an evaluator-optimizer, a Haiku draft checked by a Sonnet verify pass against the live scene, with an on-device guard that rejects any "path is clear" line the LiDAR contradicts. read_sign hands one camera frame to Opus vision to read store signs, bus numbers, and door labels, hedged when the read is high-stakes. Claude never touches the obstacle reflex. Belt: ESP32 in Wi-Fi AP mode driving four servos. A FastAPI laptop bridge over USB serial is the fallback if the ESP32 does not come up. Challenges We Ran Into Thermal headroom. Running LiDAR depth, YOLO inference, Google Maps, and a Deepgram WebSocket at once on one phone generates heat. We instrumented a thermal monitor and gated YOLO behind a thermal threshold to keep the phone from throttling mid-demo. ARKit and AVFoundation cannot share a session. We had to collapse the camera preview, LiDAR depth, and YOLO inference onto a single ARSession rather than running a second AVCaptureSession. Swift 6 strict concurrency. Every service and actor boundary had to satisfy the compiler's data-race checker, with a clean build and no warnings. Deepgram Voice Agent API quirks. The plan assumed a client_side flag on function definitions; sending one causes an UNPARSABLE_CLIENT_MESSAGE error, and the real way to mark a function client-side is to omit its endpoint. Push-to-talk with stopAudio tripped CLIENT_MESSAGE_TIMEOUT. We moved to continuous mic streaming with Deepgram's own end-of-speech detection and a tap-to-toggle UI. Chest-mount angle. LiDAR pointed at chest height sees the ground at range. Without ground-plane rejection, the obstacle cue fires constantly on flat pavement. Accomplishments We're Proud Of A complete pipeline from spoken destination to walking directions to haptic belt cues, all running on one iPhone with no cloud compute on the safety path. A belt that guides to safety rather than pointing at danger: every hazard cue taps the direction the wearer should move, and a four-tier arbiter guarantees a navigation hint can never mask a real hazard. The voice layer working end to end on device. Speak a place name, get a route, and the belt starts guiding. An evaluator-optimizer for spoken safety narration: a Haiku draft verified by Sonnet against the live scene, with an on-device guard that blocks a false "all clear." 120+ unit test assertions covering the packet codec, bearing math, routing geometry, obstacle avoidance, person detection, depth fusion, and voice command parsing. A hardware fallback (laptop FastAPI bridge to an Arduino over USB serial) so the demo is not gated on the ESP32 coming up. What We Learned Safety systems need a single, explicit arbitration point. Letting each subsystem fire the belt on its own would have produced chaos. A ranked priority stack with one sender per tick made the behavior predictable and testable. Voice APIs require hands-on device testing. Every assumption we made about the Deepgram API, the function flags, the push-to-talk model, the audio routing, turned out wrong in some detail. The as-built behavior came from running it on the phone, not from reading docs. On-device inference is fast enough to matter. YOLOv8n at the LiDAR frame rate adds real signal without a co-processor. What's Next for Wander Belt bring-up: prove the LC2 round-trip on the real ESP32 and servos, and tune the haptic patterns for clarity at walking speed. Thermal hardening: ground-plane rejection at the chest-mount angle, threshold tuning, and false-positive discipline with settle and hysteresis logic. On-device voice and vision verification: confirm the describe and read-sign paths answer within a single voice turn on the phone against live keys, and tune the tone of what Wander says. YOLO-World upgrade: swap the fixed COCO vocabulary for an open-vocabulary model so we can name any object class without retraining. Fetch.ai integration: an optional transactional tier for booking accessible transit or flagging routing hazards to a shared map. Built With Swift, SwiftUI, Swift 6, ARKit, CoreML, YOLOv8n, Google Maps SDK, Google Directions API, Deepgram Voice Agent, Anthropic Claude API, MKLocalSearch, ESP32, Arduino, FastAPI, Python, WebSockets, XcodeGen Team
Citrus Squad × Berkeley AI Hackathon 2026
Citrus Squad's entry for the Berkeley AI Hackathon 2026 at the MLK Jr. Building, UC Berkeley. Hack window opens Saturday June 20 at 11:00 AM and runs 24 hours, closing Sunday June 21 at 11:00 AM. Judging and closing ceremony follow.
Citrus Squad is a haptic navigation belt for blind and low-vision wearers. A chest-mounted iPhone reads compass direction and Google Maps turn cues, detects nearby obstacles via LiDAR, identifies objects via on-device computer vision, and taps four servos on a belt to tell the wearer which way to turn or move. No screen. No audio required. The phone is the brain; an ESP32 drives the belt.
Run it yourself
Every teammate runs their own instance on their own phone and Mac. See RUNNING.md for the ten-minute setup. The short version:
./ios/setup.sh # installs XcodeGen, creates your local signing, generates the project
open ios/CitrusSquad.xcodeproj
# set your team + bundle id in ios/Local.xcconfig, pick your iPhone, press Cmd-R
You can run the full app with just a phone (the Navigation card's demo route + simulate mode needs no belt and no API key). The ESP32 belt and live Google Maps are optional add-ons covered in RUNNING.md.
Computer vision layer (Cole — cole/computer-vision)
The CV layer adds object awareness on top of the LiDAR obstacle detection already in the base app. It identifies what is in the path (pole, person, car, bench) and feeds that into the same hazard arbitration system that already drives the belt.
What is built
cv/pipeline.py— transport-agnostic YOLOv8n inference fused with LiDAR depth. Takes a paired (RGB frame, depth map) and returns a list ofDepthFusedDetectionobjects: label, confidence, bounding box, depth at the box, and horizontal position normalized 0–1.cv/detection.py—DepthFusedDetectiondataclass and theNAVIGATION_CLASSESfilter (21 pedestrian-relevant COCO classes: person, bicycle, car, bench, parking meter, etc.). The on-device iOS filter (CitrusSquadConfig.visionNavigationClasses) mirrors this set so both recognize the same things.cv/ingest.py— FastAPI WebSocket server. Accepts binary frame pairs from the iPhone over local Wi-Fi, runs the pipeline, and broadcasts JSON detections to any connected haptic client.cv/webcam_test.py— local smoke test. Runs the pipeline against a laptop webcam with a synthetic 2.0m depth plane so the full detection path can be verified without a phone.server.py— entry point (uvicorn server:app --host 0.0.0.0 --port 8000).tests/— 17 unit tests covering depth fusion math and the wire protocol parser.
Planned: on-device CoreML path (no Wi-Fi required)
The Wi-Fi server works for prototyping but has a single point of failure at demo time. The target is to run everything on the phone:
- Export
yolov8n.mlpackagefrom the Python model (YOLO("yolov8n.pt").export(format="coreml")). ObjectDetectionService.swift— subscribes to the ARKit session already running inDepthService. EachARFramecarries both the camera image and the LiDAR depth map. RunsVNCoreMLRequeston the camera image, scales bounding boxes to depth coordinates, samples the inner 50% of each box (same as the Python fusion logic), and callsVisionHazardSource.report()with the result.- No Wi-Fi dependency. No laptop. All inference runs on the Neural Engine.
Planned: collision prediction and action layer
Pure-logic layer on top of raw detections. For each detection, it asks: is this object in my path, how close, and what is the best move?
Input: DepthFusedDetection list + LiDAR band readings
Output: NavigationAction (StepLeft(paces: 2), StepRight(paces: 1), Stop, SlowDown, Clear)
Decision factors: horizontal position (is it centered?), distance (how urgent?), object type (static pole vs. moving person), and which side has more open space. Belt fires the directional tap; Josh's audio layer can say "pole ahead, step left 2 paces."
Running the Python CV server
pip3 install -r requirements.txt
python3 server.py
# or: uvicorn server:app --host 0.0.0.0 --port 8000
Smoke-test the detection pipeline locally (no phone needed):
python3 -m cv.webcam_test # built-in camera
python3 -m cv.webcam_test 1 # external camera
Run the unit tests:
python3 -m pytest tests/ -v
System architecture
iPhone (Citrus Squad app) ESP32 (belt)
Maps directions + compass -> turn cue receives one LC2 packet
LiDAR scene depth -> obstacle cue per 100 ms heartbeat,
YOLOv8n CoreML (planned) -> object ID + action renders the event as a
| servo pattern
v arbitrate (safety > direction)
one LC2 packet / 100 ms --UDP over Wi-Fi--> 4 servos: Far L, L, R, Far R
Team
Sam (iOS, LiDAR, ESP32, CoreML iOS integration), Cole (computer vision, Python pipeline, collision prediction), Josh (audio), Angelo.
License
MIT.
Analysis
View
Metric
- 78
- 70
- 3
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- CIn code
- FastAPIIn code
- HTMLIn code
- PythonIn code
- SwiftIn code
- AnthropicClaimed
5 of 6 appear in the indexed code. 1 claimed on Devpost could not be matched to code, which may simply mean the tool leaves no trace in the repository.
AI coding agents
- Claude CodeConfig · Commits
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
976 KB
Source files
141
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
Sam-T-G/citrus-squad-x-berkeley-hackathon
156 files · 9.7 MB · @ 07e5a14
Structure
Interface
18 files · 12%Screens, components and styles rendered to the user.
API & routing
9 files · 6%Request entry points: routes, handlers and controllers.
Application logic
63 files · 40%Domain rules, services and shared utilities.
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- Markdown45%
- Swift44%
- Python6%
- HTML4%
- YAML0%
- Shell0%
- Other (1)0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
requirements.txt
pypi · 8- fastapi
- httpx
- numpy
- opencv-python
- pytest
- pytest-asyncio
- ultralytics
- uvicorn[standard]
server/requirements.txt
pypi · 4- aioserial
- fastapi
- uvicorn[standard]
- websockets
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
This project’s features have not been analysed yet.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.