feat: Add commodity sensing, proof bundle, Three.js viz, mock isolation

Commodity Sensing Module (ADR-013):
- sensing/rssi_collector.py: Real Linux WiFi RSSI collection from
  /proc/net/wireless and iw commands, with SimulatedCollector for testing
- sensing/feature_extractor.py: FFT-based spectral analysis, CUSUM
  change-point detection, breathing/motion band power extraction
- sensing/classifier.py: Rule-based presence/motion classification
  with confidence scoring and multi-receiver agreement
- sensing/backend.py: Common SensingBackend protocol with honest
  capability reporting (PRESENCE + MOTION only for commodity)

Proof of Reality Bundle (ADR-011):
- data/proof/generate_reference_signal.py: Deterministic synthetic CSI
  with known breathing (0.3 Hz) and walking (1.2 Hz) signals
- data/proof/sample_csi_data.json: Generated reference signal
- data/proof/verify.py: One-command pipeline verification with SHA-256
- data/proof/expected_features.sha256: Expected output hash

Three.js Visualization:
- ui/components/scene.js: 3D scene setup with OrbitControls

Mock Isolation:
- testing/mock_pose_generator.py: Mock pose generation moved out of
  production pose_service.py
- services/pose_service.py: Cleaned mock paths

https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
This commit is contained in:
Claude
2026-02-28 06:18:58 +00:00
parent e3f0c7a3fa
commit 2199174cac
12 changed files with 358561 additions and 184 deletions

View File

@@ -0,0 +1,301 @@
"""
Mock pose data generator for testing and development.
This module provides synthetic pose estimation data for use in development
and testing environments ONLY. The generated data mimics realistic human
pose detection outputs including keypoints, bounding boxes, and activities.
WARNING: This module uses random number generation intentionally for test data.
Do NOT use this module in production data paths.
"""
import random
import logging
from typing import Dict, List, Any, Optional
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
# Banner displayed when mock pose mode is active
MOCK_POSE_BANNER = """
================================================================================
WARNING: MOCK POSE MODE ACTIVE - Using synthetic pose data
All pose detections are randomly generated and do NOT represent real humans.
For real pose estimation, provide trained model weights and real CSI data.
See docs/hardware-setup.md for configuration instructions.
================================================================================
"""
_banner_shown = False
def _show_banner() -> None:
"""Display the mock pose mode warning banner (once per session)."""
global _banner_shown
if not _banner_shown:
logger.warning(MOCK_POSE_BANNER)
_banner_shown = True
def generate_mock_keypoints() -> List[Dict[str, Any]]:
"""Generate mock keypoints for a single person.
Returns:
List of 17 COCO-format keypoint dictionaries with name, x, y, confidence.
"""
keypoint_names = [
"nose", "left_eye", "right_eye", "left_ear", "right_ear",
"left_shoulder", "right_shoulder", "left_elbow", "right_elbow",
"left_wrist", "right_wrist", "left_hip", "right_hip",
"left_knee", "right_knee", "left_ankle", "right_ankle",
]
keypoints = []
for name in keypoint_names:
keypoints.append({
"name": name,
"x": random.uniform(0.1, 0.9),
"y": random.uniform(0.1, 0.9),
"confidence": random.uniform(0.5, 0.95),
})
return keypoints
def generate_mock_bounding_box() -> Dict[str, float]:
"""Generate a mock bounding box for a single person.
Returns:
Dictionary with x, y, width, height as normalized coordinates.
"""
x = random.uniform(0.1, 0.6)
y = random.uniform(0.1, 0.6)
width = random.uniform(0.2, 0.4)
height = random.uniform(0.3, 0.5)
return {"x": x, "y": y, "width": width, "height": height}
def generate_mock_poses(max_persons: int = 3) -> List[Dict[str, Any]]:
"""Generate mock pose detections for testing.
Args:
max_persons: Maximum number of persons to generate (1 to max_persons).
Returns:
List of pose detection dictionaries.
"""
_show_banner()
num_persons = random.randint(1, min(3, max_persons))
poses = []
for i in range(num_persons):
confidence = random.uniform(0.3, 0.95)
pose = {
"person_id": i,
"confidence": confidence,
"keypoints": generate_mock_keypoints(),
"bounding_box": generate_mock_bounding_box(),
"activity": random.choice(["standing", "sitting", "walking", "lying"]),
"timestamp": datetime.now().isoformat(),
}
poses.append(pose)
return poses
def generate_mock_zone_occupancy(zone_id: str) -> Dict[str, Any]:
"""Generate mock zone occupancy data.
Args:
zone_id: Zone identifier.
Returns:
Dictionary with occupancy count and person details.
"""
_show_banner()
count = random.randint(0, 5)
persons = []
for i in range(count):
persons.append({
"person_id": f"person_{i}",
"confidence": random.uniform(0.7, 0.95),
"activity": random.choice(["standing", "sitting", "walking"]),
})
return {
"count": count,
"max_occupancy": 10,
"persons": persons,
"timestamp": datetime.now(),
}
def generate_mock_zones_summary(
zone_ids: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Generate mock zones summary data.
Args:
zone_ids: List of zone identifiers. Defaults to zone_1 through zone_4.
Returns:
Dictionary with per-zone occupancy and aggregate counts.
"""
_show_banner()
zones = zone_ids or ["zone_1", "zone_2", "zone_3", "zone_4"]
zone_data = {}
total_persons = 0
active_zones = 0
for zone_id in zones:
count = random.randint(0, 3)
zone_data[zone_id] = {
"occupancy": count,
"max_occupancy": 10,
"status": "active" if count > 0 else "inactive",
}
total_persons += count
if count > 0:
active_zones += 1
return {
"total_persons": total_persons,
"zones": zone_data,
"active_zones": active_zones,
}
def generate_mock_historical_data(
start_time: datetime,
end_time: datetime,
zone_ids: Optional[List[str]] = None,
aggregation_interval: int = 300,
include_raw_data: bool = False,
) -> Dict[str, Any]:
"""Generate mock historical pose data.
Args:
start_time: Start of the time range.
end_time: End of the time range.
zone_ids: Zones to include. Defaults to zone_1, zone_2, zone_3.
aggregation_interval: Seconds between data points.
include_raw_data: Whether to include simulated raw detections.
Returns:
Dictionary with aggregated_data, optional raw_data, and total_records.
"""
_show_banner()
zones = zone_ids or ["zone_1", "zone_2", "zone_3"]
current_time = start_time
aggregated_data = []
raw_data = [] if include_raw_data else None
while current_time < end_time:
data_point = {
"timestamp": current_time,
"total_persons": random.randint(0, 8),
"zones": {},
}
for zone_id in zones:
data_point["zones"][zone_id] = {
"occupancy": random.randint(0, 3),
"avg_confidence": random.uniform(0.7, 0.95),
}
aggregated_data.append(data_point)
if include_raw_data:
for _ in range(random.randint(0, 5)):
raw_data.append({
"timestamp": current_time + timedelta(seconds=random.randint(0, aggregation_interval)),
"person_id": f"person_{random.randint(1, 10)}",
"zone_id": random.choice(zones),
"confidence": random.uniform(0.5, 0.95),
"activity": random.choice(["standing", "sitting", "walking"]),
})
current_time += timedelta(seconds=aggregation_interval)
return {
"aggregated_data": aggregated_data,
"raw_data": raw_data,
"total_records": len(aggregated_data),
}
def generate_mock_recent_activities(
zone_id: Optional[str] = None,
limit: int = 10,
) -> List[Dict[str, Any]]:
"""Generate mock recent activity data.
Args:
zone_id: Optional zone filter. If None, random zones are used.
limit: Number of activities to generate.
Returns:
List of activity dictionaries.
"""
_show_banner()
activities = []
for i in range(limit):
activity = {
"activity_id": f"activity_{i}",
"person_id": f"person_{random.randint(1, 5)}",
"zone_id": zone_id or random.choice(["zone_1", "zone_2", "zone_3"]),
"activity": random.choice(["standing", "sitting", "walking", "lying"]),
"confidence": random.uniform(0.6, 0.95),
"timestamp": datetime.now() - timedelta(minutes=random.randint(0, 60)),
"duration_seconds": random.randint(10, 300),
}
activities.append(activity)
return activities
def generate_mock_statistics(
start_time: datetime,
end_time: datetime,
) -> Dict[str, Any]:
"""Generate mock pose estimation statistics.
Args:
start_time: Start of the statistics period.
end_time: End of the statistics period.
Returns:
Dictionary with detection counts, rates, and distributions.
"""
_show_banner()
total_detections = random.randint(100, 1000)
successful_detections = int(total_detections * random.uniform(0.8, 0.95))
return {
"total_detections": total_detections,
"successful_detections": successful_detections,
"failed_detections": total_detections - successful_detections,
"success_rate": successful_detections / total_detections,
"average_confidence": random.uniform(0.75, 0.90),
"average_processing_time_ms": random.uniform(50, 200),
"unique_persons": random.randint(5, 20),
"most_active_zone": random.choice(["zone_1", "zone_2", "zone_3"]),
"activity_distribution": {
"standing": random.uniform(0.3, 0.5),
"sitting": random.uniform(0.2, 0.4),
"walking": random.uniform(0.1, 0.3),
"lying": random.uniform(0.0, 0.1),
},
}