Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
49f684f
Update README.md
shl225 Aug 27, 2025
bcfdb16
Merge branch 'IRL-CT:Fall2025' into Fall2025
shl225 Aug 31, 2025
19c58cb
Update README.md
shl225 Aug 31, 2025
9ce0638
Update README.md
shl225 Aug 31, 2025
dc4bc32
Update README.md
shl225 Aug 31, 2025
4a15795
Update README.md
shl225 Sep 1, 2025
25707d3
Update README.md
shl225 Sep 2, 2025
92559ff
Update README.md
shl225 Sep 3, 2025
df87480
Update README.md
shl225 Sep 3, 2025
5624faa
Update README.md
shl225 Sep 8, 2025
4c3055e
Update README.md
shl225 Sep 8, 2025
e935c31
Merge branch 'IRL-CT:Fall2025' into Fall2025
shl225 Sep 17, 2025
72639ff
Update README.md
shl225 Sep 22, 2025
6cd2a4d
Update README.md
shl225 Sep 22, 2025
7b24651
Update README.md
shl225 Sep 22, 2025
bee52db
Merge branch 'Fall2025' into rescued
shl225 Sep 24, 2025
41723fa
Update README.md
shl225 Sep 28, 2025
6d866e8
Create screen_clock_vlm.py
shl225 Sep 28, 2025
489b991
Create fastvlm_server.mjs
shl225 Sep 28, 2025
8745e03
Update README.md
shl225 Sep 29, 2025
3335049
Create greet_shawn.sh
shl225 Sep 29, 2025
5b1ba7b
Update README.md
shl225 Oct 17, 2025
c4c711f
Update README.md
shl225 Oct 17, 2025
88670e9
Update README.md
shl225 Oct 17, 2025
bab9708
Create number_input.sh
shl225 Oct 17, 2025
9ed5e83
Update README.md
shl225 Oct 17, 2025
76090cf
Update README.md
shl225 Oct 17, 2025
e1c6836
Update README.md
shl225 Oct 17, 2025
7daba7c
Update README.md
shl225 Oct 18, 2025
c7601d0
Update README.md
shl225 Oct 18, 2025
ba9aba1
Create greet_name_piper.py
shl225 Oct 18, 2025
fefadfe
Create master.py
shl225 Oct 18, 2025
f65a1f6
Create face_server.py
shl225 Oct 18, 2025
dd3ed52
Create love_server.py
shl225 Oct 18, 2025
ee0244a
Track Lab 4/Lab 5 (override .gitignore)
seanhlewis Nov 3, 2025
87699ed
Update README.md
shl225 Nov 3, 2025
bf7ebdf
Update README.md
shl225 Nov 4, 2025
4e17bef
Add lab 6
seanhlewis Nov 8, 2025
5d5fe3d
Merge branch 'rescued' of https://github.com/shl225/Interactive-Lab-H…
seanhlewis Nov 8, 2025
29cc956
Update README.md
shl225 Nov 8, 2025
2164e71
Update README.md
shl225 Nov 8, 2025
7dfb39f
Update README.md
shl225 Nov 9, 2025
f6d11dc
Update README.md
shl225 Nov 9, 2025
028d92b
Update README.md
shl225 Nov 9, 2025
7903be4
Update README.md
shl225 Nov 9, 2025
cb7a3d8
Update README.md
shl225 Nov 9, 2025
c31dc16
Update README.md
shl225 Nov 9, 2025
50df357
Update README.md
shl225 Nov 9, 2025
a67e38d
Update README.md
shl225 Nov 9, 2025
c3daa89
Update README.md
shl225 Nov 9, 2025
64a4420
Update README.md
shl225 Nov 9, 2025
a74320d
Add files via upload
shl225 Nov 9, 2025
dcc0864
Update README.md
shl225 Nov 17, 2025
b77e4c1
Update README.md
shl225 Nov 17, 2025
3cafa90
Add files via upload
shl225 Nov 17, 2025
fd26e7b
Create fastvlm_server.mjs
shl225 Nov 17, 2025
79d8473
Create rpi5_fastvlm_to_sdxl.py
shl225 Nov 17, 2025
05973a7
Update README.md
shl225 Nov 17, 2025
dd3fcae
Create README.md
shl225 Dec 16, 2025
6ba4104
Update README.md
shl225 Dec 16, 2025
b8b622c
Update README.md
shl225 Dec 16, 2025
10dcd84
Update README.md
shl225 Dec 16, 2025
135104a
Create udp_checker.py
shl225 Dec 16, 2025
b8d6a32
Create mirror.py
shl225 Dec 16, 2025
9e11453
Create compass.py
shl225 Dec 16, 2025
ea31799
Create calibrate.py
shl225 Dec 16, 2025
2c73199
Create calibrate_spin.py
shl225 Dec 16, 2025
42d656f
Create beacon.py
shl225 Dec 16, 2025
83013b1
Update README.md
shl225 Dec 16, 2025
1d7f807
Update README.md
shl225 Dec 16, 2025
aea0621
Update README.md
shl225 Dec 16, 2025
0bc8990
Update README.md
shl225 Dec 16, 2025
587eb66
Update README.md
shl225 Dec 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions Final/README.md

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions Final/beacon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
beacon.py

Device: Receiving RPI

Purpose:
- Periodically broadcast small UDP "beacon" packets.
- The Raspberry Pi (with two ears in monitor mode) will sniff 802.11 frames
from this Raspberry Pi Wi-Fi interface and use RSSI to estimate direction.

No monitor mode needed, just a normal Wi-Fi connection.
"""

import socket
import time
import json
import uuid

BROADCAST_PORT = 50050
BEACON_INTERVAL = 0.1 # seconds
BEACON_MAGIC = "RECEIV_BEACON_V1"


def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

instance_id = str(uuid.uuid4())[:8]
print(f"[RPI Beacon] Starting, instance id = {instance_id}")
print(f"[RPI Beacon] Broadcasting on UDP port {BROADCAST_PORT} every {BEACON_INTERVAL}s")
print("Press Ctrl+C to stop.")

seq = 0
try:
while True:
seq += 1
payload = {
"magic": BEACON_MAGIC,
"instance": instance_id,
"seq": seq,
"time": time.time(),
}
data = json.dumps(payload).encode("utf-8")
sock.sendto(data, ("255.255.255.255", BROADCAST_PORT))
time.sleep(BEACON_INTERVAL)
except KeyboardInterrupt:
print("\n[RPI Beacon] Stopping.")
finally:
sock.close()


if __name__ == "__main__":
main()
281 changes: 281 additions & 0 deletions Final/calibrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
"""
calibrate.py

Active Learning Calibration for Raspberry Pi Dual-Ear Compass.

HARDWARE:
- Pi 5 (Receiver) moving around a static Beacon (Sender).
- ST7789 Display showing the "Needle".

WORKFLOW:
1. BOOTSTRAP: We collect 3 baseline points (Center, Left 90, Right 90).
2. FREE ROAM: The compass starts running live using the initial model.
- You move around the circle.
- When the needle matches reality (points at beacon), you type 'y'.
- If the needle is wrong, you type 'n'.
- The model retrains instantly on 'y'.

Usage:
sudo python3 calibrate.py
"""

import os
import time
import uuid
import joblib
import threading
import math
import sys
import select
from collections import deque
from sklearn.linear_model import LinearRegression

import board
import digitalio
from PIL import Image, ImageDraw, ImageFont
from adafruit_rgb_display import st7789
from scapy.all import sniff, Dot11, RadioTap

# ---------- CONFIG ----------

RECEIV_MAC = "2c:cf:67:73:fe:2c" # BEACON MAC
LEFT_IFACE = "wlan1"
RIGHT_IFACE = "wlan2"
CHECKPOINT_DIR = "checkpoints"

# Physics limits for fallback (before ML takes over)
DELTA_MAX_DB = 20.0
MAX_ANGLE_DEG = 90.0

# ---------- STATE ----------

lock = threading.Lock()
rssi_left_buffer = deque(maxlen=10) # Fast smoothing
rssi_right_buffer = deque(maxlen=10)

X_train = [] # Features: [rssi_l, rssi_r]
y_train = [] # Labels: [angle]

ml_model = None
is_running = True
current_pred_angle = 0.0

# ---------- HARDWARE SETUP ----------

def ensure_root():
if os.geteuid() != 0:
raise SystemExit("!! MUST RUN AS ROOT (sudo) !!")

# Setup Display
cs_pin = digitalio.DigitalInOut(board.D5)
dc_pin = digitalio.DigitalInOut(board.D25)
spi = board.SPI()
disp = st7789.ST7789(spi, cs=cs_pin, dc=dc_pin, width=135, height=240, x_offset=53, y_offset=40)
width = disp.height
height = disp.width
image = Image.new("RGB", (width, height))
draw = ImageDraw.Draw(image)
bl = digitalio.DigitalInOut(board.D22)
bl.switch_to_output()
bl.value = True
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 18)
except:
font = ImageFont.load_default()

# ---------- UTILS ----------

def normalize_mac(mac):
return (mac or "").lower()

def get_current_rssi():
"""Returns average RSSI from buffers."""
with lock:
if len(rssi_left_buffer) < 2 or len(rssi_right_buffer) < 2:
return None, None
avg_l = sum(rssi_left_buffer) / len(rssi_left_buffer)
avg_r = sum(rssi_right_buffer) / len(rssi_right_buffer)
return avg_l, avg_r

def train_model():
"""Retrains the global model on X_train/y_train."""
global ml_model
if len(X_train) < 3:
return # Not enough data

# Linear Regression is robust for simple RSSI difference
clf = LinearRegression()
clf.fit(X_train, y_train)
ml_model = clf

# ---------- SNIFFER ----------

TARGET_MAC = normalize_mac(RECEIV_MAC)

def sniff_thread(iface, side):
def packet_handler(pkt):
if not pkt.haslayer(Dot11): return
if normalize_mac(pkt.addr2) == TARGET_MAC:
try:
rssi = int(pkt[RadioTap].dBm_AntSignal)
with lock:
if side == "left": rssi_left_buffer.append(rssi)
else: rssi_right_buffer.append(rssi)
except: pass

print(f"[Sniffer] Listening on {iface} ({side})...")
sniff(iface=iface, prn=packet_handler, store=False)

# ---------- DISPLAY LOOP (The "Spinning Compass") ----------

def display_thread_func():
"""Runs the display at 30FPS."""
global current_pred_angle

while is_running:
l, r = get_current_rssi()

# 1. Predict Angle
if l is not None and r is not None:
if ml_model:
# Use the Brain
try:
pred = ml_model.predict([[l, r]])[0]
current_pred_angle = max(-110, min(110, pred))
except:
current_pred_angle = 0.0
else:
# Use rough math (Fallback before bootstrap)
delta = r - l
ratio = max(-1, min(1, delta / DELTA_MAX_DB))
current_pred_angle = ratio * MAX_ANGLE_DEG

# 2. Draw
draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0))
cx, cy = width // 2, height // 2
radius = 50

# Compass circle
draw.ellipse((cx-radius, cy-radius, cx+radius, cy+radius), outline=(100,100,100), width=2)

# Needle
angle_rad = math.radians(current_pred_angle)
px = cx + radius * math.sin(angle_rad)
py = cy - radius * math.cos(angle_rad)
draw.line((cx, cy, px, py), fill=(0, 255, 0), width=4) # Green needle for calibration

# Stats
if ml_model:
draw.text((5, 5), "AI MODE", font=font, fill=(0, 255, 0))
else:
draw.text((5, 5), "MATH MODE", font=font, fill=(255, 0, 255))

draw.text((5, 25), f"Ang: {current_pred_angle:.0f}", font=font, fill=(255,255,255))
if l: draw.text((5, height-20), f"L:{l:.0f} R:{r:.0f}", font=font, fill=(100,100,100))

disp.image(image, 90)
time.sleep(0.05)

# ---------- MAIN INTERACTION ----------

def main():
global is_running, ml_model, X_train, y_train
ensure_root()
if not os.path.exists(CHECKPOINT_DIR): os.makedirs(CHECKPOINT_DIR)

# 1. Start Ears
t1 = threading.Thread(target=sniff_thread, args=(LEFT_IFACE, "left"), daemon=True)
t2 = threading.Thread(target=sniff_thread, args=(RIGHT_IFACE, "right"), daemon=True)
t1.start(); t2.start()

# 2. Wait for Signal
print("Waiting for signal...")
while True:
l, r = get_current_rssi()
if l is not None: break
time.sleep(0.5)
print("Signal found. Starting Display.")

# 3. Start Display Thread
t_disp = threading.Thread(target=display_thread_func, daemon=True)
t_disp.start()

# 4. Bootstrap Phase (3 Points)
print("\n=== PHASE 1: BOOTSTRAP ===")
print("I need 3 baseline points before I can start guessing.")

bootstrap_steps = [
(0, "Position Pi so Beacon is DEAD AHEAD (0 deg)"),
(-90, "Position Pi so Beacon is 90 LEFT"),
(90, "Position Pi so Beacon is 90 RIGHT")
]

for angle, prompt in bootstrap_steps:
print(f"\n>> {prompt}")
input(">> Press [ENTER] when ready...")

l, r = get_current_rssi()
if l is None:
print("!! Signal lost. Skipping point.")
continue

print(f" Saved: L={l:.1f}, R={r:.1f} -> Angle={angle}")
X_train.append([l, r])
y_train.append(angle)

print("\nTraining initial model...")
train_model()

# 5. Active Learning Loop
print("\n=== PHASE 2: FREE ROAM (Active Learning) ===")
print("Instructions:")
print(" 1. Walk around the beacon circle.")
print(" 2. Watch the needle on the Pi screen.")
print(" 3. When the needle is pointing CORRECTLY at the beacon -> Press 'y' then Enter")
print(" 4. If the needle is WRONG -> Press 'n' then Enter")
print(" 5. Press 's' to Save Checkpoint.")
print(" 6. Press 'q' to Quit.")

while True:
# We need to capture input without blocking the display (display is in thread, so input() is fine)
cmd = input("\n[y=Good / n=Bad / s=Save / q=Quit] > ").strip().lower()

curr_l, curr_r = get_current_rssi()
curr_angle = current_pred_angle # From the global updated by display thread

if cmd == 'y':
if curr_l is None:
print("No signal, cannot save.")
continue

# Reinforcement Learning:
# We assume if the user said "Yes", the CURRENT PREDICTION is close to truth.
# So we feed the prediction back into the model as ground truth to reinforce it.
print(f" Reinforcing: L={curr_l:.0f} R={curr_r:.0f} => {curr_angle:.0f} deg")
X_train.append([curr_l, curr_r])
y_train.append(curr_angle)
train_model()
print(f" Model Retrained! (Points: {len(X_train)})")

elif cmd == 'n':
print(" Discarded. (Bad Guess)")
# We do nothing, just don't learn from this moment.

elif cmd == 's':
uid = str(uuid.uuid4())[:8]
fname = os.path.join(CHECKPOINT_DIR, f"model_{uid}.pkl")
joblib.dump(ml_model, fname)
print(f"\n[SAVED] Checkpoint: {uid}")
print(f"To run: sudo python3 pi_fused_compass.py --ckpt {uid}")

elif cmd == 'q':
print("Exiting...")
is_running = False
break

if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
is_running = False
print("\nStopped.")
Loading