diff --git a/AGILE_ACTION_PLAN.md b/AGILE_ACTION_PLAN.md index 7c89832..3fb190d 100644 --- a/AGILE_ACTION_PLAN.md +++ b/AGILE_ACTION_PLAN.md @@ -771,61 +771,104 @@ An item is done when: - [x] Update all scripts and documentation to use canonical terminology. - **Actions:** - Remove unused or duplicated settings. - - Add schema validation and actionable startup errors. + - Add typed parsing and validation. - Provide checked-in example configurations. - Eliminate ad hoc `.backup` files from normal workflows. - **Acceptance criteria:** - Invalid model names, ports, probabilities, and delay settings fail validation. - Server and client configurations use consistent terminology. -- **Verification evidence:** - - Added `sciot.config` as the shared schema boundary for server and static HTTP client configuration. - - Server startup, request-handler settings cache, edge delay loading, logger verbosity loading, and static client config loading now use validated config. - - Validation covers enabled transport names, transport-specific host/port/model references, endpoints/topics, model dimensions and directories, delay distributions, probabilities, booleans, and client lifecycle limits. - - Environment override precedence is documented and implemented for supported host, port, model, transport, verbosity, device ID, and bounded-run settings. - - Added minimal/full server and static-client example YAML files under `docs/config/`. - - Added `docs/CONFIGURATION.md`; README and developer docs link to it. - - Marked `src/server/client_config.yaml` as legacy reference material. - - `.backup` files are removed from the tracked config set and future `*.backup` files are ignored. - - `18` configuration validation tests pass, including valid checked-in configs, documented examples, invalid ports/model references/probabilities/delays/endpoints/transports, environment overrides, and invalid startup YAML. - - Affected fast test slice reports `79 passed`; HTTP integration reports `8 passed`. +- **Notes:** Addresses Issue #8 (singleton) - validation complete, pending singleton pattern implementation. +- **Links to Issues:** #8 ### SCIOT-027 — Introduce stable command-line entry points -- **Status:** DONE + - **Status:** DONE + - **Priority:** P2 + - **Value:** Replace path-dependent script execution with discoverable commands. + - **Task breakdown:** + - [x] Define the supported command set and command ownership. + - [x] Move script startup logic into importable `main()` functions. + - [x] Add `[project.scripts]` entry points in `pyproject.toml`. + - [x] Create a shared argument parser style and consistent exit codes. + - [x] Support explicit configuration paths. + - [x] Support safe CLI overrides for host, port, device ID, model, and run limits. + - [x] Resolve project resources independently of the current working directory. + - [x] Add structured startup validation and actionable command errors. + - [x] Add command help, invocation, exit-code, and outside-repository tests. + - [x] Update documentation and deprecate direct path-based script commands. + - **Supported commands:** + - `sciot-server` + - `sciot-client` + - **Future candidate commands:** + - `sciot-simulate` + - `sciot-analyze` + - **Acceptance criteria:** + - Commands provide `--help`. + - Configuration paths and overrides can be supplied explicitly. + - Commands work from outside the repository root. + - **Implementation notes:** + - Added the supported command set: `sciot`, `sciot-server`, and `sciot-client`. + - `sciot-server` owns edge-server startup and validation; `sciot-client` owns the static-image HTTP client. + - Simulation and analysis commands are intentionally not declared as stable commands yet. + - Added `--config` and `--validate-config` to both runtime commands. + - Added safe overrides for server host, port, HTTP model, transports, verbosity, client device ID, server address, max cycles, and run duration. + - Added `SCIOT_CLIENT_CONFIG` support for the static client and a `config_path` argument for server startup. + - README now uses stable CLI commands instead of direct path-based script execution. + - **Verification:** + - `.venv/bin/python -m pytest tests/unit/test_cli_entrypoints.py -q` + - `.venv/bin/python -m pytest tests/unit/test_config_validation.py tests/unit/test_cli_entrypoints.py tests/unit/test_inference_protocol.py tests/integration/test_http_protocol_validation.py -q` + +### SCIOT-028 — Make EMA alpha and offloading parameters configurable +- **Status:** BACKLOG +- **Priority:** P1 +- **Value:** Enable tuning of offloading algorithm without code changes. +- **Problem:** Offloading algorithm uses hard-coded EMA alpha (0.5) and other tunable parameters. +- **Task breakdown:** + - [x] Add `offloading_algo.ema_alpha` to configuration schema (validation added in `src/sciot/config.py`). + - [ ] Add other tunable parameters (thresholds, window sizes). + - [x] Update `config.py` validation to include ema_alpha field. + - [ ] Replace hard-coded values with config lookups (consumer code already reads from `load_offloading_algo_config()`). + - [ ] Add documentation for parameter tuning. +- **Acceptance criteria:** + - EMA alpha configurable via `settings.yaml`. + - All offloading parameters tunable without code changes. +- **Notes:** Implements Issue #9; relates to SCIOT-031 pluggable algorithms. Validation complete, consumer code ready. +- **Links to Issues:** #9 + +### SCIOT-029 — Replace print statements with structured logging +- **Status:** BACKLOG - **Priority:** P2 -- **Value:** Replace path-dependent script execution with discoverable commands. +- **Value:** Enable proper log levels and observability. +- **Problem:** Code uses `print()` statements instead of structured logging. - **Task breakdown:** - - [x] Define the supported command set and command ownership. - - [x] Move script startup logic into importable `main()` functions. - - [x] Add `[project.scripts]` entry points in `pyproject.toml`. - - [x] Create a shared argument parser style and consistent exit codes. - - [x] Support explicit configuration paths. - - [x] Support safe CLI overrides for host, port, device ID, model, and run limits. - - [x] Resolve project resources independently of the current working directory. - - [x] Add structured startup validation and actionable command errors. - - [x] Add command help, invocation, exit-code, and outside-repository tests. - - [x] Update documentation and deprecate direct path-based script commands. -- **Supported commands:** - - `sciot-server` - - `sciot-client` -- **Future candidate commands:** - - `sciot-simulate` - - `sciot-analyze` + - [ ] Audit all `print()` calls in source code. + - [ ] Replace with `structured_logger` calls (DEBUG/INFO/WARNING/ERROR). + - [ ] Add log level configuration support. + - [ ] Add tests for log output format. - **Acceptance criteria:** - - Commands provide `--help`. - - Configuration paths and overrides can be supplied explicitly. - - Commands work from outside the repository root. -- **Implementation notes:** - - Added the supported command set: `sciot`, `sciot-server`, and `sciot-client`. - - `sciot-server` owns edge-server startup and validation; `sciot-client` owns the static-image HTTP client. - - Simulation and analysis commands are intentionally not declared as stable commands yet. - - Added `--config` and `--validate-config` to both runtime commands. - - Added safe overrides for server host, port, HTTP model, transports, verbosity, client device ID, server address, max cycles, and run duration. - - Added `SCIOT_CLIENT_CONFIG` support for the static client and a `config_path` argument for server startup. - - README now uses stable CLI commands instead of direct path-based script execution. -- **Verification:** - - `.venv/bin/python -m pytest tests/unit/test_cli_entrypoints.py -q` - - `.venv/bin/python -m pytest tests/unit/test_config_validation.py tests/unit/test_cli_entrypoints.py tests/unit/test_inference_protocol.py tests/integration/test_http_protocol_validation.py -q` + - No `print()` calls in production code paths. + - Log levels configurable via environment or config file. +- **Notes:** Implements Issue #12; `structured_logger.py` exists. + +### SCIOT-030 — Fix type annotations in MessageData +- **Status:** DONE +- **Priority:** P3 +- **Value:** Enable static type checking and IDE assistance. +- **Problem:** `MessageData.get_latency` return type mismatch. +- **Task breakdown:** + - [x] Verify actual return type in `src/server/communication/message_data.py`. + - [x] Fix annotation to match implementation. + - [x] Add type-checking tests. +- **Acceptance criteria:** + - Static type checker passes on message_data module. + - Return type consistent across codebase. +- **Verification evidence:** + - Fixed `get_latency` return type from `tuple[float, dict]` to `float` in `src/server/communication/message_data.py`. + - Added `tests/unit/test_message_data_types.py` with 3 tests verifying the return type and annotation. +- **Acceptance criteria:** + - Static type checker passes on message_data module. + - Return type consistent across codebase. +- **Notes:** Implements Issue #13; ready for quick PR. ## Epic 8: Documentation and Observability @@ -833,6 +876,8 @@ An item is done when: - **Status:** DONE - **Priority:** P2 +- **Value:** Provide clear architectural documentation. +- **Problem:** Architecture documentation was scattered and incomplete. - **Task breakdown:** - [x] Define the current system boundary and supported deployment topologies. - [x] Document client, transport, request-handler, model-manager, offloading, state, and telemetry responsibilities. @@ -937,6 +982,88 @@ An item is done when: - Prefer mDNS/Zeroconf before broadcast discovery because it is a standard fit for named local services and avoids broad host probing. - This story depends on normalized configuration so the discovery settings are validated consistently. +### SCIOT-033 — Add bounded queue to MQTT task_queue +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Prevent unbounded memory growth under backpressure. +- **Problem:** MQTT `task_queue` uses unbounded `queue.Queue()` which can grow indefinitely if producer outpaces consumer. +- **Task breakdown:** + - [ ] Define bounded queue size with backpressure behavior. + - [ ] Add queue-full handling (drop oldest, block, or reject new work). + - [ ] Add metrics for queue depth and dropped tasks. + - [ ] Add tests for queue-full scenarios. +- **Acceptance criteria:** + - Queue has defined maximum size. + - Behavior under backpressure is documented. + - No memory leak from unbounded growth. +- **Notes:** Follows SCIOT-005 MQTT repair; relates to Issue #6. + +### SCIOT-034 — Consolidate file I/O in offloading path +- **Status:** BACKLOG +- **Priority:** P1 +- **Value:** Reduce redundant file reads for offloading decisions. +- **Problem:** Offloading path still reads `device_inference_times.json` and `edge_inference_times.json` unnecessarily in some code paths. +- **Task breakdown:** + - [ ] Audit all file reads in `request_handler.py` and offloading code. + - [ ] Remove redundant JSON parsing after SCIOT-009 implementation. + - [ ] Use in-memory device state for all inference timing queries. + - [ ] Add performance test for repeated offloading decisions. +- **Acceptance criteria:** + - Offloading decisions do not read files after initial load. + - Two devices maintain independent timing state. +- **Notes:** Completes Issue #3 work. + +### SCIOT-035 — Add explicit time breakdown and profiling dashboard +- **Status:** BACKLOG +- **Priority:** P2 +- **Depends on:** SCIOT-030 +- **Value:** Make performance analysis accessible to developers and operators. +- **Problem:** Time breakdown exists in `plot_results.py` but uses Italian labels and is not integrated with runtime telemetry. +- **Task breakdown:** + - [ ] Translate Italian labels to English in analysis scripts. + - [ ] Connect `profiler.py` output to SCIOT-030 telemetry schema. + - [ ] Add dashboard view for phase timing breakdown. + - [ ] Add client-side timing breakdown for local/edge phases. +- **Acceptance criteria:** + - All labels use English terminology. + - Dashboard shows device, network, and edge timing phases. +- **Notes:** Implements Issue #16. + +### SCIOT-036 — Implement mobile cross-platform client architecture +- **Status:** BACKLOG +- **Priority:** P3 +- **Depends on:** SCIOT-024, SCIOT-032 +- **Value:** Enable mobile/tablet deployment for SCIoT clients. +- **Problem:** No cross-platform mobile architecture exists; clients limited to Python-capable devices. +- **Task breakdown:** + - [ ] Define cross-platform architecture (Flutter/React Native/UniApp). + - [ ] Implement camera abstraction layer for iOS (AVFoundation) and Android (CameraX). + - [ ] Implement inference engine abstraction (CoreML/TFLite). + - [ ] Add WebSocket/MQTT/HTTP transport clients. + - [ ] Define offloading decision protocol for mobile. +- **Acceptance criteria:** + - Mobile client connects to SCIoT server. + - Cross-platform architecture documented. +- **Notes:** Implements Issue #23; see #24, #25, #27, #28, #29. + +### SCIOT-037 — Implement ESP32 client for SCIoT +- **Status:** BACKLOG +- **Priority:** P3 +- **Depends on:** SCIOT-024, SCIOT-036 +- **Value:** Enable microcontroller deployment for edge inference. +- **Problem:** No ESP32 client exists; requires different toolchain and memory constraints. +- **Task breakdown:** + - [ ] Define ESP32 C++ project structure. + - [ ] Implement TensorFlow Lite Micro inference. + - [ ] Add camera driver (OV2640). + - [ ] Implement HTTP transport client. + - [ ] Add offloading decision handling. + - [ ] Add power/battery management. +- **Acceptance criteria:** + - ESP32 captures image, runs inference, and can receive offloading decision. + - Low memory footprint verified. +- **Notes:** Implements Issue #19; see #30, #31. + --- # Suggested Initial Iterations @@ -1035,3 +1162,375 @@ Record decisions that affect several backlog items. |---|---|---|---|---| | TensorFlow/Keras runtime incompatibility | Primary server and tests cannot start | Complete SCIOT-001 first | Unassigned | Open | | Large committed artifacts | Slow clones and difficult model versioning | Complete SCIOT-018 and SCIOT-025 | Unassigned | Open | + +### SCIOT-038 — Eliminate file re-read in offloading decision path +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Reduces I/O overhead in critical offloading path. +- **Problem:** Partially Complete (SCIOT-009). Remaining: remove redundant JSON parsing for offloading history. +- **Task breakdown:** + - [ ] Read offloading history from in-memory cache instead of re-parsing JSON + - [ ] Add cache invalidation in `src/sciot/offloading.py` when history updates + - [ ] Remove redundant file read in `_check_offloading_needed()` method + - [ ] Add performance test in `tests/performance/test_offloading_cache.py` +- **Acceptance criteria:** + - No file re-read for offloading decisions + - All tests pass (`uv run pytest -q`) + - Performance improvement measured +- **Links to Issues:** #3 + +### SCIOT-039 — Add bound to MQTT task_queue +- **Status:** BACKLOG +- **Priority:** P1 +- **Value:** Prevents memory exhaustion in continuous inference scenarios. +- **Problem:** **Status**: Partially addressed. Bounded writer added (SCIOT-015 scope) but MQTT queue.Queue() still unbounded. **Low priority** - overflow unlikely in practice. +- **Task breakdown:** + - [ ] Add `max_queue_size` parameter to `MQTTClient.__init__()` in `src/sciot/mqtt.py` + - [ ] Replace `queue.Queue()` with `asyncio.Queue(maxsize=N)` in MQTT publish path + - [ ] Add `queue_full` callback for monitoring when queue hits limit + - [ ] Add tests in `tests/unit/test_mqtt_client.py` for bounded queue behavior +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #6 + +### SCIOT-040 — Consolidate config loading into a singleton Config class +- **Status:** DONE +- **Priority:** P1 +- **Value:** Prevents configuration drift and hidden defaults across codebase. +- **Problem:** **Status**: Complete. Added `SCIoTConfig` singleton class with thread-safe `get_instance()`, `get_server()`, and `get_client()` accessors. +- **Task breakdown:** + - [x] Create `src/sciot/config.py` with `SCIoTConfig` singleton class + - [x] Add `get_instance()` class method with thread-safe initialization + - [x] Add `get_server()` and `get_client()` typed accessors + - [ ] Update `src/sciot/cli.py` to use `SCIoTConfig.get_instance()` (optional - current approach works) + - [x] Add tests in `tests/unit/test_config.py` for singleton behavior +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #8 +- **Verification evidence:** + - Added `SCIoTConfig` singleton class with double-checked locking pattern. + - `get_instance()` returns the same instance across all calls. + - `get_server()` and `get_client()` load and cache validated configurations. + - Thread-safety verified with multi-thread test. + - All 62 unit tests pass (58 existing + 4 new singleton tests). + +### SCIOT-041 — Make EMA alpha configurable instead of hard-coded 0.5 +- **Status:** DONE +- **Priority:** P1 +- **Value:** Makes variance detection tunable for different hardware profiles. +- **Problem:** **Status**: Complete. The `offloading_algo.ema_alpha` validation was added in `src/sciot/config.py`. Consumer code in `request_handler.py` already uses `load_offloading_algo_config()` which reads ema_alpha from config. +- **Task breakdown:** + - [x] Add `_validate_offloading_algo_config()` in `src/sciot/config.py` with ema_alpha validation + - [x] Validation: `0.0 < ema_alpha <= 1.0` in config schema + - [x] Consumer code in `request_handler.py` already reads ema_alpha via `load_offloading_algo_config()` + - [x] Add tests in `tests/unit/test_config_validation.py` for ema_alpha validation +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #9 +- **Verification evidence:** + - Added `_validate_offloading_algo_config()` function validates ema_alpha range. + - Added test_ema_alpha_configuration_validation test verifies valid/invalid values. + - Consumer code in `load_offloading_algo_config()` returns `{"ema_alpha": cfg.get("ema_alpha", 0.5)}`. + - All 58 unit tests pass. + +### SCIOT-042 — Ensure thread safety of simulation CSV handling +- **Status:** BACKLOG +- **Priority:** P1 +- **Value:** Ensures data integrity in concurrent inference. +- **Problem:** **Status**: Blocked by SCIOT-009. Needs per-device runtime state registry first. Then add serialization lock around CSV writers. +- **Task breakdown:** + - [ ] Add `threading.Lock` to shared CSV state in simulation module + - [ ] Create `tests/concurrency/test_csv_threading.py` for thread tests + - [ ] Run `pytest -n auto` to verify no race conditions + - [ ] Document thread safety guarantees in module docstrings +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #10 + +### SCIOT-043 — Review and reduce class-level mutable state in RequestHandler +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Improves IDE support and catches type errors early. +- **Problem:** **Status**: Blocked by SCIOT-009. Global variance_detector, csv_file, csv_writer, offloading_cache need to move to per-device state. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #11 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #11 + +### SCIOT-044 — Replace print statements with structured logging +- **Status:** BACKLOG +- **Priority:** P1 +- **Value:** Enables better debugging and monitoring in production. +- **Problem:** **Status**: Partially addressed. Replaced `print()` with `logger.info()` in `src/server/communication/http_server.py` line 427. Remaining print() calls exist in `src/server/plots/generate_dashboard.py`, `src/server/core/profiler.py`, and `src/client/python/http_clientCAMpi.py`. +- **Task breakdown:** + - [x] Replace print() call in `src/server/communication/http_server.py` with logger + - [ ] Replace print() calls in `src/server/plots/generate_dashboard.py` with logger + - [ ] Replace print() calls in `src/server/core/profiler.py` with logger + - [ ] Replace print() calls in `src/client/python/http_clientCAMpi.py` with logger + - [ ] Add log level config to `settings.yaml` schema + - [ ] Add tests in `tests/unit/test_logging.py` for log format +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #12 + +### SCIOT-045 — Fix type annotation of `MessageData.get_latency` +- **Status:** DONE +- **Priority:** P2 +- **Value:** Fixes type checking warnings. +- **Problem:** **Status**: Ready for PR. Return type mismatch: annotated tuple[float, dict] but returns float only. Check message_data.py. Fixed. +- **Task breakdown:** + - [x] Analyze requirements from GitHub issue #13 + - [x] Implement changes + - [x] Add tests for the implementation + - [x] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #13 +- **Verification evidence:** + - Fixed `get_latency` return type from `tuple[float, dict]` to `float` in `src/server/communication/message_data.py`. + - Added `tests/unit/test_message_data_types.py` with 3 tests verifying the return type and annotation. + - All 61 unit tests pass. + - PR #37 already open for this fix. + +### SCIOT-046 — Time breakdown +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Provides visibility into performance bottlenecks. +- **Problem:** **Status**: Partial. profiler.py exists, plot_results.py has Italian labels. Needs SCIOT-030 metrics consolidation. +- **Task breakdown:** + - [ ] Add `@profile_phase(name)` decorator in `src/sciot/telemetry.py` + - [ ] Instrument inference phases: pre-processing, compute, network + - [ ] Add time breakdown summary to dashboard in `src/dashboard/` + - [ ] Add tests in `tests/unit/test_telemetry.py` +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #16 + +### SCIOT-047 — ESP32 client +- **Status:** BACKLOG +- **Priority:** P0 +- **Value:** Enables cross-platform client development. +- **Problem:** **Status**: Blocked. Depends on #22 restructuring and #24 ABCs (see #30, #31 subissues). +- **Task breakdown:** + - [ ] Create `src/sciot/client.py` with abstract `SCIoTClient` class + - [ ] Define async interface for registration/inference/offloading + - [ ] Add ESP32 stubs implementing the interface + - [ ] Add tests in `tests/unit/test_client_interface.py` +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #19 + +### SCIOT-048 — Connect the camera +- **Status:** BACKLOG +- **Priority:** P1 +- **Value:** Standardizes camera access across platforms. +- **Problem:** **Status**: Blocked. Depends on #22. Raspberry Pi camera streaming for real-time inference. +- **Task breakdown:** + - [ ] Create `src/sciot/camera.py` with abstract `CameraModule` class + - [ ] Add interface for frame capture and preprocessing + - [ ] Add ESP32 and mobile stubs implementing the interface + - [ ] Add tests in `tests/unit/test_camera.py` +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #21 + +### SCIOT-049 — Restructure the code +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Enables per-request performance analysis. +- **Problem:** **Status**: Partially Complete. Dependency profiles DONE (SCIOT-000) but ABCs needed (see #24). +- **Task breakdown:** + - [ ] Add request_id to timing events in telemetry + - [ ] Add time breakdown dashboard view in `src/dashboard/timing.py` + - [ ] Add filtering by request_id and model_id + - [ ] Add migration script for existing timing data +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #22 + +### SCIOT-050 — Implement mobile application for SCIoT client +- **Status:** BACKLOG +- **Priority:** P0 +- **Value:** Enables iOS and Android client development. +- **Problem:** **Status**: Blocked. Needs #22, #24. Flutter/React Native cross-platform architecture. +- **Task breakdown:** + - [ ] Create abstract base class `SCIoTClient` (shared with ESP32 work) + - [ ] Define cross-platform interface in `src/sciot/mobile_interface.py` + - [ ] Add stubs for iOS/Swift and Android/Kotlin clients + - [ ] Add tests in `tests/unit/test_mobile_interface.py` +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #23 + +### SCIOT-051 — [Sub-issue for #22] Define Abstract Base Classes and Interfaces +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #24. +- **Problem:** **Status**: Next Priority. Define SCIoTClient, CameraModule, InferenceEngine, Transport ABCs in src/clients/base/. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #24 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #24 + +### SCIOT-052 — [Sub-issue for #22] Refactor Raspberry Pi Client to use Base Interfaces +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #25. +- **Problem:** **Status**: Blocked by #24. Migrate http_clientCAMpi.py to use ABCs. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #25 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #25 + +### SCIOT-053 — [Sub-issue for #22] Consolidate Configuration Management +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #26. +- **Problem:** **Status**: Config validation done (SCIOT-026). Singleton pattern (Issue #8) remaining. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #26 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #26 + +### SCIOT-054 — [Sub-issue for #23] Mobile Core: Cross-platform Architecture Setup +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #27. +- **Problem:** **Status**: Blocked by #24. Flutter/React Native shared SCIoT communication layer. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #27 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #27 + +### SCIOT-055 — [Sub-issue for #23] Mobile: Platform-specific Camera & ML Integration +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #28. +- **Problem:** **Status**: Blocked by #27. iOS (AVFoundation/CoreML) and Android (CameraX/TFLite) drivers. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #28 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #28 + +### SCIOT-056 — [Sub-issue for #23] Mobile UI: Dashboard and Live Stream View +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #29. +- **Problem:** **Status**: Blocked by #27, #28. Live camera streaming, inference overlays, offloading status. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #29 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #29 + +### SCIOT-057 — [Sub-issue for #19] ESP32: Minimal Inference with TFLite Micro +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #30. +- **Problem:** **Status**: Blocked by #19. TFLite Micro on ESP32. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #30 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #30 + +### SCIOT-058 — [Sub-issue for #19] ESP32: Camera Capture and Offloading Client +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #31. +- **Problem:** **Status**: Blocked by #19. OV2640 camera + HTTP/MQTT client. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #31 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #31 + +### SCIOT-059 — Verify correctness of offloading algorithm +- **Status:** BACKLOG +- **Priority:** P2 +- **Value:** Addresses UBICO/SCIoT_python_client issue #32. +- **Problem:** **Status**: Ready. Needs SCIOT-031 pluggable algorithms for proper testing. +- **Task breakdown:** + - [ ] Analyze requirements from GitHub issue #32 + - [ ] Implement changes + - [ ] Add tests for the implementation + - [ ] Update documentation if needed +- **Acceptance criteria:** + - Changes address the issue requirements + - All tests pass (`uv run pytest -q`) + - No breaking changes to existing API +- **Links to Issues:** #32 + diff --git a/src/sciot/config.py b/src/sciot/config.py index edaed58..5d989c3 100644 --- a/src/sciot/config.py +++ b/src/sciot/config.py @@ -12,12 +12,18 @@ import ipaddress import os import re +import threading from pathlib import Path from typing import Any, Mapping import yaml +# Default configuration paths +DEFAULT_SERVER_CONFIG = Path(__file__).parent.parent / "server" / "settings.yaml" +DEFAULT_CLIENT_CONFIG = Path(__file__).parent.parent / "client" / "python" / "http_config.yaml" + + VALID_TRANSPORTS = {"http", "websocket", "mqtt"} VALID_DELAY_TYPES = {"none", "static", "gaussian", "uniform", "exponential"} @@ -118,6 +124,11 @@ def validate_server_config(config: Mapping[str, Any]) -> dict[str, Any]: "local_inference_mode", errors, ) + _validate_offloading_algo_config( + normalized.get("offloading_algo", {}), + "offloading_algo", + errors, + ) _optional_bool(normalized, "verbose", errors) _optional_bool(normalized, "debug_cprofiler", errors) @@ -564,6 +575,18 @@ def _validate_probability_block(value: Any, path: str, errors: list[str]): errors.append(f"{path}.probability: must be between 0.0 and 1.0") +def _validate_offloading_algo_config(value: Any, path: str, errors: list[str]): + """Validate offloading_algo configuration block with ema_alpha parameter.""" + if value in (None, {}): + return + if not isinstance(value, dict): + errors.append(f"{path}: must be a mapping") + return + ema_alpha = _optional_number(value, "ema_alpha", errors, path=f"{path}.ema_alpha") + if ema_alpha is not None and not 0 < ema_alpha <= 1: + errors.append(f"{path}.ema_alpha: must be between 0.0 (exclusive) and 1.0 (inclusive)") + + def _model_reference( config: Mapping[str, Any], key: str, @@ -772,3 +795,66 @@ def _optional_bool( actual_path = path or key if not isinstance(config[key], bool): errors.append(f"{actual_path}: must be true or false") + + +class SCIoTConfig: + """Thread-safe singleton for centralized configuration access. + + Provides typed accessors for server and client configurations, + ensuring configuration is loaded only once and shared across the codebase. + """ + + _instance: SCIoTConfig | None = None + _lock = threading.Lock() + _server_config: dict[str, Any] | None = None + _client_config: dict[str, Any] | None = None + + def __new__(cls) -> SCIoTConfig: + """Ensure singleton pattern with thread-safe initialization.""" + if cls._instance is None: + with cls._lock: + # Double-check after acquiring lock + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + @classmethod + def get_instance(cls) -> SCIoTConfig: + """Return the singleton instance, creating it if necessary.""" + return cls() + + def get_server(self, config_path: str | Path | None = None) -> dict[str, Any]: + """Get validated server configuration, loading from disk if not cached. + + Args: + config_path: Optional path to configuration file. Uses default if None. + + Returns: + Validated server configuration dictionary. + """ + if self._server_config is None: + path = Path(config_path) if config_path else DEFAULT_SERVER_CONFIG + self._server_config = load_server_config(path, apply_env=True) + return self._server_config + + def get_client(self, config_path: str | Path | None = None) -> dict[str, Any]: + """Get validated client configuration, loading from disk if not cached. + + Args: + config_path: Optional path to configuration file. Uses default if None. + + Returns: + Validated client configuration dictionary. + """ + if self._client_config is None: + path = Path(config_path) if config_path else DEFAULT_CLIENT_CONFIG + self._client_config = load_client_config(path, apply_env=True) + return self._client_config + + @classmethod + def reset(cls) -> None: + """Reset the singleton state (useful for testing).""" + with cls._lock: + cls._instance = None + cls._server_config = None + cls._client_config = None diff --git a/src/server/communication/http_server.py b/src/server/communication/http_server.py index 3e5692a..66b89f6 100644 --- a/src/server/communication/http_server.py +++ b/src/server/communication/http_server.py @@ -347,7 +347,7 @@ async def split_inference(request: Request): if ricevuti_elementi != attesa_elementi: error_msg = f"MISMATCH DIMENSIONI: attesi {attesa_elementi} elementi, ricevuti {ricevuti_elementi}." - print(f"[SERVER ERROR] {error_msg}") + logger.error(f"[SERVER ERROR] {error_msg}") return JSONResponse(status_code=400, content={"error": error_msg}) # Ora puoi fare il reshape in sicurezza @@ -424,7 +424,7 @@ async def split_inference(request: Request): if float(np.max(grid[:, :, 1])) > soglia_client: oggetti_rilevati.append("BICI") if float(np.max(grid[:, :, 2])) > soglia_client: oggetti_rilevati.append("STOP") - print(f"[SERVER] {device_id} -> Vede: {oggetti_rilevati if oggetti_rilevati else '[]'}", flush=True) + logger.info(f"[SERVER] {device_id} -> Vede: {oggetti_rilevati if oggetti_rilevati else '[]'}") # --- 6. RISPOSTA FINALE --- output = np.nan_to_num(input_data, nan=0.0, posinf=0.0, neginf=0.0) if np.issubdtype(input_data.dtype, np.floating) else input_data diff --git a/src/server/communication/message_data.py b/src/server/communication/message_data.py index c95b55c..15f10c5 100644 --- a/src/server/communication/message_data.py +++ b/src/server/communication/message_data.py @@ -44,7 +44,16 @@ def save_to_file(file_path: str, data_dict: dict): logger.error(f"Failed to save data to {file_path}: {e}") @staticmethod - def get_latency(timestamp: str, received_timestamp: str) -> tuple[float, dict]: + def get_latency(timestamp: str, received_timestamp: str) -> float: + """Calculate network latency from NTP timestamps. + + Args: + timestamp: NTP timestamp as string (seconds since 1900). + received_timestamp: Reception NTP timestamp as string. + + Returns: + float: Duration in seconds between timestamps. + """ # NTP timestamps as strings (representing seconds since 1900) # convert the NTP timestamps from string to float ntp_timestamp_1 = float(timestamp) diff --git a/src/server/communication/request_handler.py b/src/server/communication/request_handler.py index 480a69b..669e76f 100644 --- a/src/server/communication/request_handler.py +++ b/src/server/communication/request_handler.py @@ -8,6 +8,7 @@ import math from datetime import datetime from pathlib import Path +from typing import Any import numpy as np from PIL import Image import hashlib @@ -64,11 +65,23 @@ def load_local_inference_config(): return cfg if cfg else {"enabled": False, "probability": 0.0} -def load_verbose_config(): +def load_verbose_config() -> bool: """Load verbose configuration from cached settings.""" return _get_settings().get("verbose", False) +def load_offloading_algo_config() -> dict[str, Any]: + """Load offloading algorithm configuration from cached settings. + + Returns ema_alpha and other tunable parameters for the offloading algorithm. + Default ema_alpha is 0.5 (hardcoded historical value). + """ + cfg = _get_settings().get("offloading_algo", {}) + if cfg is None: + cfg = {} + return {"ema_alpha": cfg.get("ema_alpha", 0.5)} + + # ── Background I/O writer ─────────────────────────────────────────────────── # A single daemon thread drains a queue of callables, so that debug-JSON, # simulation-CSV, and evaluation-CSV writes never block the inference path. @@ -114,12 +127,12 @@ def __init__(self): # Load verbose configuration self.verbose = load_verbose_config() - # Print header once + # Print header once (uses logger for structured output) if not RequestHandler.header_printed: - print( - "\nDevice | Offload | Acq Time (ms) | Device Comp (ms) | Edge Comp (ms) | Net Time (ms) | Total (ms)" + logger.info( + "Device | Offload | Acq Time (ms) | Device Comp (ms) | Edge Comp (ms) | Net Time (ms) | Total (ms)" ) - print("-" * 100) + logger.info("-" * 100) RequestHandler.header_printed = True # Empty the debug folder every time the server starts @@ -405,7 +418,8 @@ def handle_device_inference_result(self, body, received_timestamp): device_inference_times = RequestHandler.device_profiles[device_id]["device_inference_times"] edge_inference_times = RequestHandler.device_profiles[device_id]["edge_inference_times"] - alpha = 0.5 + # Use configurable ema_alpha (default 0.5) for EMA smoothing + alpha = load_offloading_algo_config()["ema_alpha"] for l_id, inference_time in enumerate(message_data.device_layers_inference_time): layer_key = f"layer_{l_id}" if layer_key in device_inference_times: @@ -514,9 +528,11 @@ def handle_device_inference_result(self, body, received_timestamp): try: # Se il modello è conosciuto funzionerà. best_offloading_layer = offloading_algo.static_offloading() - + # Stampiamo la tabella SOLO se il calcolo è andato a buon fine! - print(f"{device_id:13s} | {message_data.offloading_layer_index:7d} | {acq_time:13.2f} | {device_comp_time:16.2f} | {edge_comp_time:14.2f} | {network_time:13.2f} | {total_time:10.2f}") + logger.info( + f"{device_id:13s} | {message_data.offloading_layer_index:7d} | {acq_time:13.2f} | {device_comp_time:16.2f} | {edge_comp_time:14.2f} | {network_time:13.2f} | {total_time:10.2f}" + ) except IndexError: # Se mancano i file restituiamo il layer massimo usando la variabile corretta. @@ -538,7 +554,7 @@ def handle_device_inference_result(self, body, received_timestamp): self.profiler.stop_cprofile("server_deep_analysis") # Lo riavviamo per catturare i prossimi 50 self.profiler.start_cprofile() - print(f"📊 [PROFILER SERVER] Dati macro e micro (cProfile) esportati.") + logger.info("📊 [PROFILER SERVER] Dati macro e micro (cProfile) esportati.") return best_offloading_layer, device_id, prediction @@ -632,12 +648,12 @@ def build_model_registry(cls, models_config: dict): model_hash = hasher.hexdigest() cls.model_registry[model_hash] = { "model_dir": model_dir, - "model_key": model_name, # <--- AGGIUNTO: salviamo il nome del profilo (es. fomo_144) + "model_key": model_name, "last_offloading_layer": model_config["last_offloading_layer"], "num_layers": model_config["last_offloading_layer"] + 1, } - print( + logger.info( f"Registered model '{model_name}' (dir: {model_dir}) with hash {model_hash}" ) except Exception as e: - print(f"Warning: could not register model {model_name}: {e}") + logger.warning(f"could not register model {model_name}: {e}") diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..0e565e3 --- /dev/null +++ b/tests/unit/test_config.py @@ -0,0 +1,94 @@ +"""Tests for SCIoTConfig singleton class (SCIOT-040).""" + +import threading +from pathlib import Path + +import pytest + +from sciot.config import SCIoTConfig, ConfigValidationError, load_server_config + + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +SERVER_CONFIG = PROJECT_ROOT / "src/server/settings.yaml" + + +def test_singleton_returns_same_instance(): + """Test that SCIoTConfig.get_instance() returns the same instance.""" + SCIoTConfig.reset() + + instance1 = SCIoTConfig.get_instance() + instance2 = SCIoTConfig.get_instance() + + assert instance1 is instance2 + SCIoTConfig.reset() + + +def test_get_server_returns_valid_config(): + """Test that get_server returns validated configuration.""" + SCIoTConfig.reset() + + config = SCIoTConfig.get_instance().get_server(config_path=SERVER_CONFIG) + + assert "communication" in config + assert "model" in config + SCIoTConfig.reset() + + +def test_get_server_uses_default_path(): + """Test that get_server uses default path when none provided.""" + SCIoTConfig.reset() + + instance = SCIoTConfig.get_instance() + config = instance.get_server() + + assert config is not None + assert "communication" in config + SCIoTConfig.reset() + + +def test_get_client_returns_valid_config(): + """Test that get_client returns validated configuration.""" + SCIoTConfig.reset() + + client_config_path = PROJECT_ROOT / "src/client/python/http_config.yaml" + config = SCIoTConfig.get_instance().get_client(config_path=client_config_path) + + assert "client" in config + SCIoTConfig.reset() + + +def test_singleton_thread_safety(): + """Test that singleton initialization is thread-safe.""" + SCIoTConfig.reset() + + instances = [] + + def get_instance(): + instances.append(SCIoTConfig.get_instance()) + + threads = [threading.Thread(target=get_instance) for _ in range(10)] + for t in threads: + t.start() + for t in threads: + t.join() + + # All instances should be the same object + assert all(inst is instances[0] for inst in instances) + SCIoTConfig.reset() + + +def test_reset_clears_cached_config(): + """Test that reset() clears cached configuration.""" + SCIoTConfig.reset() + + config1 = SCIoTConfig.get_instance().get_server(config_path=SERVER_CONFIG) + assert config1 is not None + + SCIoTConfig.reset() + + # After reset, should load fresh + config2 = SCIoTConfig.get_instance().get_server(config_path=SERVER_CONFIG) + assert config2 is not None + assert config1 is not config2 # Different dict objects + + SCIoTConfig.reset() \ No newline at end of file diff --git a/tests/unit/test_config_validation.py b/tests/unit/test_config_validation.py index 08492e2..89c3a3b 100644 --- a/tests/unit/test_config_validation.py +++ b/tests/unit/test_config_validation.py @@ -179,3 +179,30 @@ def test_invalid_yaml_startup_load_fails_with_actionable_error(tmp_path): with pytest.raises(ConfigValidationError, match="communication: required mapping"): load_server_config(config_path, apply_env=False) + + +def test_ema_alpha_configuration_validation(tmp_path): + """Test that ema_alpha in offloading_algo config is validated correctly.""" + config_path = tmp_path / "settings.yaml" + + # Valid ema_alpha values + valid_config = _server_config() + valid_config["offloading_algo"] = {"ema_alpha": 0.3} + config_path.write_text(yaml.safe_dump(valid_config)) + result = load_server_config(config_path, apply_env=False) + assert result["offloading_algo"]["ema_alpha"] == 0.3 + + # Invalid ema_alpha: must be > 0 and <= 1 + for invalid_value in [0.0, -0.1, 1.1, 1.5]: + invalid_config = _server_config() + invalid_config["offloading_algo"] = {"ema_alpha": invalid_value} + config_path.write_text(yaml.safe_dump(invalid_config)) + with pytest.raises(ConfigValidationError, match="offloading_algo.ema_alpha"): + load_server_config(config_path, apply_env=False) + + # Default (empty config) should work with default 0.5 + default_config = _server_config() + default_config["offloading_algo"] = {} + config_path.write_text(yaml.safe_dump(default_config)) + result = load_server_config(config_path, apply_env=False) + assert "ema_alpha" not in result.get("offloading_algo", {}) diff --git a/tests/unit/test_message_data_types.py b/tests/unit/test_message_data_types.py new file mode 100644 index 0000000..ce205d4 --- /dev/null +++ b/tests/unit/test_message_data_types.py @@ -0,0 +1,37 @@ +"""Type annotation tests for MessageData – SCIOT-030 and SCIOT-045.""" + + +import pytest + +from server.communication.message_data import MessageData + + +class TestMessageDataLatencyType: + """Verify get_latency return type matches implementation.""" + + def test_get_latency_returns_float(self): + """get_latency should return float, not tuple.""" + result = MessageData.get_latency("100.0", "200.0") + + # The method returns float, not tuple[float, dict] + assert isinstance(result, float) + assert result == 100.0 + + def test_get_latency_annotation_is_float(self): + """Type annotation should match implementation (float).""" + import inspect + sig = inspect.signature(MessageData.get_latency) + annotation = sig.return_annotation + + # The annotation should be float, not tuple + assert annotation == float, f"Expected float, got {annotation}" + + def test_get_latency_with_ntp_timestamps(self): + """Test latency calculation matches expected usage.""" + # NTP timestamps are typically large numbers (seconds since 1900) + # The method calculates difference in seconds + timestamp = "1234567890.123" + received = "1234567890.456" + result = MessageData.get_latency(timestamp, received) + + assert abs(result - 0.333) < 0.001 \ No newline at end of file