A poor-man's radar. A servo sweeps an ultrasonic sensor across 0–180°, an Arduino Nano measures distance at each angle and streams it to a Raspberry Pi over Bluetooth, and the Pi renders the returns as a live radar sweep on a small OLED.
░░░░░ 90° ░░░░░
░░ │ ░░ • = detected object
░ ╲│╱ ░ ─ = lit sweep (blue)
0°━━━━━━━━━ ● ━━━━━━━━━180° ● = sensor origin
┌───────────────────────────────────────────┐
PHYSICAL │ ARDUINO NANO firmware: nano/ │
┌──────────┐ trig │ ┌────────────┐ ┌──────────────────┐ │
│ HC-SR04 │◀─────────┤ │ scanner.ino│ │ bluetooth.cpp/.h │ │
│ ultrasonic├─────────▶│ │ read + ├───▶│ btSendDistance() │ │
└──────────┘ echo │ │ median x5 │ └────────┬─────────┘ │
▲ │ └────────────┘ │ │
│ rides on └─────────────────────────────┼──────────────┘
│ servo horn │ JSON {"d": <cm>}
┌────┴─────┐ │ over Bluetooth (RFCOMM)
│ SERVO │ ▼
│ GPIO18 │◀─move(angle)─┐ ┌───────────────────────────────┐
└──────────┘ │ │ RASPBERRY PI (Python) │
│ │ │
┌───────┴──────┐ │ ┌──────────────────────────┐ │
│ ServoManger │◀─┼──┤ BluetoothManager │ │
│ (servo.py) │ │ │ (bluetooth.py) │ │
└───────┬──────┘ │ │ reader thread → │ │
│ │ │ latest_distance │ │
move │ │ └────────────┬─────────────┘ │
▼ │ │ latest_distance│
┌──────────────────────────────┐ │ │
│ ScanManger │◀─┘ │
│ (scan.py) │ │
│ sweep thread • distances[181]│ │
└───────────────┬───────────────┘ │
│ get_distances() │
▼ │
┌──────────────────────────────┐ │
│ DisplayManager / OLEDDisplay│ ┌────────────┐ │
│ (display.py) ├──▶│ SH1106 OLED│ │
│ radar render │ │ I²C 0x3C │ │
└──────────────────────────────┘ └────────────┘ │
all wired by main.py │
└────────────────────────────────────────────────── │
| Component | File | Responsibility |
|---|---|---|
| Firmware | nano/ |
Pulse HC-SR04, median-filter 5 samples, send {"d":<cm>} over HC-05. |
| BluetoothManager | bluetooth.py |
RFCOMM client; background thread keeps the newest latest_distance. |
| ServoManger | servo.py |
Angular servo on GPIO18; move(angle) for 0–180°. |
| ScanManger | scan.py |
Background sweep; records distance per angle into distances[0..180]. |
| OLEDDisplay | display.py |
Renders the radar (rings + sweep) to the SH1106 panel. |
| DisplayManager | display.py |
Thin adapter the display loop calls with the distance snapshot. |
| main.py | main.py |
Wires the parts together and runs the OLED refresh loop. |
HC-SR04 Nano HC-05 ── BT ── Pi ScanManger OLED
│ │ │ │ │ │
│ echo │ │ │ │ │
├────────────▶│ median(5) │ │ │ │
│ ├───────────────▶│ {"d":N} │ │ │
│ │ ├──────────▶│ latest_distance│ │
│ │ │ │ │ │
│ │ move(angle) ◀───┤ sweep thread │ │
│ │ │ ├──────────────▶│ distances[a]=N│
│ │ │ │ │ │
│ │ │ │ get_distances() │
│ │ │ │ ├─────────────▶│ draw
- Measure — the Nano pulses the HC-SR04, takes 5 samples, median-filters
them, and emits a JSON line
{"d": <cm>}(or-1when out of range). - Receive —
BluetoothManagerreads that line over RFCOMM in a background thread and exposes the newest value aslatest_distance. - Correlate —
ScanManger's sweep thread moves the servo one angle at a time and stores the currentlatest_distanceintodistances[angle](index = degree,-1= out of range / not yet scanned). - Render — the display loop in
main.pypollsget_distances()atREFRESH_RATEand hands the snapshot toOLEDDisplay, which draws the radar.
The SH1106 is a 128×64 monochrome panel — "blue" simply means a lit pixel.
OLEDDisplay draws a half-circle radar with the sensor at the bottom-centre:
┌──────────────────────────────────────┐
│ · · · · · · │ · · · range rings (RINGS arcs,
│ · · │ evenly spaced to MAX_DIST)
│ · \ | / / · │
│ · \ | / / · │ \ | / lit radial lines = sweep,
│ · \|// · │ drawn from origin out to
│ ──────────────●────────────────── │ each detected object
│ 180° origin 0° │
└──────────────────────────────────────┘
- Range rings —
RINGSreference arcs at evenly spaced distances, the outermost atMAX_DISTcm. - Object returns — each in-range angle draws a lit radial line from the origin out to the object's distance; out-of-range angles stay black.
- Orientation — 0° at the right edge, 90° straight up, 180° at the left (mirrors the servo's physical sweep).
See SETUP.md for wiring, dependencies, and OS configuration. Then:
python3 main.pyThe servo calibrates (0 → 90 → 180°), the sweep starts, and the radar appears on
the OLED. Press Ctrl-C to stop.
| Knob | Where | Effect |
|---|---|---|
sweepRate |
scan.py |
Angle steps per second (servo settle). |
MAX_DIST |
display.py |
Distance (cm) mapped to the outer ring. |
RINGS |
display.py |
Number of reference range rings. |
REFRESH_RATE |
main.py |
OLED redraw rate (polls per second). |