Measure Distance Data with Arduino AI
Build an Arduino distance logger and audit cleaned sensor data with Claude.
Introduction
30 Second Summary
Measuring one distance is easy. Proving that a whole set of readings is stable takes careful collection plus a clear record of what went wrong.
In this project, you will build a distance-measurement station using an Arduino Uno plus an HC-SR04. Python will capture 30 readings through USB serial communication before Claude turns the saved evidence into a measurement-quality report.
What You'll Build
Picture moving a book in front of your sensor while live centimeter readings change on screen before becoming a saved dataset with a grounded quality report.
By the end of this project, you'll have:
- A live distance station where moving a flat target changes the centimeter readings shown in Arduino IDE Serial Monitor.
- A repeatable measurement run that preserves 30 raw readings in distance_measurements.csv while calculating cleaned mean, median, range, plus population standard deviation.
- An evidence-grounded quality report that checks valid counts, discusses measurement spread, plus labels possible experimental causes as hypotheses.
- Secret Mission: Capture the same target at two marked positions before asking Claude to compare repeatability, spread, plus invalid readings.
Are there any prerequisites?
You'll need an Arduino Uno-compatible board, a four-pin HC-SR04, jumper wires, a breadboard, a data-capable USB cable, plus a Windows computer with internet access.
The software path costs $0 with Claude Free. You do not need a Claude API key.
Before We Start
The Arduino Uno with HC-SR04 produces physical distance readings for Python to log and clean. Claude has one evidence boundary: it may analyze supplied evidence or propose hypotheses without claiming it took the measurements.
Set Up the Measurement Lab
Reliable distance data depends on a verified route from the HC-SR04 to the Arduino Uno. An unknown COM port can make correct code look broken.
Printed labels prevent guessed wiring. You will follow those labels before reconnecting USB power.
The software side needs its own proof. Windows PowerShell will verify Python plus pySerial 3.5.
Visual Studio Code will open the project folder. Arduino IDE 2 will prove the board connection with Blink.
In this step, get ready to:
- Create the project folder and prepare Python serial communication.
- Wire the HC-SR04 by following its printed pin labels.
- Select the Arduino Uno and prove its connection with Blink.
Create the project workspace
The ai-distance-lab folder gives every later script and data file one known location. PowerShell also lets you verify that the same Python launcher can install pySerial.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open Windows PowerShell.
- Move to your Desktop by running this command:
Set-Location -Path "$env:USERPROFILE\Desktop"
What does this command do?
Set-Location changes the folder PowerShell is using. Starting on your Desktop makes the project folder easy to find.
- Create the ai-distance-lab folder and enter it by running these commands:
New-Item -ItemType Directory -Path ai-distance-lab
Set-Location -Path ai-distance-lab
What did these commands do?
- New-Item creates the ai-distance-lab directory on your Desktop.
- Set-Location moves PowerShell into that directory.
Your PowerShell prompt should now end with ai-distance-lab. This confirms that later commands will run inside the project folder.
- Check whether the Python launcher is available by running this command:
py --version
What does this check prove?
The py launcher starts the active Windows Python installation. A displayed version confirms that PowerShell can reach it.
✔️ I see a Python version
Python is available through the Windows launcher. Continue with the pySerial installation below.
ⓧ The command is unavailable
PowerShell cannot currently reach the Python launcher. Install Python for Windows before continuing.
- Follow the official Python Windows guide to install Python.
- Close Windows PowerShell after the installation finishes.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open a fresh Windows PowerShell window.
- Check the Python launcher again by running this command:
py --version
What should this confirm?
PowerShell should now display a Python version. That output confirms the new terminal can reach the launcher.
- Install the pinned serial package by running this command:
py -m pip install pyserial==3.5
What does this command install?
The py launcher runs Python's package installer. The exact package pin keeps this project on pySerial 3.5.
PySerial gives the logger access to the Arduino's Windows serial port. The same Python launcher will run that logger later.
Good progress. Your Python environment now has the serial package needed to communicate with the board.
Did the pySerial installation fail?
- Confirm that the earlier Python check displayed a version.
- Check your internet connection before repeating the installation command.
- Use the same Windows PowerShell window so the package installs for the launcher you verified.
Help me diagnose my pySerial installation problem.
- Open the current project folder in Visual Studio Code by running this command:
code .
What does this command do?
The code command starts Visual Studio Code. The dot tells it to open the current ai-distance-lab folder.
✔️ The project folder opens
You should see ai-distance-lab in the Visual Studio Code Explorer sidebar. The empty workspace is ready for the logger you will create later.
ⓧ The command is unavailable
This means the Visual Studio Code command is unavailable in your current PowerShell session. The Windows User setup adds it to your system path.
- Install the User setup from the official Visual Studio Code Windows guide.
- Close Windows PowerShell after the installation finishes.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open a fresh Windows PowerShell window.
- Return to the project folder and open it in Visual Studio Code by running these commands:
Set-Location -Path "$env:USERPROFILE\Desktop"
Set-Location -Path ai-distance-lab
code .
What should happen now?
The fresh PowerShell window can see the newly installed Visual Studio Code command. Visual Studio Code should open with ai-distance-lab shown in the Explorer sidebar.
Wire the HC-SR04 safely
Working around small pin labels can feel fiddly. Removing USB power keeps the Arduino unpowered while you make each connection.
The printed labels define the purpose of each sensor pin. Checking them directly prevents assumptions about the module's physical pin order.
- Disconnect the data-capable USB cable from the Arduino Uno.
- Read the printed labels beside the four HC-SR04 pins.
- Connect the sensor pin labeled VCC to Arduino 5V.
- Connect the sensor pin labeled TRIG to Arduino D9.
- Connect the sensor pin labeled ECHO to Arduino D10.
- Connect the sensor pin labeled GND to Arduino GND.
- Inspect all four connections against the printed sensor labels.
What does each connection do?
- VCC supplies the sensor from the Arduino's 5V rail.
- GND gives the sensor a shared electrical reference with the Arduino.
- TRIG carries the trigger signal from digital pin D9.
- ECHO returns the timed echo pulse to digital pin D10.
Select the Uno and upload Blink
Blink is a known test sketch for board communication. A successful upload proves Windows can reach the selected Uno through the chosen COM port.
- Reconnect the Arduino Uno with the data-capable USB cable.
- Press the Windows key to open your search bar.
- Type Arduino IDE and press Enter to open it.
- Skip any optional cloud sign-in prompt.
Arduino IDE is ready for a local board connection. The board selection tells the IDE what hardware it will compile for.
- Click Tools in the top menu.
- Select Board.
- Select Arduino AVR Boards.
- Select Arduino Uno if your board is a classic Uno.
- Select the exact detected Uno variant if Arduino IDE identifies another Uno model.
- Click Tools in the top menu.
- Select Port.
- Choose the Windows COM port entry that names your connected Uno.
- Record the displayed COM port here: your Arduino COM port.
Why select both the board and port?
The board selection gives Arduino IDE the correct hardware target. The port selection identifies the connected board that receives the upload.
- Click File in the top menu.
- Select Examples.
- Select 01.Basics.
- Select Blink to load the example sketch.
Before you upload, do you expect the selected board to accept the sketch through the recorded COM port?
- Click the Upload button in the Arduino IDE toolbar.
You should see the upload complete in Arduino IDE. The Uno's built-in LED should repeatedly turn on and off.
That proves the hardware link is working. Your computer can now upload a sketch to the selected Uno.
Before you check the serial ports, do you expect the recorded COM port to appear in pySerial's list?
- Switch back to the Windows PowerShell window from earlier.
- List the available serial ports by running this command:
py -m serial.tools.list_ports
What should this list show?
PySerial scans Windows for available serial ports. You should see an entry containing your Arduino COM port.
Matching the port proves that Python can discover the same board selected in Arduino IDE. The logger can use that port after Serial Monitor releases it.
Is the Uno missing from the port list?
- Reconnect the USB cable at both ends.
- Try another data-capable USB cable if the board receives power but Windows does not list a port.
- Confirm that Arduino IDE still shows the selected Uno under Port.
Help me find why my Arduino Uno is missing from the Windows serial port list.
- Switch back to Visual Studio Code from earlier.
- Confirm that the Explorer sidebar shows ai-distance-lab as the open folder.
Your software workspace and hardware connection now agree on one project folder and one recorded COM port.
Setup checkpoint
- Windows PowerShell is open inside the ai-distance-lab folder.
- Visual Studio Code has the ai-distance-lab folder open.
- Python responds through the py launcher.
- PySerial 3.5 is installed for the active Python launcher.
- The Arduino Uno is connected through a data-capable USB cable.
- The correct Uno model and recorded Windows COM port are selected in Arduino IDE.
- The Blink example has uploaded successfully.
- The HC-SR04 wiring matches its printed labels.
Your measurement lab now has a proven board connection and verified sensor wiring. Next up, you will replace Blink with the sketch that streams real distance measurements.
Stream Distance Measurements
Your Arduino Uno connection is verified through the Blink upload in Arduino IDE 2. The HC-SR04 is wired to the pins used by the distance sketch.
Now you will turn ultrasonic time-of-flight into visible USB serial communication. Before Python or Claude enters the loop, this stream must prove that the sensor produces changing evidence.
In this step, get ready to:
- Build the sensor timing sketch in distance_lab/distance_lab.ino.
- Stream centimeter readings through Serial Monitor.
- Test the stream under changing target conditions.
Build the sensor timing logic
The sensor turns echo duration into a distance estimate. Your sketch packages that sequence in measureDistanceCm().
- Create a new sketch in the Arduino IDE 2 window from earlier.
- Save the sketch as distance_lab inside the existing ai-distance-lab folder.
You will see distance_lab.ino open inside the new distance_lab folder.
- Replace the starter content in distance_lab.ino with this measurement section:
const byte TRIG_PIN = 9;
const byte ECHO_PIN = 10;
const unsigned long ECHO_TIMEOUT_US = 25000UL;
float measureDistanceCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long echoDuration = pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT_US);
if (echoDuration == 0) {
return -1.0;
}
return echoDuration / 58.0;
}
How does the sensor measure distance?
- The TRIG_PIN constant points to digital pin 9.
- The ECHO_PIN constant points to digital pin 10.
- The ECHO_TIMEOUT_US constant limits the echo wait to 25000UL microseconds.
- The trigger sequence sends the required 10 microsecond pulse.
- The pulseIn() call stores the measured echo duration in echoDuration.
- A zero duration returns the -1.0 timeout sentinel.
- A received echo is divided by 58.0 to calculate centimeters.
- Add this runtime section directly below measureDistanceCm():
void setup() {
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
digitalWrite(TRIG_PIN, LOW);
Serial.begin(9600);
}
void loop() {
float distanceCm = measureDistanceCm();
Serial.println(distanceCm, 1);
delay(100);
}
How does the sketch stream readings?
- The setup() function configures TRIG_PIN as an output.
- The same function configures ECHO_PIN as an input.
- The Serial.begin(9600) call starts serial communication at 9600 baud.
- The loop() function stores each result in distanceCm.
- The Serial.println() call sends one newline-delimited reading with one decimal place.
- The delay(100) call spaces the measurements 100 milliseconds apart.
✔️ Awesome, I've got everything!
Great. Your complete distance sketch is ready to upload.
ⓧ I'd like to double check the full code
const byte TRIG_PIN = 9;
const byte ECHO_PIN = 10;
const unsigned long ECHO_TIMEOUT_US = 25000UL;
float measureDistanceCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long echoDuration = pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT_US);
if (echoDuration == 0) {
return -1.0;
}
return echoDuration / 58.0;
}
void setup() {
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
digitalWrite(TRIG_PIN, LOW);
Serial.begin(9600);
}
void loop() {
float distanceCm = measureDistanceCm();
Serial.println(distanceCm, 1);
delay(100);
}
Send readings to Serial Monitor
Uploading replaces the Blink example with your distance sketch. Serial Monitor then displays every newline-delimited value sent by the board.
- Click Upload in Arduino IDE 2.
Arduino IDE confirms the upload completed. Your Uno is now running the distance sketch.
- Open the Tools menu in Arduino IDE 2.
- Select Serial Monitor.
- Set the Serial Monitor baud control to 9600 baud.
You should see one reading about every 100 milliseconds. Each line contains a centimeter value with one decimal place.
Serial Monitor blank?
- Confirm Serial Monitor is set to 9600 baud.
- Confirm the four sensor wires still match the printed labels you verified earlier.
- Confirm both round transducers face a clear target.
Ask for help with the exact symptom you see: Help me troubleshoot a blank Arduino Serial Monitor for my HC-SR04 distance sketch.
Test changing and missing echoes
A flat surface gives the ultrasonic pulse a clear path back to the sensor. Changing the target position lets you confirm that the values represent physical distance.
- Point both round transducers toward a flat book.
- Place the book roughly an arm's length from the sensor.
- Keep the path between the sensor and book clear.
- Hold the sensor still while the readings settle.
Before you test it, consider how target movement should affect the readings. Also consider which value should represent a missing echo.
- Move the book closer to the sensor.
You should see the centimeter values decrease.
- Move the book farther from the sensor.
You should see the centimeter values increase.
- Point the sensor away from a useful target.
You can see -1.0 when no echo returns before the timeout. This value is the missing-echo sentinel.
Why use a timeout sentinel?
The pulseIn() call returns 0 when no pulse arrives before the timeout. The sketch converts that result to -1.0.
The sentinel marks a missing echo in the serial evidence. Python can later keep that raw result separate from real distances.
- Keep Serial Monitor open at 9600 baud.
That is your first live stream of physical evidence working. Next, you will connect Python to this data source and learn what controls access to the COM port.
Discover COM Port Ownership
Your Arduino Uno now streams changing distance measurements into Serial Monitor. The next part of the pipeline needs Python to receive those same measurements.
Only one application can control the COM port at a time. In this step, you will build the logger before testing which application currently owns that connection.
In this step, get ready to:
- Build the complete Python measurement logger.
- Run the logger while Serial Monitor remains open.
- Use the resulting error to identify the current COM-port owner.
Create the logger foundation
The logger needs fixed measurement settings before it can process a serial line. These constants keep the sample count and valid sensor range in one place.
- Use the file creation control in Visual Studio Code's Explorer sidebar to create distance_logger.py inside the ai-distance-lab folder.
- Add the imports and measurement settings by pasting this code into distance_logger.py:
import csv
import statistics
import time
import serial
BAUD_RATE = 9600
SAMPLE_COUNT = 30
MIN_DISTANCE_CM = 2.0
MAX_DISTANCE_CM = 400.0
OUTPUT_FILE = "distance_measurements.csv"
PROMPT_FILE = "claude_prompt.txt"
What does this code set up?
- The imports provide CSV writing, statistical calculations, timing, and serial communication.
- BAUD_RATE matches the Arduino sketch's serial speed.
- SAMPLE_COUNT limits each experiment to 30 recorded readings.
- MIN_DISTANCE_CM and MAX_DISTANCE_CM represent the sensor's documented range of 2 to 400 centimeters.
- OUTPUT_FILE and PROMPT_FILE name the two files produced after a successful capture.
- Save distance_logger.py.
- Confirm the file begins with four imports followed by six measurement settings.
- Add the first part of collect_readings() below the settings by pasting this code:
def collect_readings(port_name):
rows = []
with serial.Serial(port=port_name, baudrate=BAUD_RATE, timeout=2) as board:
print("Waiting for the Arduino serial connection...")
time.sleep(2)
board.reset_input_buffer()
start_time = time.perf_counter()
while len(rows) < SAMPLE_COUNT:
raw_line = board.readline().decode("utf-8", errors="replace").strip()
if not raw_line:
continue
try:
distance_cm = float(raw_line)
except ValueError:
print(f"Skipped non-numeric line: {raw_line!r}")
continue
How does collection begin?
- collect_readings() receives the COM-port name entered by the learner.
- serial.Serial() requests access to that port at 9600 baud with a two-second timeout.
- reset_input_buffer() discards older measurements that arrived before the timed run began.
- readline() waits for one newline-delimited value from the Arduino.
- The conversion step keeps numeric lines while reporting any unexpected text.
- Save distance_logger.py.
- Display the saved logger in PowerShell by running this command:
Get-Content -Path .\distance_logger.py
You will see the imports, constants, and the first part of collect_readings() printed in PowerShell. This confirms that the new file is saved inside ai-distance-lab.
Missing the new code in PowerShell?
Check that Visual Studio Code saved the file as distance_logger.py inside ai-distance-lab. A file in another folder will not appear through the current PowerShell path.
Use Help me find why PowerShell cannot display my logger file.
Complete the evidence pipeline
A numeric reading still needs context before it becomes useful evidence. The rest of the pipeline labels each reading before preserving it in a CSV file.
- Continue collect_readings() directly below its final continue line by pasting this code:
is_valid = MIN_DISTANCE_CM <= distance_cm <= MAX_DISTANCE_CM
row = {
"sample": len(rows) + 1,
"elapsed_seconds": f"{time.perf_counter() - start_time:.2f}",
"distance_cm": f"{distance_cm:.1f}",
"valid": str(is_valid).lower(),
}
rows.append(row)
label = "valid" if is_valid else "invalid"
print(
f"Sample {row['sample']:02d}: "
f"{row['distance_cm']} cm ({label})"
)
return rows
How does a reading become evidence?
- is_valid checks whether a reading falls between 2 and 400 centimeters.
- Each row preserves the sample number, elapsed time, raw numeric reading, and validity label.
- The console label makes invalid readings visible during collection.
- The function returns every row so later stages can preserve both valid and invalid evidence.
- Save distance_logger.py.
- Confirm collect_readings() now ends with return rows.
- Add the CSV writer below collect_readings() by pasting this code:
def write_csv(rows):
fieldnames = ["sample", "elapsed_seconds", "distance_cm", "valid"]
with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
How is the evidence preserved?
- fieldnames fixes the CSV column order.
- DictWriter maps each row dictionary to those four columns.
- writeheader() adds the column names before writerows() saves all captured readings.
- Save distance_logger.py.
- Display the updated file in PowerShell by running this command:
Get-Content -Path .\distance_logger.py
You will see collect_readings() followed by write_csv() near the bottom. The saved file now carries readings from the serial line into structured rows.
Does the CSV writer look nested?
Make sure def write_csv(rows): begins at the far left of the file. Extra indentation would place it inside collect_readings().
Use Help me check the indentation between my Python functions.
- Add the first part of summarize() below write_csv() by pasting this code:
def summarize(rows):
valid_distances = [
float(row["distance_cm"])
for row in rows
if row["valid"] == "true"
]
if not valid_distances:
raise RuntimeError("No valid distance readings were captured.")
summary = {
"total": len(rows),
"valid": len(valid_distances),
"invalid": len(rows) - len(valid_distances),
"mean": statistics.mean(valid_distances),
"median": statistics.median(valid_distances),
"minimum": min(valid_distances),
"maximum": max(valid_distances),
"pstdev": statistics.pstdev(valid_distances),
}
Why filter before calculating statistics?
- valid_distances contains only rows labeled true by the documented range check.
- The RuntimeError stops a summary built from an empty valid dataset.
- The summary counts all evidence while calculating statistics from valid distances only.
- pstdev measures the population standard deviation across this complete 30-sample run.
- Save distance_logger.py.
- Confirm the summary dictionary contains total, valid, invalid, mean, median, minimum, maximum, and population standard deviation values.
- Complete summarize() directly below the summary dictionary by pasting this code:
print("\nCleaned measurement summary")
print(f"Total readings: {summary['total']}")
print(f"Valid readings: {summary['valid']}")
print(f"Invalid readings: {summary['invalid']}")
print(f"Mean: {summary['mean']:.2f} cm")
print(f"Median: {summary['median']:.2f} cm")
print(f"Minimum: {summary['minimum']:.2f} cm")
print(f"Maximum: {summary['maximum']:.2f} cm")
print(f"Population standard deviation: {summary['pstdev']:.2f} cm")
return summary
What will the summary show?
The console receives the cleaned counts and descriptive statistics with consistent decimal formatting. Returning summary also makes those values available to the prompt writer.
- Save distance_logger.py.
- Display the updated file in PowerShell by running this command:
Get-Content -Path .\distance_logger.py
You will see summarize() end with return summary. The logger can now separate preserved raw evidence from cleaned statistical calculations.
Are summary lines missing?
Check that the print lines remain indented inside summarize(). The final return summary line must use the same indentation.
Use Help me inspect my summarize function for missing or misindented lines.
Finish the logger and test port ownership
The final functions turn the cleaned summary into an AI-ready prompt. The control flow also reports serial-port errors without hiding their cause.
- Add the prompt writer below summarize() by pasting this code:
def write_claude_prompt(summary):
prompt = f"""Analyze distance_measurements.csv as evidence from an Arduino HC-SR04 experiment.
Python's cleaned summary is:
- Total readings: {summary['total']}
- Valid readings: {summary['valid']}
- Invalid readings: {summary['invalid']}
- Mean: {summary['mean']:.2f} cm
- Median: {summary['median']:.2f} cm
- Minimum: {summary['minimum']:.2f} cm
- Maximum: {summary['maximum']:.2f} cm
- Population standard deviation: {summary['pstdev']:.2f} cm
Tasks:
1. Recalculate the valid and invalid counts from the CSV and flag any mismatch.
2. Describe the stability and spread of valid readings using the data.
3. List possible causes of invalid or variable readings, clearly labeling them as hypotheses.
4. Propose one controlled follow-up experiment.
5. Do not claim that Claude took the physical measurements. The Arduino sensor produced them.
"""
with open(PROMPT_FILE, "w", encoding="utf-8") as prompt_file:
prompt_file.write(prompt)
How does the prompt protect the evidence?
- The prompt embeds Python's cleaned summary beside the CSV evidence.
- Claude must recalculate the counts before discussing the result.
- Possible causes must be labeled as hypotheses.
- The final instruction keeps physical measurement credit with the Arduino sensor.
- Save distance_logger.py.
- Confirm write_claude_prompt() ends after writing prompt into PROMPT_FILE.
- Add the program control flow below write_claude_prompt() by pasting this code:
def main():
print("Close Arduino Serial Monitor before a real capture.")
port_name = input("Enter the Arduino COM port, for example COM3: ").strip()
try:
rows = collect_readings(port_name)
write_csv(rows)
summary = summarize(rows)
write_claude_prompt(summary)
except serial.SerialException as error:
print(f"Could not open {port_name}: {error}")
print("Close Arduino Serial Monitor, then run this program again.")
raise SystemExit(1)
except RuntimeError as error:
print(error)
raise SystemExit(1)
print(f"\nSaved {OUTPUT_FILE}")
print(f"Saved {PROMPT_FILE}")
if __name__ == "__main__":
main()
How does the logger run?
- main() asks for the recorded COM-port name before starting collection.
- A successful run passes the rows through CSV writing, summarization, and prompt creation.
- SerialException reports a port-access problem before stopping the program.
- RuntimeError reports a run with no valid measurements.
- The final condition starts main() when PowerShell runs this file directly.
- Save distance_logger.py.
- Confirm the final two lines call main() only when Python runs this file directly.
How does the complete pipeline flow?
- collect_readings() turns each serial line into a numeric distance.
- The range check assigns a validity label before preserving the row.
- write_csv() stores the complete evidence trail.
- summarize() calculates statistics from valid rows.
- write_claude_prompt() creates instructions grounded in those statistics.
✔️ Awesome, I've got everything!
Great. Double check that distance_logger.py is saved before testing the COM port.
ⓧ I'd like to double check the full code
Compare your complete distance_logger.py file with this reference.
import csv
import statistics
import time
import serial
BAUD_RATE = 9600
SAMPLE_COUNT = 30
MIN_DISTANCE_CM = 2.0
MAX_DISTANCE_CM = 400.0
OUTPUT_FILE = "distance_measurements.csv"
PROMPT_FILE = "claude_prompt.txt"
def collect_readings(port_name):
rows = []
with serial.Serial(port=port_name, baudrate=BAUD_RATE, timeout=2) as board:
print("Waiting for the Arduino serial connection...")
time.sleep(2)
board.reset_input_buffer()
start_time = time.perf_counter()
while len(rows) < SAMPLE_COUNT:
raw_line = board.readline().decode("utf-8", errors="replace").strip()
if not raw_line:
continue
try:
distance_cm = float(raw_line)
except ValueError:
print(f"Skipped non-numeric line: {raw_line!r}")
continue
is_valid = MIN_DISTANCE_CM <= distance_cm <= MAX_DISTANCE_CM
row = {
"sample": len(rows) + 1,
"elapsed_seconds": f"{time.perf_counter() - start_time:.2f}",
"distance_cm": f"{distance_cm:.1f}",
"valid": str(is_valid).lower(),
}
rows.append(row)
label = "valid" if is_valid else "invalid"
print(
f"Sample {row['sample']:02d}: "
f"{row['distance_cm']} cm ({label})"
)
return rows
def write_csv(rows):
fieldnames = ["sample", "elapsed_seconds", "distance_cm", "valid"]
with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def summarize(rows):
valid_distances = [
float(row["distance_cm"])
for row in rows
if row["valid"] == "true"
]
if not valid_distances:
raise RuntimeError("No valid distance readings were captured.")
summary = {
"total": len(rows),
"valid": len(valid_distances),
"invalid": len(rows) - len(valid_distances),
"mean": statistics.mean(valid_distances),
"median": statistics.median(valid_distances),
"minimum": min(valid_distances),
"maximum": max(valid_distances),
"pstdev": statistics.pstdev(valid_distances),
}
print("\nCleaned measurement summary")
print(f"Total readings: {summary['total']}")
print(f"Valid readings: {summary['valid']}")
print(f"Invalid readings: {summary['invalid']}")
print(f"Mean: {summary['mean']:.2f} cm")
print(f"Median: {summary['median']:.2f} cm")
print(f"Minimum: {summary['minimum']:.2f} cm")
print(f"Maximum: {summary['maximum']:.2f} cm")
print(f"Population standard deviation: {summary['pstdev']:.2f} cm")
return summary
def write_claude_prompt(summary):
prompt = f"""Analyze distance_measurements.csv as evidence from an Arduino HC-SR04 experiment.
Python's cleaned summary is:
- Total readings: {summary['total']}
- Valid readings: {summary['valid']}
- Invalid readings: {summary['invalid']}
- Mean: {summary['mean']:.2f} cm
- Median: {summary['median']:.2f} cm
- Minimum: {summary['minimum']:.2f} cm
- Maximum: {summary['maximum']:.2f} cm
- Population standard deviation: {summary['pstdev']:.2f} cm
Tasks:
1. Recalculate the valid and invalid counts from the CSV and flag any mismatch.
2. Describe the stability and spread of valid readings using the data.
3. List possible causes of invalid or variable readings, clearly labeling them as hypotheses.
4. Propose one controlled follow-up experiment.
5. Do not claim that Claude took the physical measurements. The Arduino sensor produced them.
"""
with open(PROMPT_FILE, "w", encoding="utf-8") as prompt_file:
prompt_file.write(prompt)
def main():
print("Close Arduino Serial Monitor before a real capture.")
port_name = input("Enter the Arduino COM port, for example COM3: ").strip()
try:
rows = collect_readings(port_name)
write_csv(rows)
summary = summarize(rows)
write_claude_prompt(summary)
except serial.SerialException as error:
print(f"Could not open {port_name}: {error}")
print("Close Arduino Serial Monitor, then run this program again.")
raise SystemExit(1)
except RuntimeError as error:
print(error)
raise SystemExit(1)
print(f"\nSaved {OUTPUT_FILE}")
print(f"Saved {PROMPT_FILE}")
if __name__ == "__main__":
main()
Before you run the logger, do you think Python can open the recorded COM port while Serial Monitor is still receiving measurements?
- Keep Arduino IDE Serial Monitor open at 9600 baud.
- Run the logger from the PowerShell window that remains in ai-distance-lab by using this command:
py distance_logger.py
- Enter your recorded Arduino COM port when the logger prompts you.
You will see a line beginning with Could not open. The next line tells you to close Serial Monitor before running the program again.
That failure is the intended result. Serial Monitor currently owns the COM port, so Windows refuses Python's request for the same connection.
Did Python start collecting samples?
Return to Arduino IDE and confirm Serial Monitor is still open at 9600 baud. Reopen the monitor if it was closed before the test.
Use Help me reproduce the COM-port ownership conflict.
You have proved that the sensor connection has one active owner at a time. Next, you will release the port so Python can capture and clean a complete measurement run.
Capture and Clean a Measurement Run
The expected COM port error proved that Arduino IDE Serial Monitor owns the connection. Your complete Python logger is ready to take over.
Closing the competing monitor gives Python access to the sensor readings. The logger preserves every captured value in a CSV file. Its range check excludes timeout sentinels plus impossible distances from the cleaned statistics.
In this step, get ready to:
- Release the recorded COM port to Python.
- Capture exactly 30 readings at one fixed target position.
- Verify the generated evidence package.
Release the COM port
A Windows serial port has one active owner at a time. Closing Serial Monitor removes the conflict you observed in the previous step.
- Close Serial Monitor in Arduino IDE.
- Place a flat target at one fixed position in front of the HC-SR04 sensor.
- Point both round transducers directly toward the target.
The uploaded Arduino sketch continues streaming readings after Serial Monitor closes. Python can now become the connection owner.
- Prepare your recorded COM port for the program's input prompt: your recorded COM port.
Before you run the logger, ask yourself whether Python can open the port now.
- Start the measurement capture from the PowerShell window in ai-distance-lab by running:
py distance_logger.py
What Happens During This Run?
The logger asks for the Arduino COM port. It waits briefly for the serial connection before clearing older buffered readings.
Each new numeric value becomes one row. The run stops after exactly 30 rows.
- Submit your recorded COM port at the prompt using Enter.
- Keep the target still while the samples appear.
- Briefly angle the target during one reading to test the validity label.
- Return the target to its original angle.
You'll see each sample appear with a valid or invalid label. The logger then prints the cleaned measurement summary.
That's the ownership conflict resolved. Python can now capture evidence directly from your Arduino Uno.
How Does the Cleaning Work?
The logger applies MIN_DISTANCE_CM <= distance_cm <= MAX_DISTANCE_CM to every numeric reading. Values from 2.0 through 400.0 centimeters are valid.
Invalid values remain in the CSV as evidence. Only valid distances contribute to the mean, median, minimum, maximum, or population standard deviation.
Capture Still Failing?
- If you still see a message beginning with Could not open, confirm that Serial Monitor is closed.
- If you see No valid distance readings were captured., point the sensor toward a flat target within its documented range.
- If no samples appear, confirm that you entered the recorded COM port exactly.
Help me diagnose my measurement capture.
Inspect the CSV evidence
The CSV keeps the measurement evidence separate from the cleaned summary. Each row records when the sample arrived plus whether it passed the range check.
- Switch back to Visual Studio Code.
- Select distance_measurements.csv in the Explorer sidebar.
- Confirm that the header contains sample, elapsed_seconds, distance_cm, plus valid.
- Confirm that 30 data rows appear beneath the header.
- Compare each valid label with its recorded distance.
Why Preserve Invalid Readings?
An invalid measurement still describes what happened during the experiment. Keeping it reveals timeouts or range problems without allowing those values to distort the statistics.
This creates an audit trail from the raw capture to the cleaned result. Every exclusion remains visible.
Audit the summary and Claude prompt
Python has already performed the deterministic cleaning. The generated prompt gives Claude the calculated summary plus rules for evidence-grounded analysis.
Your logger remained unchanged during this capture. Use the reference below if you want to confirm the complete pipeline.
✔️ Awesome, I've got everything!
Your logger captured the run successfully. Keep Serial Monitor closed before future Python captures.
ⓧ I'd like to double check the full code
Compare distance_logger.py with this complete reference. Every identifier plus value should match.
import csv
import statistics
import time
import serial
BAUD_RATE = 9600
SAMPLE_COUNT = 30
MIN_DISTANCE_CM = 2.0
MAX_DISTANCE_CM = 400.0
OUTPUT_FILE = "distance_measurements.csv"
PROMPT_FILE = "claude_prompt.txt"
def collect_readings(port_name):
rows = []
with serial.Serial(port=port_name, baudrate=BAUD_RATE, timeout=2) as board:
print("Waiting for the Arduino serial connection...")
time.sleep(2)
board.reset_input_buffer()
start_time = time.perf_counter()
while len(rows) < SAMPLE_COUNT:
raw_line = board.readline().decode("utf-8", errors="replace").strip()
if not raw_line:
continue
try:
distance_cm = float(raw_line)
except ValueError:
print(f"Skipped non-numeric line: {raw_line!r}")
continue
is_valid = MIN_DISTANCE_CM <= distance_cm <= MAX_DISTANCE_CM
row = {
"sample": len(rows) + 1,
"elapsed_seconds": f"{time.perf_counter() - start_time:.2f}",
"distance_cm": f"{distance_cm:.1f}",
"valid": str(is_valid).lower(),
}
rows.append(row)
label = "valid" if is_valid else "invalid"
print(
f"Sample {row['sample']:02d}: "
f"{row['distance_cm']} cm ({label})"
)
return rows
def write_csv(rows):
fieldnames = ["sample", "elapsed_seconds", "distance_cm", "valid"]
with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def summarize(rows):
valid_distances = [
float(row["distance_cm"])
for row in rows
if row["valid"] == "true"
]
if not valid_distances:
raise RuntimeError("No valid distance readings were captured.")
summary = {
"total": len(rows),
"valid": len(valid_distances),
"invalid": len(rows) - len(valid_distances),
"mean": statistics.mean(valid_distances),
"median": statistics.median(valid_distances),
"minimum": min(valid_distances),
"maximum": max(valid_distances),
"pstdev": statistics.pstdev(valid_distances),
}
print("\nCleaned measurement summary")
print(f"Total readings: {summary['total']}")
print(f"Valid readings: {summary['valid']}")
print(f"Invalid readings: {summary['invalid']}")
print(f"Mean: {summary['mean']:.2f} cm")
print(f"Median: {summary['median']:.2f} cm")
print(f"Minimum: {summary['minimum']:.2f} cm")
print(f"Maximum: {summary['maximum']:.2f} cm")
print(f"Population standard deviation: {summary['pstdev']:.2f} cm")
return summary
def write_claude_prompt(summary):
prompt = f"""Analyze distance_measurements.csv as evidence from an Arduino HC-SR04 experiment.
Python's cleaned summary is:
- Total readings: {summary['total']}
- Valid readings: {summary['valid']}
- Invalid readings: {summary['invalid']}
- Mean: {summary['mean']:.2f} cm
- Median: {summary['median']:.2f} cm
- Minimum: {summary['minimum']:.2f} cm
- Maximum: {summary['maximum']:.2f} cm
- Population standard deviation: {summary['pstdev']:.2f} cm
Tasks:
1. Recalculate the valid and invalid counts from the CSV and flag any mismatch.
2. Describe the stability and spread of valid readings using the data.
3. List possible causes of invalid or variable readings, clearly labeling them as hypotheses.
4. Propose one controlled follow-up experiment.
5. Do not claim that Claude took the physical measurements. The Arduino sensor produced them.
"""
with open(PROMPT_FILE, "w", encoding="utf-8") as prompt_file:
prompt_file.write(prompt)
def main():
print("Close Arduino Serial Monitor before a real capture.")
port_name = input("Enter the Arduino COM port, for example COM3: ").strip()
try:
rows = collect_readings(port_name)
write_csv(rows)
summary = summarize(rows)
write_claude_prompt(summary)
except serial.SerialException as error:
print(f"Could not open {port_name}: {error}")
print("Close Arduino Serial Monitor, then run this program again.")
raise SystemExit(1)
except RuntimeError as error:
print(error)
raise SystemExit(1)
print(f"\nSaved {OUTPUT_FILE}")
print(f"Saved {PROMPT_FILE}")
if __name__ == "__main__":
main()
What Does the Full Logger Cover?
The logger collects serial values before saving every accepted numeric line as a row. It calculates statistics from rows marked true.
The final function writes the same calculated values into claude_prompt.txt. That shared summary makes later AI claims easier to audit.
Before you compare the outputs, ask yourself whether the valid count plus the invalid count should cover all 30 rows.
- Return to the existing PowerShell window.
- Confirm that the summary reports 30 total readings.
- Compare the valid count with the valid values in distance_measurements.csv.
- Compare the invalid count with the invalid values in distance_measurements.csv.
- Switch back to Visual Studio Code.
- Select claude_prompt.txt in the Explorer sidebar.
- Compare its calculated values with the PowerShell summary.
- Confirm that its tasks separate observations from hypotheses.
You'll find distance_measurements.csv with exactly 30 evidence rows. You'll also find claude_prompt.txt with the matching cleaned summary.
Your measurement pipeline now preserves raw evidence plus cleaned statistics. Next up, you'll give both files to Claude for an evidence audit.
Ask Claude to Audit the Evidence
Your Python logger has turned the Arduino readings into a 30-row CSV with cleaned statistics. Now you will ask Claude to audit that evidence.
Python has already handled the deterministic cleaning. Claude can now focus on explaining patterns without silently changing the measurements.
A convincing report can still contain a mismatched count. It can also present an unsupported cause as fact.
In this step, get ready to:
- Enable file analysis in Claude if it is unavailable.
- Upload the measurement evidence and request an analysis.
- Check every numerical claim and separate observations from hypotheses.
Enable Claude file analysis
Claude can analyze CSV files in the web app when its file capability is available.
Your existing account is enough. This step never asks for an API key.
- Go to the Claude web app in your web browser.
- Complete the sign-in flow if Claude asks for your account details.
- Start a new conversation from Claude's main screen.
- Open Settings from the account menu if file attachment is unavailable.
- Select Capabilities if you opened the settings.
- Turn on Code execution and file creation.
- Return to the new conversation after changing the capability.
You should see a + control beside the message box. This confirms that the conversation can accept your evidence files.
Can't see the attachment control?
- Return to Settings if the + control remains unavailable.
- Confirm that Code execution and file creation is turned on under Capabilities.
- Refresh the conversation after enabling the capability.
Help me find Claude's file attachment control.
Upload the evidence and request analysis
The CSV contains the 30 preserved readings. The generated prompt tells Claude which statistics to check and how to separate observations from hypotheses.
- Click the + control beside the message box.
- Select Add files or photos.
- Browse to the ai-distance-lab folder in the file picker.
- Select distance_measurements.csv.
- Hold Ctrl while selecting claude_prompt.txt.
- Press Enter to attach both selected files.
Good, both filenames should now appear beside the message box. The evidence bundle is ready for one grounded analysis.
- Enter the analysis request by pasting this text into the message box:
Follow the instructions in claude_prompt.txt and analyze distance_measurements.csv.
What does this request do?
- The request directs Claude to use claude_prompt.txt as its analysis checklist.
- The request keeps distance_measurements.csv as the source of measurement evidence.
- The generated instructions require Claude to recalculate the counts before discussing possible causes.
Before you send the request, do you expect Claude to report only measured patterns or also propose possible explanations?
- Click Send.
You should receive a measurement-quality report that states the valid and invalid counts. It should discuss spread using the supplied statistics while labeling possible causes as hypotheses.
Claude can't analyze both files?
- Use the + control to attach the missing file if only one filename appears.
- Send the request again after both filenames appear beside the message box.
- Enable Code execution and file creation if Claude cannot read the uploaded CSV.
Help me troubleshoot Claude's file analysis.
Audit Claude's report
Your CSV and Python summary remain the authority for every numerical claim. Possible causes require a separate experiment before they become measured conclusions.
Before the final audit, do you think the CSV can prove that angle, surface, motion, or reflections caused a reading to vary?
- Switch back to Windows PowerShell from earlier.
- Compare Claude's total, valid count, invalid count, mean, median, minimum, maximum, and population standard deviation with the Python summary.
- Switch back to Visual Studio Code from earlier.
- Select distance_measurements.csv in the Explorer sidebar.
- Check that the valid labels support the counts in Claude's report.
- Return to the Claude conversation.
- Accept numerical statements only when they match the Python summary and CSV evidence.
- Treat comments about angle, surface, motion, or reflections as hypotheses unless a separate experiment tests them.
Claude's valid and invalid counts should match the Python summary. Its discussion of spread should use the supplied statistics.
Comments about angle, surface, motion, or reflections should be labeled as hypotheses. Strong finish. Your report now stays grounded in the sensor data.
Do the claims disagree with your evidence?
- Identify the mismatched metric in a new Claude message.
- Ask Claude to recalculate that metric from the uploaded CSV.
- Ask Claude to relabel an unsupported cause as a hypothesis.
- Repeat the comparison after Claude updates the report.
Help me audit a mismatch in Claude's report.
Secret mission
Compare Two Target Positions
Turn your distance station into a controlled experiment. Capture the same flat target at marked near and far positions. Then ask Claude to compare repeatability using valid counts, spread, and invalid readings without presenting possible causes as facts.
Clean Up Your Resources
Clean Up Your Resources
This lab creates no ongoing charges because its software path costs $0. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The wired Arduino Uno and HC-SR04 measurement setup.
- The ai-distance-lab folder containing the Python logger, Arduino sketch, generated prompt, and measurement files.
- The Claude conversation containing the original evidence audit and two-position comparison.
Keep everything running
No action needed. Choose this if you plan to run more distance experiments with the same setup.
- Leave the wired Arduino Uno connected over USB.
- Keep Arduino IDE Serial Monitor closed before each Python capture.
- Retain the ai-distance-lab folder with its code and measurement evidence.
- Retain the Claude conversation for future comparisons.
Pause - I'll come back to this later
Shut down the physical measurement stream while preserving your code and evidence. Your experiment remains ready for another session.
- Disconnect the Arduino Uno's USB cable from your Windows device.
- Leave the HC-SR04 wiring in place only if you can store the unpowered setup safely.
- Keep the ai-distance-lab folder in its current location.
- Keep the Claude conversation available for reference.
- Reconnect the USB cable when you are ready to resume measuring.
Delete - I don't want to use this again
Remove all project resources to start fresh. The wiring can feel delicate, so USB power comes off first.
- Disconnect the Arduino Uno's USB cable from your Windows device.
- Remove the four jumper-wire connections from the Arduino Uno and HC-SR04.
- Use Visual Studio Code's file sidebar to reveal the Windows location of the ai-distance-lab folder.
- Close Visual Studio Code.
- Close Windows PowerShell so the project directory is no longer in use.
- Use the folder's Windows context menu to delete ai-distance-lab.
- Confirm that ai-distance-lab no longer appears in its previous Windows location.
Your local code and measurement evidence are removed when the folder disappears from its previous location.
- Return to the Claude conversation from earlier.
- Use that conversation's menu to delete it.
- Confirm that the conversation no longer appears in your conversation list.
Nice Work!
Nice Work!
You made it! Your Arduino Uno and HC-SR04 now power a complete distance-measurement pipeline.
Here's what you learned:
- Programmed the distance sketch to convert HC-SR04 echo timing into centimeter readings over USB serial communication.
- Built a Python logger that captures 30 readings in distance_measurements.csv. The logger preserves every raw value with a validity label before calculating cleaned descriptive statistics.
- Used Claude to generate an evidence-grounded quality report from distance_measurements.csv. You checked each numeric claim against the CSV. You kept possible physical causes labeled as hypotheses.
- Completed the optional Secret Mission by comparing distance_near.csv with distance_far.csv. You judged repeatability from each run's spread. You also compared invalid-reading counts.
Ready to quiz yourself?