Skip to content

Commit 2fafe14

Browse files
authored
Merge pull request #6 from ATNoG/fix-car-names
Random ids
2 parents 7f6725f + 1e11fa8 commit 2fafe14

File tree

12 files changed

+342
-71
lines changed

12 files changed

+342
-71
lines changed

src/services/accident_detector/service.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,14 @@ def _detect_sudden_stop(self, update: CarUpdate, state: CarState) -> bool:
227227
def _on_car_update(self, payload: str):
228228
try:
229229
data = json.loads(payload)
230+
231+
# Handle test cleanup
232+
if data.get("_test_cleanup"):
233+
car_id = data.get("car_id")
234+
if car_id:
235+
self._cleanup_car(car_id)
236+
return
237+
230238
update = CarUpdate.from_dict(data)
231239
except Exception as e:
232240
logger.error(f"Error processing car update: {e}")
@@ -303,6 +311,22 @@ def _on_car_update(self, payload: str):
303311

304312
self._cleanup_expired_accidents()
305313

314+
def _cleanup_car(self, car_id: str):
315+
"""Remove all state for a specific car (used for test cleanup)."""
316+
# Remove car state
317+
if car_id in self.cars:
318+
del self.cars[car_id]
319+
logger.info(f"[CLEANUP] Removed car state: {car_id}")
320+
321+
# Remove any accidents caused by this car
322+
accidents_to_remove = [
323+
event_id for event_id, accident in self.active_accidents.items()
324+
if accident.source_vehicle_id == car_id
325+
]
326+
for event_id in accidents_to_remove:
327+
del self.active_accidents[event_id]
328+
logger.info(f"[CLEANUP] Removed accident {event_id} caused by {car_id}")
329+
306330
def run(self):
307331
logger.info("Starting Accident Detector...")
308332
self.mqtt.connect()

src/services/emergency_vehicle_detector/service.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,16 @@ def __init__(self, config):
4141

4242
def _on_car_update(self, payload: str):
4343
try:
44-
update = CarUpdate.from_dict(json.loads(payload))
44+
data = json.loads(payload)
45+
46+
# Handle test cleanup
47+
if data.get("_test_cleanup"):
48+
car_id = data.get("car_id")
49+
if car_id:
50+
self._cleanup_car(car_id)
51+
return
52+
53+
update = CarUpdate.from_dict(data)
4554
except Exception as e:
4655
logger.error(f"Failed to parse car update: {e}")
4756
return
@@ -86,6 +95,12 @@ def _on_car_update(self, payload: str):
8695
f"from {other_id}"
8796
)
8897

98+
def _cleanup_car(self, car_id: str):
99+
"""Remove all state for a specific car (used for test cleanup)."""
100+
if car_id in self.cars:
101+
del self.cars[car_id]
102+
logger.info(f"[CLEANUP] Removed car state: {car_id}")
103+
89104
def run(self):
90105
logger.info("Starting Emergency Vehicle Detector...")
91106
self.mqtt.connect()

src/services/highway_entry_detector/service.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,16 @@ def _predict_collision(self, entering_car: CarUpdate, highway_car: CarUpdate) ->
276276

277277
def _on_car_update(self, payload: str):
278278
try:
279-
update = CarUpdate.from_dict(json.loads(payload))
279+
data = json.loads(payload)
280+
281+
# Handle test cleanup
282+
if data.get("_test_cleanup"):
283+
car_id = data.get("car_id")
284+
if car_id:
285+
self._cleanup_car(car_id)
286+
return
287+
288+
update = CarUpdate.from_dict(data)
280289
except Exception as e:
281290
logger.error(f"Failed to parse car update: {e}")
282291
return
@@ -318,6 +327,9 @@ def _on_car_update(self, payload: str):
318327
if dist_to_merge < self.MERGE_POINT_DETECTION_M:
319328
logger.info(f"[ENTRY DETECTION] Car {update.car_id} is approaching merge point (distance: {dist_to_merge:.1f}m)")
320329

330+
# Track if we found any highway cars in detection zone
331+
found_highway_car_in_zone = False
332+
321333
# Check for potential collisions with highway cars
322334
for highway_car_id in self.highway_cars:
323335
if highway_car_id not in self.cars:
@@ -339,6 +351,7 @@ def _on_car_update(self, payload: str):
339351
)
340352

341353
if dist_highway_to_merge < self.ENTRY_ZONE_M:
354+
found_highway_car_in_zone = True
342355
logger.info(f"[ENTRY DETECTION] Analyzing collision: entering {update.car_id} vs highway {highway_car_id}, dist={dist_highway_to_merge:.1f}m")
343356
# Predict collision
344357
collision, ttc, min_dist = self._predict_collision(update, highway_car)
@@ -391,6 +404,51 @@ def _on_car_update(self, payload: str):
391404
f"can safely merge. Min distance to {highway_car_id}: {min_dist:.1f}m"
392405
)
393406
self.alerted_pairs.add(pair_key)
407+
408+
# If no highway cars found in detection zone, it's safe to enter
409+
if not found_highway_car_in_zone:
410+
# Only alert once per entering car
411+
if update.car_id not in [pair[0] for pair in self.alerted_pairs]:
412+
alert = {
413+
"alert_type": "highway_entry_safe",
414+
"entering_car_id": update.car_id,
415+
"highway_car_id": None,
416+
"entering_speed_kmh": update.speed_kmh,
417+
"highway_speed_kmh": None,
418+
"predicted_min_distance_m": None,
419+
"status": "safe",
420+
"timestamp": time.time(),
421+
"latitude": update.latitude,
422+
"longitude": update.longitude,
423+
}
424+
425+
self.mqtt.publish(self.alert_topic, json.dumps(alert))
426+
logger.info(
427+
f"[HIGHWAY ENTRY - SAFE] Car {update.car_id} "
428+
f"can safely merge - no highway traffic detected in entry zone"
429+
)
430+
# Mark this entering car as alerted
431+
self.alerted_pairs.add((update.car_id, "no-traffic"))
432+
433+
def _cleanup_car(self, car_id: str):
434+
"""Remove all state for a specific car (used for test cleanup)."""
435+
# Remove from cars dictionary
436+
if car_id in self.cars:
437+
del self.cars[car_id]
438+
logger.info(f"[CLEANUP] Removed car state: {car_id}")
439+
440+
# Remove from classification sets
441+
self.highway_cars.discard(car_id)
442+
self.entering_cars.discard(car_id)
443+
444+
# Remove from alerted pairs
445+
pairs_to_remove = {
446+
pair for pair in self.alerted_pairs
447+
if car_id in pair
448+
}
449+
self.alerted_pairs -= pairs_to_remove
450+
if pairs_to_remove:
451+
logger.info(f"[CLEANUP] Removed {len(pairs_to_remove)} alert pairs for {car_id}")
394452

395453
def run(self):
396454
logger.info("Starting Highway Entry Detector...")

src/services/overtaking_detector/service.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,16 @@ def _projection_sign(ax, ay, bx, by, heading_deg) -> int:
6363

6464
def _on_car_update(self, payload: str):
6565
try:
66-
update = CarUpdate.from_dict(json.loads(payload))
66+
data = json.loads(payload)
67+
68+
# Handle test cleanup
69+
if data.get("_test_cleanup"):
70+
car_id = data.get("car_id")
71+
if car_id:
72+
self._cleanup_car(car_id)
73+
return
74+
75+
update = CarUpdate.from_dict(data)
6776
except Exception as e:
6877
logger.error(f"Failed to parse car update: {e}")
6978
return
@@ -124,6 +133,23 @@ def _on_car_update(self, payload: str):
124133
# save new relative sign
125134
self.relative_positions[key] = sign
126135

136+
def _cleanup_car(self, car_id: str):
137+
"""Remove all state for a specific car (used for test cleanup)."""
138+
# Remove car state
139+
if car_id in self.cars:
140+
del self.cars[car_id]
141+
logger.info(f"[CLEANUP] Removed car state: {car_id}")
142+
143+
# Remove any relative position pairs involving this car
144+
pairs_to_remove = [
145+
key for key in self.relative_positions.keys()
146+
if car_id in key
147+
]
148+
for key in pairs_to_remove:
149+
del self.relative_positions[key]
150+
if pairs_to_remove:
151+
logger.info(f"[CLEANUP] Removed {len(pairs_to_remove)} position pairs for {car_id}")
152+
127153
def run(self):
128154
logger.info("Starting Overtaking Detector...")
129155
self.mqtt.connect()

src/services/position_processor/service.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ def _resolve_speed_limit(self, lat: float, lon: float) -> float:
7979

8080

8181
def _handle_raw_gps(self, car_id: str, lat: float, lon: float, emergency: bool = False):
82+
# Handle test cleanup marker (coordinates at origin with tiny tolerance)
83+
if abs(lat) < 0.0001 and abs(lon) < 0.0001:
84+
# This is a cleanup signal, remove the car state
85+
with self.states_lock:
86+
if car_id in self.states:
87+
del self.states[car_id]
88+
logger.info(f"[CLEANUP] Removed car state: {car_id}")
89+
return
90+
8291
now = time.time()
8392

8493
# Thread-safe state access

tests/conftest.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Pytest configuration for test suite."""
2+
import json
3+
import os
4+
import time
5+
import uuid
6+
from pathlib import Path
7+
from typing import List
8+
9+
import paho.mqtt.client as mqtt
10+
import pytest
11+
12+
# Test configuration
13+
MQTT_HOST = os.getenv("MQTT_HOST", "localhost")
14+
MQTT_PORT = int(os.getenv("MQTT_PORT", "1884"))
15+
SIM_DIR = Path(__file__).resolve().parent.parent / "simulations"
16+
17+
18+
def pytest_addoption(parser):
19+
"""Add custom command-line options to pytest."""
20+
parser.addoption(
21+
"--fixed-ids",
22+
action="store_true",
23+
default=False,
24+
help="Use fixed car IDs instead of random UUIDs (useful for frontend testing)",
25+
)
26+
27+
28+
@pytest.fixture(scope="session")
29+
def use_fixed_ids(request):
30+
"""Fixture that returns whether to use fixed car IDs."""
31+
return request.config.getoption("--fixed-ids")
32+
33+
34+
@pytest.fixture
35+
def test_car_registry():
36+
"""Track all cars created during a test for cleanup."""
37+
car_ids = []
38+
yield car_ids
39+
# Cleanup after test - happens even if test fails
40+
_cleanup_test_cars(car_ids)
41+
42+
43+
@pytest.fixture
44+
def get_car_id(use_fixed_ids, test_car_registry):
45+
"""Fixture that returns a function to generate car IDs and track them for cleanup."""
46+
def _get_car_id(base_name: str) -> str:
47+
"""Generate car ID: random UUID by default, or fixed name with --fixed-ids flag."""
48+
if use_fixed_ids:
49+
car_id = base_name
50+
else:
51+
car_id = f"{base_name}-{str(uuid.uuid4())[:8]}"
52+
53+
# Register for cleanup
54+
test_car_registry.append(car_id)
55+
return car_id
56+
57+
return _get_car_id
58+
59+
60+
def _cleanup_test_cars(car_ids: List[str]) -> None:
61+
"""
62+
Comprehensive cleanup of test cars:
63+
1. Send cleanup signals to services via MQTT
64+
2. Remove local device files
65+
3. Wait for services to process cleanup
66+
"""
67+
if not car_ids:
68+
return
69+
70+
print(f"\n[CLEANUP] Removing {len(car_ids)} test cars from services...")
71+
72+
# Send cleanup messages to services
73+
try:
74+
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
75+
client.connect(MQTT_HOST, MQTT_PORT, keepalive=5)
76+
client.loop_start()
77+
time.sleep(0.2) # Ensure connection established
78+
79+
for car_id in car_ids:
80+
# Send cleanup signal to all services
81+
cleanup_msg = json.dumps({
82+
"action": "cleanup",
83+
"car_id": car_id,
84+
"timestamp": time.time()
85+
})
86+
client.publish(f"test/cleanup/{car_id}", cleanup_msg, qos=1)
87+
88+
# Also send a final position update with special marker to trigger state cleanup
89+
# This ensures the car is removed from all service states
90+
cleanup_update = json.dumps({
91+
"car_id": car_id,
92+
"latitude": 0.0,
93+
"longitude": 0.0,
94+
"speed_kmh": None,
95+
"heading_deg": None,
96+
"speed_limit_kmh": 50.0,
97+
"emergency": False,
98+
"timestamp": time.time(),
99+
"_test_cleanup": True
100+
})
101+
client.publish("cars/updates", cleanup_update, qos=1)
102+
103+
# Give services time to process cleanup messages
104+
time.sleep(0.3)
105+
client.loop_stop()
106+
client.disconnect()
107+
except Exception as e:
108+
print(f"[CLEANUP] Warning: MQTT cleanup failed: {e}")
109+
110+
# Remove local device files
111+
for car_id in car_ids:
112+
car_file = SIM_DIR / "devices" / f"{car_id}.json"
113+
if car_file.exists():
114+
try:
115+
car_file.unlink()
116+
except Exception as e:
117+
print(f"[CLEANUP] Warning: Failed to delete {car_file}: {e}")
118+
119+
print(f"[CLEANUP] Cleanup complete for {len(car_ids)} cars")

0 commit comments

Comments
 (0)