Portfolio in progress · Portfolio content is being assembled

← All projects

PROJECT 002 / EMBEDDED SYSTEMS LAB

OrbitScan.See the echoes.

I’m taking a distance sensor beyond a single number. This project turns a servo sweep into a map—and makes signal quality part of the story.

ELEGOO MEGA 2560 · ULTRASONIC SENSING · DESIGN & BUILD GUIDE

MY QUESTION

Where is the object?How much do I trust
the reading?

I break the problem into five layers: motion, echo timing, filtering, feedback, and a map I can inspect.

30°–150° ARC / 31 ANGLES / 3 ECHOES PER ANGLE

TRY THE IDEA / LOCAL WORKBENCH

A map that shows
what it knows.

I use synthetic echoes here to explain the signal pipeline. Change the scene, add noise, or step through one angle.

ORBITSCAN / POLAR VIEWSYNTHETIC SCENE
Use the readouts below for the closest distance, valid angles, and current angle.
NEAREST / LATEST MAP—
VALID ANGLES—
CURRENT ANGLE30°
Outside the near zone

Synthetic scene loaded.

0 rows · dots = accepted ranges · × = unknown · centimeters

The viewer highlights the nearest range in the latest map; the hardware indicator responds to the current angle and adds a 5 cm release margin. The simulation is accelerated. It generates idealized rays with synthetic noise; real ultrasonic beams are broad and reflections depend on target material and angle. Imported CSV stays in this browser. Nothing is uploaded, and no board connects automatically. Each dot is the latest sample for that angle, so a sweep is not a simultaneous snapshot.

MY APPROACH

More than a moving sensor.

I use the Mega 2560 to coordinate the sensor, servo, buttons, display, and serial output. The useful part is the pipeline: each layer makes a clear decision about timing, validity, and what the user should see.

01 / CAPTURE

Keep the loop responsive.

I capture Echo edges on D18 with an interrupt. The loop schedules motion and a 26 ms timeout. A 10 μs trigger pulse is the only deliberate microsecond wait; there is no blocking pulseIn().

02 / FILTER

Accept a useful reading.

I collect three echoes per angle, reject values outside 2–200 cm, and require at least two valid samples. Three use the median; two use their rounded mean. I expose sample count and spread instead of hiding uncertainty.

03 / INTERPRET

Unknown means unknown.

An invalid reading becomes −1 in the CSV and an unknown marker in the map. The near indicator has a 5 cm release margin. Filtering rejects isolated spikes; it cannot correct a consistent reflection error.

The trade-off I’m exploring: three measurements and settling time give a steadier map, but a one-way pass takes roughly eight seconds. Moving objects can shift during that pass. This is a 2D ultrasonic range sketch, with commanded servo angles—not a 3D reconstruction or precision instrument.

The conversion assumes approximately 343 m/s sound speed. Temperature, target geometry, servo backlash, and mounting alignment affect the result. I keep the software operating window at 2–200 cm and compare readings with a ruler rather than quoting the module’s best-case accuracy.

PIN-BY-PIN / MEGA 2560

One clear connection map.

I use the kit’s Mega, HC-SR04, micro servo, parallel LCD1602, 10 kΩ potentiometer, RGB LED, two buttons, resistors, breadboard, and jumpers. I also need a secure lightweight sensor bracket and a separate regulated 5 V servo supply rated for the actual servo’s startup/stall current; 1 A is a reasonable starting supply rating for one unloaded SG90-class servo. These extra mounting and power items may be outside the kit.

My power arrangement: USB powers the Mega, LCD, and ultrasonic sensor. The separate 5 V supply powers only the servo’s positive rail. I join supply ground to Mega GND and servo GND, but keep external +5 V separate from Mega +5 V. I disconnect both sources before moving wires, check polarity and voltage, and keep the sweep clear of fingers and wires. A rectangular 9 V battery and an unknown breadboard regulator are not substitutes for an adequate servo supply.
Part / signalConnectionDetail
HC-SR04 VCC / GNDMega 5V / GNDUse the kit’s 5 V module; check printed pin labels.
HC-SR04 Trig / EchoD22 / D18D18 supports an external interrupt on Mega 2560. Do not use Serial1 at the same time.
Servo signalD9Verify signal, positive and ground from the actual servo documentation.
Servo power / groundExternal regulated 5V / common GNDExternal +5V goes only to the servo. Grounds join; positive supplies do not.
Pause buttonD24 → switch → GNDINPUT_PULLUP; tap to pause/resume.
Near-zone buttonD25 → switch → GNDCycles 40 → 60 → 20 cm.
RGB R / G / BD6 / D7 / D8, each through 220 ΩCommon cathode → GND. Confirm leg order. Set COMMON_ANODE=true and common → Mega 5V only for that confirmed type.

Parallel LCD1602

LCD pinConnectionPurpose
1 VSS / 2 VDDGND / Mega 5VLogic supply
3 VO10 kΩ potentiometer wiperPot outer legs → Mega 5V and GND
4 RS / 5 RW / 6 ED12 / GND / D11Matches LiquidCrystal(12,11,5,4,3,2)
7–10 D0–D3Unconnected4-bit interface
11 D4 / 12 D5 / 13 D6 / 14 D7D5 / D4 / D3 / D2LCD pin names differ from Mega pin numbers
15 LED+ / 16 LED−Mega 5V through 220 Ω / GNDBacklight polarity and current limiting

I use the pin tables as the connection source of truth. The LCD wiring differs from LumaDesk, so I rebuild the connections rather than swapping sketches on the old circuit. I check breadboard rail continuity and button internal pairs before powering up.

MY BUILD SEQUENCE

Prove each layer.
Then connect the system.

01 / Bring up the board and display

I select Arduino Mega or Mega 2560 / ATmega2560 in Arduino IDE, confirm the port with Blink, and wire the LCD with power disconnected. I install the official Servo and LiquidCrystal libraries through Library Manager. I extract the ZIP and keep OrbitScan.ino and ScanCore.h together in the OrbitScan folder.

02 / Check motion without a load

I keep the servo horn and sensor detached, connect the separate 5 V supply and common ground, and upload OrbitScan. The display greets “Hello, Ori!” before scanning. I send c over serial to center the servo at 90° and pause. I disconnect both supplies and attach a lightweight sensor mount facing straight ahead without rotating the shaft. I verify that the 30°–150° arc clears the base and wiring. The code cannot sense a mechanical stall.

03 / Read a stationary target

I wire the HC-SR04, place a broad flat target upright, and open Serial Monitor at 115200 baud. I wait for the head to point at the target and compare the corresponding angle’s distance with a ruler. A soft or tilted surface can return no echo; I check with a flat target before changing the conversion.

04 / Check the interface

I tap Pause to hold the last angle and resume with a fresh three-sample group. The second button cycles the near-zone threshold. Red means a current valid range is inside the zone, green means valid and outside, magenta means unknown, and blue means paused. I can also send p to pause or n to change the threshold over serial. The c command centers the head at 90° and pauses; resume returns it to the scan position after a 500 ms settle.

05 / Inspect a complete sweep

I copy serial rows into a plain-text CSV with the exact header below, then use Import scan CSV in this page. The map shows the latest value per angle. I save a separate log for each target arrangement and record the target positions, surface, power setup, and sensor alignment.

COMPLETE SOURCE / SMALL TESTABLE CORE

The firmware behind the map.

I separate pure logic in ScanCore.h from hardware I/O in the sketch. The core covers pulse conversion, validity checks, three-sample aggregation, bounded sweep reversal, button debounce, and near-zone hysteresis. Atomic sections protect shared interrupt values on the 8-bit Mega.

Telemetry contract

ms,sweep,angle_deg,distance_cm,valid_samples,spread_cm

ms is device uptime, sweep counts direction reversals, and angle_deg is the commanded servo angle. distance_cm=-1 means unknown. Spread is the largest minus smallest accepted sample; it is zero when fewer than two are valid. The firmware spaces triggers by at least 65 ms and waits 65 ms after each 4° move, with an initial 500 ms settle. Logs go over USB; the board does not store a session after power-off.

Read the complete Arduino sketch
// OrbitScan — Mega 2560 ultrasonic mapping lab. Project guide: /projects/orbitscan
// Install official Servo and LiquidCrystal libraries. Keep ScanCore.h in this folder.
#include <Servo.h>
#include <LiquidCrystal.h>
#include <util/atomic.h>
#include "ScanCore.h"

const uint8_t TRIG=22,ECHO=18,SERVO=9,PAUSE_BUTTON=24,THRESHOLD_BUTTON=25;
const uint8_t RED=6,GREEN=7,BLUE=8;
const bool COMMON_ANODE=false;
LiquidCrystal lcd(12,11,5,4,3,2);
Servo head;
Scan::Sweep sweep;
Scan::Button pauseButton,thresholdButton;
Scan::Proximity proximity;
volatile bool armed=false,riseSeen=false,echoReady=false;
volatile uint32_t riseUs=0,pulseUs=0;
bool running=true,waiting=false;
uint32_t sentUs=0,lastShotMs=0,movedMs=0,settleMs=500;
uint16_t raw[3]={0,0,0},threshold=40;
uint8_t sample=0;
enum Stage {SETTLE,SAMPLE};Stage stage=SETTLE;
uint16_t mapCm[Scan::BINS]={0}; // 0 means unknown; never free space

void echoChanged(){
  if(!armed)return;
  uint32_t now=micros();
  if(digitalRead(ECHO)){riseUs=now;riseSeen=true;}
  else if(riseSeen){pulseUs=uint32_t(now-riseUs);echoReady=true;armed=false;}
}
void disarm(){ATOMIC_BLOCK(ATOMIC_RESTORESTATE){armed=false;echoReady=false;riseSeen=false;}waiting=false;}
void rgb(bool r,bool g,bool b){
  digitalWrite(RED,r^COMMON_ANODE);digitalWrite(GREEN,g^COMMON_ANODE);digitalWrite(BLUE,b^COMMON_ANODE);
}
void displayResult(Scan::Filtered f){
  lcd.setCursor(0,0);lcd.print(F("                "));lcd.setCursor(0,0);
  lcd.print(sweep.angle());lcd.print(F("deg "));
  if(f.cm){lcd.print(f.cm);lcd.print(F("cm"));}else lcd.print(F("NO ECHO"));
  lcd.setCursor(0,1);lcd.print(F("                "));lcd.setCursor(0,1);
  lcd.print(F("T"));lcd.print(threshold);lcd.print(F(" V"));lcd.print(f.valid);lcd.print(F("/3 "));
  lcd.print(!f.cm?F("UNKNOWN"):proximity.near?F("NEAR"):F("SCAN"));
  if(!f.cm)rgb(true,false,true);else if(proximity.near)rgb(true,false,false);else rgb(false,true,false);
}
void finishBin(uint32_t now){
  Scan::Filtered f=Scan::filter(raw);mapCm[sweep.bin]=f.cm;proximity.update(f.cm,threshold);
  displayResult(f);
  Serial.print(now);Serial.print(',');Serial.print(sweep.passes);Serial.print(',');
  Serial.print(sweep.angle());Serial.print(',');
  if(f.cm)Serial.print(f.cm);else Serial.print(-1);
  Serial.print(',');Serial.print(f.valid);Serial.print(',');Serial.println(f.spread);
  sweep.advance();head.write(sweep.angle());movedMs=now;settleMs=65;stage=SETTLE;sample=0;
}
void takeSample(uint32_t duration,uint32_t now){
  waiting=false;raw[sample++]=Scan::cmFromPulse(duration);
  if(sample==3)finishBin(now);
}
void togglePause(uint32_t now){
  running=!running;disarm();sample=0;stage=SETTLE;movedMs=now;
  if(running){head.write(sweep.angle());settleMs=500;}
  if(!running){lcd.setCursor(0,1);lcd.print(F("PAUSED          "));rgb(false,false,true);}
}
void setup(){
  pinMode(TRIG,OUTPUT);digitalWrite(TRIG,LOW);pinMode(ECHO,INPUT);
  pinMode(PAUSE_BUTTON,INPUT_PULLUP);pinMode(THRESHOLD_BUTTON,INPUT_PULLUP);
  pinMode(RED,OUTPUT);pinMode(GREEN,OUTPUT);pinMode(BLUE,OUTPUT);rgb(false,false,true);
  Serial.begin(115200);Serial.println(F("ms,sweep,angle_deg,distance_cm,valid_samples,spread_cm"));
  lcd.begin(16,2);lcd.print(F("Hello, Ori!"));lcd.setCursor(0,1);lcd.print(F("OrbitScan / init"));
  attachInterrupt(digitalPinToInterrupt(ECHO),echoChanged,CHANGE);
  head.attach(SERVO);head.write(sweep.angle());movedMs=millis();lastShotMs=movedMs;
}
void loop(){
  uint32_t now=millis();
  if(pauseButton.update(digitalRead(PAUSE_BUTTON)==LOW,now))togglePause(now);
  if(thresholdButton.update(digitalRead(THRESHOLD_BUTTON)==LOW,now))threshold=threshold==60?20:threshold+20;
  while(Serial.available()){
    char c=Serial.read();if(c=='p')togglePause(now);
    if(c=='n')threshold=threshold==60?20:threshold+20;
    if(c=='c'){
      running=false;disarm();sample=0;head.write(90);rgb(false,false,true);
      lcd.setCursor(0,0);lcd.print(F("CENTER 90deg    "));lcd.setCursor(0,1);lcd.print(F("PAUSED          "));
    }
  }
  if(!running)return; // servo holds its last angle; never force its shaft
  if(stage==SETTLE){if(!Scan::elapsed(now,movedMs,settleMs))return;stage=SAMPLE;}
  if(waiting){
    bool ready;uint32_t width;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE){ready=echoReady;width=pulseUs;if(ready)echoReady=false;}
    if(ready){takeSample(width,now);return;}
    if(Scan::elapsed(micros(),sentUs,Scan::TIMEOUT_US)){disarm();takeSample(0,now);}
    return;
  }
  if(!Scan::elapsed(now,lastShotMs,Scan::SHOT_GAP_MS))return;
  // If Echo is stuck HIGH, count an invalid sample rather than manufacturing distance.
  lastShotMs=now;
  if(digitalRead(ECHO)==HIGH){takeSample(0,now);return;}
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE){echoReady=false;riseSeen=false;armed=true;}
  sentUs=micros();waiting=true;
  digitalWrite(TRIG,HIGH);delayMicroseconds(10);digitalWrite(TRIG,LOW);
}

The downloadable package includes this guide, the firmware, a native C++ core test, a hardware checklist, and exact software validation results. The guide and code use AI assistance; I keep the technical explanations and measurements open to review.

MY VALIDATION PLAN

Measure what the map earns.

I use the checklist to record observations rather than turning a plausible screen into a performance claim. I separate software checks from sensor measurements.

SOFTWARE

Boundaries first.

The core tests exercise invalid pulses, a single outlier, missing samples, scan reversal, timer rollover, debounce, and hysteresis. Mega compilation checks the sketch and library interfaces. Exact tool versions and results are in VALIDATION.txt.

MEASUREMENT

Range & repeatability.

I compare broad targets at 20, 50, 100, and 150 cm, repeat each arrangement, and record error and spread. I test a tilted surface, missing Echo, pause/resume, and servo power stability.

REFLECTION

Explain the compromise.

I compare raw and filtered readings, count unknown angles, and time a full pass. My project log has space for photos, real CSV captures, a fault and fix, and the next design change.

When a reading looks wrong

SymptomMy first checks
All angles unknownPower off and verify VCC/GND, Trig D22, Echo D18, shared ground, and the exact module labels. Retry with a broad flat target at 50 cm.
Servo jitters / Mega resetsCheck external regulated supply rating, common ground, cable strain, and mechanical clearance. Remove power before adjusting the mount. Never force the shaft.
LCD backlight but no textAdjust VO contrast. Check RW → GND, LCD data ordering, and pin 1 orientation.
One angle jumps between surfacesKeep the scene still; compare raw returns, spread and target geometry. Median filtering does not narrow the acoustic beam.
CSV import failsCopy only the six-column header and numeric records. Keep 30–150° in 4° steps; use −1 for unknown. Reset returns to the synthetic scene.

I can extend this design with temperature compensation, a scan-to-scan change view, or a rigid enclosure. Each extension needs its own measurement question and test.

PRIMARY REFERENCES

Where I check the details.

PROJECT NOTEBOOK / OCTOBER 2026

PIXEL COMPANION. REAL CURIOSITY.

Meet OriBit.

Ori’s portfolio companion

Play with OriBit ✧

Small steps. Real evidence. Stay curious.

Rule-based companion, not generative AI. Chat stays in this tab. Email is sent only when you submit the contact form.

PROFILE SUMMARY

Ori Alsheh

Irvine Valley College · Electrical engineering direction

Seeking
Practical, supervised engineering experience in summer 2027.
Location
Orange County & Southern California.
Format
Internship, engineering project, laboratory experience, or mentorship.
Duration
Ideally 3–6 weeks. Exact dates, weekly availability, and eligibility are to be confirmed with Ori.
Email
Ori.alshech2008@gmail.com
Current documentation
Résumé, coursework, demonstrated technical skills, project evidence, and hackathon participation are being assembled.

A summary of the confirmed profile, not a complete résumé.