Skip to content

Feat/sensor integration - #16

Merged
CarlosVasquez04 merged 10 commits into
mainfrom
feat/sensor-integration
Apr 14, 2026
Merged

Feat/sensor integration#16
CarlosVasquez04 merged 10 commits into
mainfrom
feat/sensor-integration

Conversation

@CarlosVasquez04

@CarlosVasquez04 CarlosVasquez04 commented Dec 2, 2025

Copy link
Copy Markdown
Collaborator

Summary

Added sensor pipeline to the backend, including sensor database tables, a simple SensorClient interface, the Konica Minolta T-10A driver, a sensor manager, and new API endpoints for listing sensors and retrieving latest or past metric data.

Type

  • Feature
  • Fix
  • Docs
  • Chore

Testing

  • Verified that a T-10A device appears as a COM port and can be opened with correct serial settings.
  • Ran a standalone script to confirm PC-mode command and measurement command return data.
  • Started FastAPI and confirmed:
    • /sensors lists registered sensor heads.
    • Automatic polling inserts readings into the DB at the configured interval.
    • /metrics/latest returns current values.
    • /metrics/history returns time-ranged samples suitable for graphing.

Risk and rollout

  • Risks: Serial communication errors or bad parsing could cause missed readings or worker failures.
  • Rollback: Revert the commit, the Halio control system will still work normally.

Checklist

  • Issue linked if applicable
  • Added or updated docs
  • CI green
  • At least one reviewer not the author

… T10A driver, sensor manager with workers, configuration support, and new metrics API endpoints.
@tylerlv3

tylerlv3 commented Dec 7, 2025

Copy link
Copy Markdown
Collaborator

Looks good for what we have to go off of, will have to try once we are able to actually connect to the sensor

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a sensor integration pipeline to enable collection and storage of environmental metrics (e.g., illuminance) from Konica Minolta T-10A light meters. The implementation includes database tables for sensor metadata and readings, a pluggable sensor client interface, a T-10A serial driver, a background worker manager, and REST API endpoints for querying sensor data.

Key Changes:

  • Database schema extension with sensor metadata and time-series readings tables
  • Background worker threads for continuous sensor polling at configurable intervals
  • Three new API endpoints: /sensors, /metrics/latest, and /metrics/history

Reviewed changes

Copilot reviewed 11 out of 13 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
web/package-lock.json Removed peer: true flag from React dependency (unrelated cosmetic change)
svc/requirements.txt Added pyserial==3.5 dependency for serial communication
svc/pyproject.toml Added pyserial>=3.5 to project dependencies
svc/tests/test_t10a_serial.py Manual integration test script for T-10A serial communication (not pytest-compatible)
svc/data/sensors_config.json Configuration file defining T-10A devices, COM ports, polling intervals, and sensor heads
svc/app/state.py Added sensor database tables and CRUD operations (register_sensor, insert_sensor_reading, fetch_readings, etc.)
svc/app/sensors/interface.py Defined SensorClient protocol and SensorReading dataclass for pluggable sensor drivers
svc/app/sensors/t10a_client.py Implemented T-10A serial driver with frame building, command sending, and measurement parsing
svc/app/sensors/manager.py Sensor lifecycle manager that starts worker threads for polling configured sensors
svc/app/routes.py Added three sensor API endpoints for listing sensors and retrieving metric data
svc/app/models.py Added SensorInfo and SensorReadingResponse Pydantic models
svc/main.py Integrated sensor worker startup/shutdown with FastAPI lifecycle events
Files not reviewed (1)
  • web/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +28 to +33
- 9600 baud, 7 data bits, even parity, 1 stop bit.
- ASCII framed commands with STX/ETX + BCC.
- Command '54' → PC mode.
- Command '10' → measurement data output (long frame).
"""

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _build_frame method at lines 60-83 builds empty frame data (pc_mode_frame = b"" and meas_frame = b""), which means actual serial communication will fail. The frame building logic needs to be properly implemented according to the T-10A Communication Specifications before this can work in production. This appears to be a stub implementation.

Copilot uses AI. Check for mistakes.
Comment thread svc/app/sensors/manager.py Outdated
Comment on lines +98 to +100
client = T10AClient(device_id=device_id, port=port, heads=heads_cfg)
clients_with_interval.append((client, interval_s))

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If T10AClient initialization fails (e.g., COM port unavailable or serial communication error), the exception is not caught and will propagate, potentially preventing the entire application from starting. Consider wrapping the client creation in a try-except block and logging the error so other sensors can still initialize successfully.

Suggested change
client = T10AClient(device_id=device_id, port=port, heads=heads_cfg)
clients_with_interval.append((client, interval_s))
try:
client = T10AClient(device_id=device_id, port=port, heads=heads_cfg)
clients_with_interval.append((client, interval_s))
except Exception as e:
logger.error(f"Failed to initialize T10AClient for device_id={device_id} on port={port}: {e}", exc_info=True)

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +40
import time
import serial

PORT = "COM3"


def main():
ser = serial.Serial(
port=PORT,
baudrate=9600,
bytesize=serial.SEVENBITS,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
timeout=1.0,
)

print(f"Opened {PORT}")


def send_frame(raw: bytes) -> str:
ser.write(raw)
ser.flush()
line = ser.readline().decode("ascii", errors="replace").strip()
print("REPLY:", repr(line))
return line

# 1) Build PC mode frame (command 54, head 0)
pc_mode_frame = b""
send_frame(pc_mode_frame)
time.sleep(0.5)

# 2) Build measurement frame (command 10, head 0)
meas_frame = b""
send_frame(meas_frame)

ser.close()


if __name__ == "__main__":
main()

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file doesn't follow pytest conventions and won't be automatically discovered by pytest. The function should be named with a test_ prefix to be recognized as a test (e.g., def test_t10a_serial_communication()), and it should include assertions. Alternatively, if this is meant to be a manual integration test or standalone script, it should be moved out of the tests/ directory to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +52
self.ser = serial.Serial(
port=self.port,
baudrate=9600,
bytesize=serial.SEVENBITS,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
timeout=timeout_s,
)

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serial port connection is not thread-safe. Since poll() is called from a worker thread in manager.py, concurrent calls to self.ser.write() and self.ser.readline() could interleave, leading to corrupted communication. Consider adding a lock (threading.Lock()) around serial I/O operations in _send_command() to ensure thread-safe access.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,161 @@
# app/sensors/t10a_client.py

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File contains a UTF-8 BOM (Byte Order Mark) character  at the beginning. This is unnecessary for UTF-8 files and can cause issues with some tools. Remove the BOM character.

Suggested change
# app/sensors/t10a_client.py
# app/sensors/t10a_client.py

Copilot uses AI. Check for mistakes.
Comment thread svc/app/models.py
Comment on lines +80 to +91
id: str
kind: str
label: str
location: Optional[str] = None
config: Dict = Field(default_factory=dict)


class SensorReadingResponse(BaseModel):
sensor_id: str
metric: str
value: float
ts: float

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new sensor models lack field descriptions. For consistency with the rest of the codebase (which uses Field(description=...) for all model fields), add descriptions to the fields. For example:

class SensorInfo(BaseModel):
    id: str = Field(description="Unique sensor identifier, e.g. 'KM1-00'")
    kind: str = Field(description="Sensor type, e.g. 't10a', 'jeti'")
    label: str = Field(description="Human-readable sensor label")
    location: Optional[str] = Field(None, description="Physical location of the sensor")
    config: Dict = Field(default_factory=dict, description="Sensor-specific configuration")
Suggested change
id: str
kind: str
label: str
location: Optional[str] = None
config: Dict = Field(default_factory=dict)
class SensorReadingResponse(BaseModel):
sensor_id: str
metric: str
value: float
ts: float
id: str = Field(description="Unique sensor identifier, e.g. 'KM1-00'")
kind: str = Field(description="Sensor type, e.g. 't10a', 'jeti'")
label: str = Field(description="Human-readable sensor label")
location: Optional[str] = Field(default=None, description="Physical location of the sensor")
config: Dict = Field(default_factory=dict, description="Sensor-specific configuration")
class SensorReadingResponse(BaseModel):
sensor_id: str = Field(description="Unique sensor identifier for this reading")
metric: str = Field(description="Type of metric measured (e.g., 'illuminance', 'temperature')")
value: float = Field(description="Measured value for the metric")
ts: float = Field(description="Unix timestamp when the reading was taken")

Copilot uses AI. Check for mistakes.
clients_with_interval: list[tuple[SensorClient, float]] = []

# --- T-10A -------------------------------------------------------------
for dev_cfg in cfg.get("t10a", [])[:4]: # enforce 1-4 devices

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded slice [:4] limiting devices to 4 is somewhat arbitrary and inconsistent with the docstring that mentions "up to 4 devices". Consider either removing this limit or making it configurable, or at minimum logging a warning when more than 4 devices are configured but only 4 are being used.

Copilot uses AI. Check for mistakes.
Comment thread svc/app/state.py
Comment on lines +417 to +418


Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sensor_readings table lacks indexes on commonly queried columns. Given the queries in fetch_latest_readings() and fetch_readings(), consider adding indexes on (sensor_id, metric, ts) and (ts) to improve query performance as the table grows with continuous sensor data:

CREATE INDEX IF NOT EXISTS idx_readings_sensor_metric_ts 
  ON sensor_readings(sensor_id, metric, ts);
CREATE INDEX IF NOT EXISTS idx_readings_ts 
  ON sensor_readings(ts);
Suggested change
# Add indexes to improve query performance for common queries
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_readings_sensor_metric_ts ON sensor_readings(sensor_id, metric, ts);"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_readings_ts ON sensor_readings(ts);"
)

Copilot uses AI. Check for mistakes.
Comment thread svc/app/routes.py
Comment on lines +218 to +222
def get_metric_history(
sensor_id: str = Query(..., description="Sensor ID, e.g. KM1-00"),
metric: str = Query(..., description="Metric name, e.g. 'lux'"),
ts_from: float = Query(..., description="Start timestamp (unix seconds)"),
ts_to: float = Query(..., description="End timestamp (unix seconds)"),

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /metrics/history endpoint lacks input validation for the time range. Consider validating that:

  1. ts_from <= ts_to to prevent invalid ranges
  2. The time range is reasonable (e.g., not more than 1 year) to prevent excessive queries
  3. Both timestamps are not in the future

Example:

if ts_from > ts_to:
    raise HTTPException(status_code=400, detail="ts_from must be <= ts_to")
if ts_to - ts_from > 31536000:  # 1 year
    raise HTTPException(status_code=400, detail="Time range cannot exceed 1 year")

Copilot uses AI. Check for mistakes.
Comment thread svc/main.py
Comment on lines +122 to +129
@app.on_event("startup")
async def _start_sensors() -> None:
start_sensor_workers()


@app.on_event("shutdown")
async def _stop_sensors() -> None:
stop_sensor_workers()

Copilot AI Dec 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @app.on_event decorator is deprecated in FastAPI 0.115.5. Use lifespan context managers instead with @asynccontextmanager and pass it to the FastAPI app initialization. See: https://fastapi.tiangolo.com/advanced/events/

Example:

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    start_sensor_workers()
    yield
    # Shutdown
    stop_sensor_workers()

app = FastAPI(lifespan=lifespan)

Copilot uses AI. Check for mistakes.
tylerlv3 and others added 8 commits January 27, 2026 15:20
…results to the backend. Accessible at /metrics/latest
- Add [JetiSpectravalFileWatcher] to ingest sensor data from  files.
- Implement dynamic directory watching: automatically detects and switches to the newest capture file.
- Refactor [JetiSpectravalSimClient] to act as a pure data generator (sim mode).
- Update [manager.py] to coordinate Watcher and Sim clients.
- Config updated to use directory-based output paths for automatic file rotation support.

Frontend:
- Add [LiveGraph] component using  for real-time Lux visualization.
- Integrate graph into .
- Optimize polling intervals for responsiveness.
…of python for now as it seems it may be moore user friendly as we can run it sandboxed in the browser. Will reevaluate and decide if running python server side is a better option. Allows for saving routines, setting intervals, and access to currently available sensor data
…update mappings

- Migrated the Routine Engine from frontend JavaScript execution to Python backend subprocesses for robust, asynchronous execution.
- Implemented SQLite persistence for active and saved routines, ensuring they cleanly survive complete server restarts.
- Refactored the UI to use backend APIs instead of browser local storage for state and logs.
- Fixed a critical database schema bug that caused exponentially recursive runaway subprocess loops on restart.
- Added routine actor tagging to backend commands for clearer audit logging.
- Integrated React Router to support multi-page navigation.
- Built a comprehensive Routine Builder Documentation page with syntax guides, API wrapper references, and example scripts.
- Updated the window mapping configuration with the correct Halio UUIDs.
@CarlosVasquez04
CarlosVasquez04 merged commit 968b888 into main Apr 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants