lot
This commit is contained in:
parent
56f957a346
commit
dd54b81262
23
AGENTS.md
Normal file
23
AGENTS.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Entry Points
|
||||
Meteor is split into npm workspaces plus standalone Go/Rust services. `meteor-web-backend/` (NestJS) boots from `src/main.ts` and modules under `src/devices|events|metrics`. `meteor-frontend/` (Next.js App Router) serves UI from `src/app/` with data access helpers in `src/services/`. `meteor-compute-service/` (Go) starts at `cmd/meteor-compute-service/main.go` and pushes SQS payloads through `internal/processor`. `meteor-edge-client/` (Rust) exposes the CLI in `src/main.rs`; the runtime loop lives in `Application::run` inside `src/app.rs`. Shared fixtures sit in `test-data/`, Terraform under `infrastructure/`, and long-form docs in `docs/`.
|
||||
|
||||
## Build & Run
|
||||
- `npm run install:all` installs JS/TS workspaces; `npm run dev` launches backend (3000) and frontend (3001).
|
||||
- `npm run test:backend|frontend|integration|e2e` guard merges; `npm run test:fullstack` orchestrates docker-compose suites via `docker-compose.test.yml`.
|
||||
- Edge client: `cargo run -- run --camera file:meteor-edge-client/video.mp4` replays fixtures locally; use `cargo build --release` for deployment binaries.
|
||||
- Compute service: `go run cmd/meteor-compute-service/main.go` and `go test ./...` before shipping changes.
|
||||
- Use `./test-setup.sh` to provision LocalStack/Postgres before integration tests.
|
||||
|
||||
## Coding Style & Naming
|
||||
TypeScript follows ESLint + Prettier (`npm run lint:*`); keep controllers/services PascalCase, providers suffixed `*.service.ts`, DTOs in `src/devices/dto/`. React components live in `src/app/<route>/` using 2-space indent and camelCase hooks. Rust adheres to `rustfmt` (`cargo fmt`) with modules snake_case; prefer `?` over `unwrap()`. Go code must pass `go fmt` and `go vet`. Avoid committing generated bundles, compiled binaries, or `tmp-home` artifacts.
|
||||
|
||||
## Testing Expectations
|
||||
Backend uses Jest under `test/` with Supertest integration specs; keep `npm run test:cov` green. Frontend combines Jest + Testing Library with Playwright scripts in `e2e/`. Rust edge client relies on `cargo test` plus end-to-end runs such as `cargo run -- run --camera file:…`; test fixtures live in `meteor-edge-client/test-data`. Compute service needs table-driven `go test` coverage. Add regression tests alongside bug fixes.
|
||||
|
||||
## Commit & PR Workflow
|
||||
Write imperative commits with optional scopes (`fix(edge): ...`). Reference issues in bodies, list commands executed, and attach screenshots for UI work or CLI transcripts for device flows. PRs must call out cross-service impacts and any config migrations (`migrations/` or `meteor-client-config.toml`). Wait for CI green and obtain approvals from service owners before merge.
|
||||
|
||||
## Security & Ops
|
||||
Keep JWTs and bearer tokens in `.env` or OS keychain; never commit `meteor-client-config.toml`. Edge devices read configs from `dirs::config_dir()`—override with `HOME=<path>` when testing. Rotate credentials when sharing sample configs and scrub logs before uploading.
|
||||
63
README.md
63
README.md
@ -1,32 +1,49 @@
|
||||
# Meteor Fullstack - Distributed Monitoring Network
|
||||
|
||||
## Project Structure
|
||||
- `meteor-frontend/` - Next.js frontend application
|
||||
- `meteor-web-backend/` - NestJS backend API
|
||||
- `meteor-compute-service/` - Go processing service (submodule)
|
||||
- `meteor-edge-client/` - Rust edge client (submodule)
|
||||
## Overview
|
||||
Meteor Fullstack 是一个覆盖边缘设备、云端 API、数据处理和 Web 前端的完整流星监测网络。仓库采用 npm workspace 管理前后端,并通过 Go、Rust 等多语言组件连接实时观测、事件验证和观测结果展示。
|
||||
|
||||
## Testing & Integration
|
||||
- Complete E2E testing framework with Playwright
|
||||
- Docker-based integration testing with LocalStack
|
||||
- Comprehensive test coverage for Gallery feature
|
||||
## System Topology
|
||||
- `meteor-edge-client/`:Rust 编写的边缘客户端 CLI(命令在 `src/main.rs`),负责摄像头采集、事件检测与上传,核心运行循环位于 `src/app.rs::Application::run`。
|
||||
- `meteor-web-backend/`:NestJS Web API,入口 `src/main.ts` 启动 `AppModule`,聚合认证、设备注册、事件存取、支付与指标等子模块,持久化通过 TypeORM/PostgreSQL。
|
||||
- `meteor-frontend/`:Next.js 15 应用(App Router),入口 `src/app/layout.tsx`,主要页面位于 `src/app/dashboard|gallery|analysis` 等目录,使用 React Query 与自建服务交互。
|
||||
- `meteor-compute-service/`:Go 处理服务,入口 `cmd/meteor-compute-service/main.go`,从 SQS 拉取事件、执行校验并写入数据库,辅以 CloudWatch 指标与健康检查。
|
||||
- `infrastructure/`:Terraform 定义 S3、SQS、CloudWatch、RDS/VPC 等资源,为各服务提供统一云端环境。
|
||||
|
||||
## Recent Milestone: Epic 1 Complete ✅
|
||||
- ✅ Authentication system implemented
|
||||
- ✅ Event upload and processing pipeline
|
||||
- ✅ Gallery page with infinite scroll
|
||||
- ✅ Full integration testing suite
|
||||
- ✅ First Light achieved - complete data flow validated
|
||||
## Execution Entry Points
|
||||
- **边缘采集**:`meteor-edge-client/src/main.rs` 解析 CLI(`Run` 子命令),构造 `Application`,根据 `camera` 参数(例如 `sim:pattern:meteor`)覆盖配置并启动摄像头、检测、存储、心跳等控制器。
|
||||
- **Web API**:`meteor-web-backend/src/main.ts` 通过 `NestFactory` 启动服务,`AppModule` 汇集 `devices`, `events`, `metrics` 等模块;Socket 网关与定时任务由 `ScheduleModule` 提供。
|
||||
- **前端应用**:`meteor-frontend/src/app/page.tsx` 定义默认仪表盘;路由由 `app/` 目录自动生成,`services/` 封装 API 调用,`contexts/` 提供跨页面状态。
|
||||
- **计算服务**:`meteor-compute-service/internal/processor` 中的 `Processor.Start` 协程消费 SQS 消息,配合 `internal/validation` 动态加载检测策略。
|
||||
|
||||
## Quick Start
|
||||
## Data Flow Summary
|
||||
1. 边缘设备通过 `meteor-edge-client run --camera …` 捕获帧,`storage` 模块归档事件,`communication` 模块将打包结果上传后端。
|
||||
2. NestJS 后端在 `events` 与 `devices` 模块中接收上传,写入事件表并向 SQS 推送需要进一步分析的数据。
|
||||
3. Go 计算服务从 SQS 获取消息,调用验证提供者生成分析结果,再回写数据库并发送 CloudWatch 指标。
|
||||
4. 前端通过 React Query 请求 NestJS API,展示仪表盘、图库与分析视图,实现实时监控闭环。
|
||||
|
||||
## Development Workflow
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
|
||||
# Testing
|
||||
npm run install:all # 初始化依赖
|
||||
npm run dev # 并行启动前端(3001)与后端(3000)
|
||||
npm run test:fullstack
|
||||
|
||||
# Setup integration tests
|
||||
./test-setup.sh
|
||||
./test-setup.sh # 启动 LocalStack + 测试数据库
|
||||
```
|
||||
Rust 与 Go 组件分别使用 `cargo run`, `cargo test`, `go run`, `go test ./...`;详细测试矩阵参见 `TESTING.md`。
|
||||
|
||||
## Testing & Verification
|
||||
- 前端:Jest + Testing Library (`npm run test:frontend`),Playwright E2E (`npm run test:e2e`).
|
||||
- 后端:Jest 单元与集成测试 (`npm run test:backend`, `npm run test:integration`).
|
||||
- 边缘客户端:`cargo check`、`cargo test`;模拟摄像机脚本位于 `meteor-edge-client/scripts/`。
|
||||
- 组合流程:`npm run test:fullstack` 调用前后端及集成测试,配合 `docker-compose.test.yml`。
|
||||
|
||||
## Infrastructure & Operations
|
||||
- Terraform 输出的 S3/SQS/CloudWatch 资源需同步到 `.env` 与部署配置,细节参见 `infrastructure/README.md`。
|
||||
- Pino + CloudWatch 提供日志链路,`metrics` 模块和 Go `metrics` 包推送业务指标。
|
||||
- CI/CD 可利用 `meteor-edge-client/.github/workflows/` 示例以及 repo 根目录的 npm scripts。
|
||||
|
||||
## Additional Documentation
|
||||
- 贡献者指南:`AGENTS.md`
|
||||
- 测试细节:`TESTING.md`
|
||||
- 边缘摄像模拟:`meteor-edge-client/CAMERA_SIMULATION_USAGE.md`
|
||||
- 基础设施:`infrastructure/README.md`
|
||||
|
||||
@ -1,219 +0,0 @@
|
||||
# Camera Controller Module - Story 4.2 Implementation
|
||||
|
||||
This document describes the implementation of the Camera Controller module that captures video frames and publishes them to the event bus for real-time processing.
|
||||
|
||||
## Features Implemented
|
||||
|
||||
✅ **Event-Driven Architecture**: Camera controller runs as an independent Tokio task and communicates via the central EventBus
|
||||
|
||||
✅ **Configurable Frame Rate**: Supports configurable FPS (default: 30 FPS)
|
||||
|
||||
✅ **Multiple Input Sources**:
|
||||
- Physical camera devices (requires OpenCV installation)
|
||||
- Video files (requires OpenCV installation)
|
||||
- **Simulated camera** (works without OpenCV - currently active)
|
||||
|
||||
✅ **High-Precision Timestamps**: Each frame includes precise UTC timestamps
|
||||
|
||||
✅ **Frame Metadata**: Each FrameCapturedEvent includes:
|
||||
- Unique frame ID (auto-incrementing)
|
||||
- High-precision timestamp
|
||||
- Frame dimensions (width/height)
|
||||
- Compressed frame data (JPEG format)
|
||||
|
||||
## Current Implementation: Simulated Camera
|
||||
|
||||
The current implementation uses a **simulated camera controller** that generates synthetic video frames. This allows the entire event-driven system to be tested without requiring OpenCV installation.
|
||||
|
||||
### Running the Simulated Camera
|
||||
|
||||
```bash
|
||||
# Run the edge client with simulated camera
|
||||
cargo run -- run
|
||||
```
|
||||
|
||||
### Sample Output
|
||||
|
||||
```
|
||||
🎯 Initializing Event-Driven Meteor Edge Client...
|
||||
📊 Application Statistics:
|
||||
Event Bus Capacity: 1000
|
||||
Initial Subscribers: 0
|
||||
🚀 Starting Meteor Edge Client Application...
|
||||
✅ Received SystemStartedEvent!
|
||||
🎥 Starting simulated camera controller...
|
||||
Source: Device(0)
|
||||
Target FPS: 30
|
||||
Resolution: 640x480
|
||||
📸 Received FrameCapturedEvent #1
|
||||
Timestamp: 2025-07-30 17:39:35.969333 UTC
|
||||
Resolution: 640x480
|
||||
Data size: 115206 bytes
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### App Configuration File
|
||||
|
||||
The camera can be configured via a TOML configuration file. The system looks for:
|
||||
|
||||
1. `/etc/meteor-client/app-config.toml` (system-wide)
|
||||
2. `~/.config/meteor-client/app-config.toml` (user-specific)
|
||||
3. `meteor-app-config.toml` (local directory)
|
||||
|
||||
### Sample Configuration
|
||||
|
||||
```toml
|
||||
[camera]
|
||||
source = "device" # "device" for camera, or path to video file
|
||||
device_id = 0 # Camera device ID (for source = "device")
|
||||
fps = 30.0 # Target frame rate
|
||||
width = 640 # Frame width (optional)
|
||||
height = 480 # Frame height (optional)
|
||||
```
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
**Physical Camera:**
|
||||
```toml
|
||||
[camera]
|
||||
source = "device"
|
||||
device_id = 0
|
||||
fps = 30.0
|
||||
width = 1280
|
||||
height = 720
|
||||
```
|
||||
|
||||
**Video File:**
|
||||
```toml
|
||||
[camera]
|
||||
source = "/path/to/video.mp4"
|
||||
fps = 25.0
|
||||
```
|
||||
|
||||
## Enabling Physical Camera Support (OpenCV)
|
||||
|
||||
To enable real camera and video file support, you need to install OpenCV and uncomment the dependency.
|
||||
|
||||
### Installing OpenCV
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install opencv
|
||||
```
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install libopencv-dev clang libclang-dev
|
||||
```
|
||||
|
||||
**Enable OpenCV in Code:**
|
||||
|
||||
1. Edit `Cargo.toml`, uncomment the opencv dependency:
|
||||
```toml
|
||||
opencv = { version = "0.88", default-features = false }
|
||||
```
|
||||
|
||||
2. Update `src/camera.rs` to use the OpenCV implementation instead of the simulated version
|
||||
|
||||
## Event Structure
|
||||
|
||||
### FrameCapturedEvent
|
||||
|
||||
```rust
|
||||
pub struct FrameCapturedEvent {
|
||||
pub frame_id: u64, // Unique frame identifier (1-based)
|
||||
pub timestamp: DateTime<Utc>, // High-precision capture timestamp
|
||||
pub width: u32, // Frame width in pixels
|
||||
pub height: u32, // Frame height in pixels
|
||||
pub frame_data: Vec<u8>, // JPEG-encoded frame data
|
||||
}
|
||||
```
|
||||
|
||||
### Event Publishing
|
||||
|
||||
The camera controller publishes events to the central EventBus:
|
||||
|
||||
```rust
|
||||
event_bus.publish_frame_captured(frame_event)?;
|
||||
```
|
||||
|
||||
### Event Subscription
|
||||
|
||||
Other modules can subscribe to frame events:
|
||||
|
||||
```rust
|
||||
let mut receiver = event_bus.subscribe();
|
||||
|
||||
while let Ok(event) = receiver.recv().await {
|
||||
match event {
|
||||
SystemEvent::FrameCaptured(frame_event) => {
|
||||
println!("Frame #{}: {}x{}, {} bytes",
|
||||
frame_event.frame_id,
|
||||
frame_event.width,
|
||||
frame_event.height,
|
||||
frame_event.frame_data.len()
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Frame Rate Control**: Precise timing control maintains consistent FPS
|
||||
- **Memory Efficient**: Frames are JPEG-compressed to reduce memory usage
|
||||
- **Async Processing**: Camera runs in independent Tokio task, non-blocking
|
||||
- **Configurable Buffer**: EventBus capacity can be tuned based on processing speed
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
cargo test camera
|
||||
```
|
||||
|
||||
### Integration Test
|
||||
```bash
|
||||
cargo run -- run
|
||||
```
|
||||
|
||||
## Architecture Integration
|
||||
|
||||
The Camera Controller fits into the larger event-driven architecture:
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Camera │───▶│ EventBus │───▶│ Detection │
|
||||
│ Controller │ │ │ │ Module │
|
||||
│ │ │ │ │ (Future) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ │
|
||||
│ Storage │
|
||||
│ Module │
|
||||
│ (Future) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
With the Camera Controller implemented, the system is ready for:
|
||||
|
||||
1. **Detection Module**: Process frames for motion/object detection
|
||||
2. **Storage Module**: Save frames and metadata to persistent storage
|
||||
3. **Analytics Module**: Generate insights from captured data
|
||||
4. **Network Module**: Stream/upload data to cloud services
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Failed to publish frame captured event"**: This is normal when no subscribers are active. The camera will continue generating frames.
|
||||
|
||||
**OpenCV Build Errors**: Ensure OpenCV development libraries are installed on your system before enabling the opencv dependency.
|
||||
|
||||
**High CPU Usage**: Reduce FPS in configuration if system resources are limited.
|
||||
497
meteor-edge-client/CAMERA_SIMULATION_USAGE.md
Normal file
497
meteor-edge-client/CAMERA_SIMULATION_USAGE.md
Normal file
@ -0,0 +1,497 @@
|
||||
# Camera Simulation Usage Guide
|
||||
|
||||
This guide provides comprehensive instructions for using the camera simulation system in the meteor edge client for development, testing, and continuous integration.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Quick Start](#quick-start)
|
||||
2. [Configuration](#configuration)
|
||||
3. [Camera Source Types](#camera-source-types)
|
||||
4. [Development Workflow](#development-workflow)
|
||||
5. [Testing Strategies](#testing-strategies)
|
||||
6. [CI/CD Integration](#cicd-integration)
|
||||
7. [Troubleshooting](#troubleshooting)
|
||||
8. [Performance Optimization](#performance-optimization)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Ensure you have Rust installed
|
||||
rustc --version
|
||||
|
||||
# Install FFmpeg (optional, for full video support)
|
||||
# Ubuntu/Debian:
|
||||
sudo apt-get install ffmpeg libavcodec-dev libavformat-dev
|
||||
|
||||
# macOS:
|
||||
brew install ffmpeg
|
||||
|
||||
# For V4L2 loopback on Linux:
|
||||
sudo apt-get install v4l2loopback-dkms v4l2loopback-utils
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Build with camera simulation support
|
||||
cargo build --features camera_sim
|
||||
|
||||
# Run with test pattern
|
||||
cargo run --features camera_sim -- --camera-source "test:meteor"
|
||||
|
||||
# Run with video file
|
||||
cargo run --features camera_sim -- --camera-source "file:test_data/meteor_video.mp4"
|
||||
|
||||
# Run with specific test pattern
|
||||
cargo run --features camera_sim -- --camera-source "test:checkerboard"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### TOML Configuration
|
||||
|
||||
Add camera simulation configuration to your `meteor-client-config.toml`:
|
||||
|
||||
```toml
|
||||
[camera]
|
||||
# Source type: "device", "file", "test_pattern", "v4l2_loopback", "network"
|
||||
source_type = "test_pattern"
|
||||
|
||||
# Test pattern configuration
|
||||
[camera.test_pattern]
|
||||
type = "simulated_meteor" # Options: static, noise, bar, meteor, checkerboard, gradient
|
||||
fps = 30.0
|
||||
width = 1280
|
||||
height = 720
|
||||
duration_seconds = 300 # Optional, infinite if not specified
|
||||
|
||||
# File input configuration
|
||||
[camera.file]
|
||||
path = "test_data/meteor_capture.mp4"
|
||||
loop = true
|
||||
playback_speed = 1.0
|
||||
|
||||
# V4L2 loopback configuration (Linux only)
|
||||
[camera.v4l2_loopback]
|
||||
device = "/dev/video10"
|
||||
input_file = "test_data/meteor_capture.mp4" # Optional
|
||||
|
||||
# Performance settings
|
||||
[camera.performance]
|
||||
enable_memory_optimization = true
|
||||
max_camera_memory_mb = 64
|
||||
buffer_pool_size = 20
|
||||
```
|
||||
|
||||
### Command Line Options
|
||||
|
||||
```bash
|
||||
# Specify camera source directly
|
||||
--camera-source "test:meteor" # Meteor simulation pattern
|
||||
--camera-source "test:static" # Static test pattern
|
||||
--camera-source "test:noise" # Random noise pattern
|
||||
--camera-source "test:bar" # Moving bar pattern
|
||||
--camera-source "test:checkerboard" # Checkerboard pattern
|
||||
--camera-source "test:gradient" # Gradient pattern
|
||||
|
||||
# File sources
|
||||
--camera-source "file:video.mp4" # Video file input
|
||||
--camera-source "file:/path/to/video.mp4" # Absolute path
|
||||
|
||||
# Device sources
|
||||
--camera-source "device:0" # Camera device index
|
||||
--camera-source "v4l2:/dev/video10" # V4L2 loopback device (Linux)
|
||||
|
||||
# Test mode options
|
||||
--test-mode # Enable test mode
|
||||
--frames 100 # Capture specific number of frames
|
||||
--benchmark-mode # Enable performance benchmarking
|
||||
```
|
||||
|
||||
## Camera Source Types
|
||||
|
||||
### 1. Test Patterns
|
||||
|
||||
Synthetic patterns generated in real-time for testing detection algorithms.
|
||||
|
||||
```bash
|
||||
# Available patterns:
|
||||
cargo run --features camera_sim -- --camera-source "test:static" # Fixed pattern
|
||||
cargo run --features camera_sim -- --camera-source "test:noise" # Random noise
|
||||
cargo run --features camera_sim -- --camera-source "test:bar" # Moving bar
|
||||
cargo run --features camera_sim -- --camera-source "test:meteor" # Meteor simulation
|
||||
cargo run --features camera_sim -- --camera-source "test:checkerboard" # Alternating squares
|
||||
cargo run --features camera_sim -- --camera-source "test:gradient" # Gradient pattern
|
||||
```
|
||||
|
||||
**Meteor Simulation Features:**
|
||||
- Random meteor events every 3-10 seconds
|
||||
- Realistic brightness variations (2-4x normal brightness)
|
||||
- Multiple meteor events per capture session
|
||||
- Deterministic patterns for testing
|
||||
|
||||
### 2. File Reader
|
||||
|
||||
Read video files for testing with realistic astronomical data.
|
||||
|
||||
```bash
|
||||
# Supported formats: MP4, AVI, MKV, MOV
|
||||
cargo run --features camera_sim -- --camera-source "file:test_data/meteor_2024.mp4"
|
||||
|
||||
# With FFmpeg support (more formats):
|
||||
cargo run --features "camera_sim,ffmpeg" -- --camera-source "file:complex_video.mkv"
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Automatic loop playback
|
||||
- Variable playback speed
|
||||
- Resolution detection from filename
|
||||
- Frame rate detection
|
||||
- Seeking support (with FFmpeg)
|
||||
|
||||
### 3. V4L2 Loopback (Linux Only)
|
||||
|
||||
Use virtual V4L2 devices for testing with external video streams.
|
||||
|
||||
```bash
|
||||
# Setup virtual camera
|
||||
sudo scripts/setup_virtual_camera.sh start meteor_video.mp4
|
||||
|
||||
# Use virtual camera
|
||||
cargo run --features camera_sim -- --camera-source "v4l2:/dev/video10"
|
||||
|
||||
# Multiple virtual cameras
|
||||
sudo scripts/setup_virtual_camera.sh start-all meteor_video.mp4
|
||||
cargo run --features camera_sim -- --camera-source "v4l2:/dev/video11"
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Testing with external streaming sources
|
||||
- Integration with existing camera testing tools
|
||||
- Multi-camera simulation
|
||||
- Real-time video processing testing
|
||||
|
||||
### 4. FFmpeg Integration (Optional)
|
||||
|
||||
Enhanced video processing with FFmpeg backend.
|
||||
|
||||
```bash
|
||||
# Enable FFmpeg support
|
||||
cargo build --features "camera_sim,ffmpeg"
|
||||
|
||||
# Supports additional formats and hardware acceleration
|
||||
cargo run --features "camera_sim,ffmpeg" -- --camera-source "file:high_res_video.mkv"
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Hardware acceleration support
|
||||
- Broader format compatibility
|
||||
- Advanced video processing capabilities
|
||||
- Frame-accurate seeking
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Algorithm Development
|
||||
|
||||
```bash
|
||||
# Start with static pattern for basic testing
|
||||
cargo run --features camera_sim -- --camera-source "test:static" --frames 100
|
||||
|
||||
# Test with meteor simulation
|
||||
cargo run --features camera_sim -- --camera-source "test:meteor" --frames 1000
|
||||
|
||||
# Use real video data for validation
|
||||
cargo run --features camera_sim -- --camera-source "file:real_meteor_capture.mp4"
|
||||
```
|
||||
|
||||
### 2. Performance Testing
|
||||
|
||||
```bash
|
||||
# Benchmark different patterns
|
||||
cargo run --release --features camera_sim -- --benchmark-mode --camera-source "test:meteor"
|
||||
|
||||
# Test file reader performance
|
||||
cargo run --release --features camera_sim -- --benchmark-mode --camera-source "file:1080p_video.mp4"
|
||||
|
||||
# Memory leak testing
|
||||
scripts/test_camera_simulation.sh memory
|
||||
```
|
||||
|
||||
### 3. Integration Testing
|
||||
|
||||
```bash
|
||||
# Run comprehensive test suite
|
||||
scripts/test_camera_simulation.sh all
|
||||
|
||||
# Test specific components
|
||||
scripts/test_camera_simulation.sh patterns # Test pattern generators
|
||||
scripts/test_camera_simulation.sh files # Test file readers
|
||||
scripts/test_camera_simulation.sh performance # Performance benchmarks
|
||||
scripts/test_camera_simulation.sh concurrent # Concurrent access
|
||||
scripts/test_camera_simulation.sh errors # Error handling
|
||||
```
|
||||
|
||||
## Testing Strategies
|
||||
|
||||
### Unit Testing
|
||||
|
||||
```bash
|
||||
# Run camera simulation unit tests
|
||||
cargo test --features camera_sim camera_sim::
|
||||
|
||||
# Test specific modules
|
||||
cargo test --features camera_sim test_pattern
|
||||
cargo test --features camera_sim file_reader
|
||||
cargo test --features camera_sim camera_source_factory
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
```bash
|
||||
# End-to-end camera simulation test
|
||||
cargo test --features camera_sim integration_camera_simulation
|
||||
|
||||
# Test with real detection pipeline
|
||||
cargo test --features camera_sim test_meteor_detection_pipeline
|
||||
```
|
||||
|
||||
### Performance Testing
|
||||
|
||||
```bash
|
||||
# Benchmark frame generation
|
||||
cargo bench --features camera_sim bench_pattern_generation
|
||||
|
||||
# Memory usage analysis
|
||||
cargo run --features camera_sim -- --profile-memory --camera-source "test:meteor"
|
||||
|
||||
# Throughput testing
|
||||
cargo run --release --features camera_sim -- --measure-throughput --frames 10000
|
||||
```
|
||||
|
||||
### Automated Testing
|
||||
|
||||
Create test data:
|
||||
|
||||
```bash
|
||||
# Generate test videos
|
||||
scripts/test_camera_simulation.sh
|
||||
|
||||
# This creates:
|
||||
# - test_data/test_480p.mp4
|
||||
# - test_data/test_720p.mp4
|
||||
# - test_data/test_1080p.mp4
|
||||
# - test_data/meteor_simulation.mp4
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### Docker Integration
|
||||
|
||||
```dockerfile
|
||||
# Dockerfile for testing environment
|
||||
FROM rust:1.70
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ffmpeg libavcodec-dev libavformat-dev \
|
||||
v4l2loopback-dkms v4l2loopback-utils
|
||||
|
||||
# Copy and build
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN cargo build --features "camera_sim,ffmpeg"
|
||||
|
||||
# Run tests
|
||||
CMD ["cargo", "test", "--features", "camera_sim"]
|
||||
```
|
||||
|
||||
### Test Data Management
|
||||
|
||||
```bash
|
||||
# Download test videos for CI
|
||||
curl -O https://example.com/test_videos/meteor_test_suite.zip
|
||||
unzip meteor_test_suite.zip -d test_data/
|
||||
|
||||
# Verify test data
|
||||
ls -la test_data/
|
||||
# - meteor_bright.mp4 (bright meteor events)
|
||||
# - meteor_faint.mp4 (faint meteor events)
|
||||
# - meteor_multiple.mp4 (multiple meteors)
|
||||
# - background_only.mp4 (no meteors, background)
|
||||
# - noise_test.mp4 (high noise conditions)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. "Camera source not available"
|
||||
```bash
|
||||
# Check if feature is enabled
|
||||
cargo build --features camera_sim
|
||||
|
||||
# Verify source specification
|
||||
cargo run --features camera_sim -- --camera-source "test:meteor" # Correct
|
||||
cargo run --features camera_sim -- --camera-source "test:invalid" # Incorrect
|
||||
```
|
||||
|
||||
#### 2. "File not found" errors
|
||||
```bash
|
||||
# Check file path
|
||||
ls -la test_data/your_video.mp4
|
||||
|
||||
# Use absolute path
|
||||
cargo run --features camera_sim -- --camera-source "file:/absolute/path/to/video.mp4"
|
||||
```
|
||||
|
||||
#### 3. V4L2 device errors (Linux)
|
||||
```bash
|
||||
# Check if module is loaded
|
||||
lsmod | grep v4l2loopback
|
||||
|
||||
# Load module manually
|
||||
sudo modprobe v4l2loopback devices=1 video_nr=10
|
||||
|
||||
# Check device permissions
|
||||
ls -la /dev/video*
|
||||
sudo chmod 666 /dev/video10
|
||||
```
|
||||
|
||||
#### 4. FFmpeg build errors
|
||||
```bash
|
||||
# Install development libraries
|
||||
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev
|
||||
|
||||
# Or disable FFmpeg support
|
||||
cargo build --features camera_sim # Without FFmpeg
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
#### High memory usage
|
||||
```toml
|
||||
# Reduce buffer pool size
|
||||
[camera.performance]
|
||||
buffer_pool_size = 10
|
||||
max_camera_memory_mb = 32
|
||||
```
|
||||
|
||||
#### Low frame rate
|
||||
```bash
|
||||
# Check system resources
|
||||
top
|
||||
htop
|
||||
|
||||
# Reduce resolution or frame rate
|
||||
cargo run --features camera_sim -- --camera-source "test:meteor" --resolution 640x480
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
#### Enable debug logging
|
||||
```bash
|
||||
RUST_LOG=debug cargo run --features camera_sim -- --camera-source "test:meteor"
|
||||
```
|
||||
|
||||
#### Memory leak detection
|
||||
```bash
|
||||
valgrind --leak-check=full ./target/debug/meteor-edge-client --camera-source "test:static"
|
||||
```
|
||||
|
||||
#### Performance profiling
|
||||
```bash
|
||||
perf record ./target/release/meteor-edge-client --camera-source "test:meteor" --frames 1000
|
||||
perf report
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memory Optimization
|
||||
|
||||
```rust
|
||||
// Use smaller buffer pools for resource-constrained devices
|
||||
let frame_pool = Arc::new(HierarchicalFramePool::new(8)); // Reduced from 20
|
||||
|
||||
// Enable memory optimization
|
||||
camera_config.enable_memory_optimization = true;
|
||||
camera_config.max_camera_memory = 32 * 1024 * 1024; // 32MB limit
|
||||
```
|
||||
|
||||
### CPU Optimization
|
||||
|
||||
```bash
|
||||
# Use release builds for performance testing
|
||||
cargo build --release --features camera_sim
|
||||
|
||||
# Enable specific CPU features
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --release --features camera_sim
|
||||
```
|
||||
|
||||
### Network Optimization
|
||||
|
||||
```toml
|
||||
# For network streams (future feature)
|
||||
[camera.network]
|
||||
buffer_size_mb = 16
|
||||
connection_timeout_ms = 5000
|
||||
read_timeout_ms = 1000
|
||||
```
|
||||
|
||||
### Platform-Specific Optimizations
|
||||
|
||||
#### Raspberry Pi
|
||||
```toml
|
||||
[camera.pi]
|
||||
frame_width = 1280
|
||||
frame_height = 720
|
||||
fps = 15.0 # Conservative for Pi
|
||||
buffer_pool_size = 4
|
||||
enable_memory_optimization = true
|
||||
```
|
||||
|
||||
#### High-Performance Server
|
||||
```toml
|
||||
[camera.server]
|
||||
frame_width = 1920
|
||||
frame_height = 1080
|
||||
fps = 60.0
|
||||
buffer_pool_size = 32
|
||||
enable_hardware_acceleration = true
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Test Data Organization
|
||||
```
|
||||
test_data/
|
||||
├── meteors/
|
||||
│ ├── bright_meteors.mp4
|
||||
│ ├── faint_meteors.mp4
|
||||
│ └── multiple_meteors.mp4
|
||||
├── backgrounds/
|
||||
│ ├── clear_sky.mp4
|
||||
│ ├── cloudy_sky.mp4
|
||||
│ └── high_noise.mp4
|
||||
└── synthetic/
|
||||
├── patterns.mp4
|
||||
└── calibration.mp4
|
||||
```
|
||||
|
||||
### 2. Configuration Management
|
||||
```bash
|
||||
# Environment-specific configs
|
||||
meteor-client-config.dev.toml # Development
|
||||
meteor-client-config.test.toml # Testing
|
||||
meteor-client-config.prod.toml # Production
|
||||
```
|
||||
|
||||
### 3. Continuous Validation
|
||||
```bash
|
||||
# Regular test suite execution
|
||||
crontab -e
|
||||
0 */6 * * * cd /path/to/project && scripts/test_camera_simulation.sh
|
||||
```
|
||||
|
||||
This comprehensive guide should enable you to effectively use the camera simulation system for development, testing, and deployment of the meteor edge client.
|
||||
12
meteor-edge-client/Cargo.lock
generated
12
meteor-edge-client/Cargo.lock
generated
@ -136,6 +136,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
@ -1329,6 +1340,7 @@ name = "meteor-edge-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
|
||||
@ -2,6 +2,18 @@
|
||||
name = "meteor-edge-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
default-run = "meteor-edge-client"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# Production hardware features
|
||||
hardware_camera = []
|
||||
opencv_integration = []
|
||||
|
||||
# Debug and development features
|
||||
debug_logging = []
|
||||
performance_profiling = []
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
@ -28,8 +40,8 @@ num_cpus = "1.16"
|
||||
# Device registration and security dependencies
|
||||
sha2 = "0.10"
|
||||
base64 = "0.22"
|
||||
rand = "0.8"
|
||||
hmac = "0.12"
|
||||
rand = "0.8"
|
||||
jsonwebtoken = { version = "9.2", default-features = false }
|
||||
hex = "0.4"
|
||||
ring = "0.17"
|
||||
@ -48,6 +60,11 @@ sysinfo = "0.30"
|
||||
mac_address = "1.1"
|
||||
# opencv = { version = "0.88", default-features = false } # Commented out for demo - requires system OpenCV installation
|
||||
|
||||
# Camera interface dependencies
|
||||
async-trait = "0.1"
|
||||
|
||||
# Optional video processing backends
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3", features = ["memoryapi", "winnt", "handleapi"] }
|
||||
|
||||
|
||||
@ -1,755 +0,0 @@
|
||||
# 内存管理优化方案
|
||||
|
||||
## 当前问题分析
|
||||
|
||||
### 1. 内存拷贝问题
|
||||
当前架构中存在的主要内存问题:
|
||||
|
||||
```rust
|
||||
// 当前实现 - 每次事件传递都会克隆整个帧数据
|
||||
pub struct FrameCapturedEvent {
|
||||
pub frame_data: Vec<u8>, // 640x480 RGB = ~900KB per frame
|
||||
}
|
||||
|
||||
// 问题分析:
|
||||
// - 30 FPS = 27MB/秒的内存拷贝
|
||||
// - 事件总线广播时,每个订阅者都会克隆数据
|
||||
// - 3个订阅者 = 81MB/秒的内存操作
|
||||
```
|
||||
|
||||
### 2. 内存分配压力
|
||||
- 每帧都需要新的内存分配
|
||||
- GC压力导致延迟峰值
|
||||
- 内存碎片化问题
|
||||
|
||||
### 3. 缓冲区管理
|
||||
- Detection模块维护独立的帧缓冲
|
||||
- Storage模块也有自己的缓冲
|
||||
- 重复存储相同数据
|
||||
|
||||
## 优化方案详细设计
|
||||
|
||||
### 方案1: 零拷贝架构 (Zero-Copy Architecture)
|
||||
|
||||
#### A. 使用Arc实现共享不可变数据
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use bytes::Bytes;
|
||||
|
||||
// 新的事件结构 - 使用Arc共享数据
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FrameCapturedEvent {
|
||||
pub frame_id: u64,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub metadata: FrameMetadata,
|
||||
pub frame_data: Arc<FrameData>, // 共享引用,克隆只增加引用计数
|
||||
}
|
||||
|
||||
// 帧数据包装,包含原始数据和元信息
|
||||
#[derive(Debug)]
|
||||
pub struct FrameData {
|
||||
pub data: Bytes, // 使用bytes crate,支持零拷贝切片
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub format: FrameFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FrameMetadata {
|
||||
pub camera_id: u32,
|
||||
pub exposure_time: f32,
|
||||
pub gain: f32,
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FrameFormat {
|
||||
RGB888,
|
||||
YUV420,
|
||||
JPEG,
|
||||
H264Frame,
|
||||
}
|
||||
|
||||
// 实现示例
|
||||
impl FrameCapturedEvent {
|
||||
pub fn new_zero_copy(
|
||||
frame_id: u64,
|
||||
data: Vec<u8>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Self {
|
||||
let frame_data = Arc::new(FrameData {
|
||||
data: Bytes::from(data), // 转换为Bytes,之后可零拷贝切片
|
||||
width,
|
||||
height,
|
||||
format: FrameFormat::RGB888,
|
||||
});
|
||||
|
||||
Self {
|
||||
frame_id,
|
||||
timestamp: chrono::Utc::now(),
|
||||
metadata: FrameMetadata::default(),
|
||||
frame_data,
|
||||
}
|
||||
}
|
||||
|
||||
// 获取帧数据的只读引用
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.frame_data.data
|
||||
}
|
||||
|
||||
// 创建数据的零拷贝切片
|
||||
pub fn slice(&self, start: usize, end: usize) -> Bytes {
|
||||
self.frame_data.data.slice(start..end)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### B. 优化事件总线
|
||||
|
||||
```rust
|
||||
use tokio::sync::broadcast;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct OptimizedEventBus {
|
||||
// 使用Arc包装的发送器,避免克隆整个通道
|
||||
sender: Arc<broadcast::Sender<Arc<SystemEvent>>>,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl OptimizedEventBus {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
let (sender, _) = broadcast::channel(capacity);
|
||||
Self {
|
||||
sender: Arc::new(sender),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
// 发布事件时使用Arc包装
|
||||
pub fn publish(&self, event: SystemEvent) -> Result<()> {
|
||||
let arc_event = Arc::new(event);
|
||||
self.sender.send(arc_event)
|
||||
.map_err(|_| anyhow::anyhow!("No subscribers"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 订阅者接收Arc包装的事件
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<Arc<SystemEvent>> {
|
||||
self.sender.subscribe()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方案2: 帧池化 (Frame Pooling)
|
||||
|
||||
#### A. 对象池实现
|
||||
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// 帧缓冲池,复用内存分配
|
||||
pub struct FramePool {
|
||||
pool: Arc<Mutex<VecDeque<Vec<u8>>>>,
|
||||
frame_size: usize,
|
||||
max_pool_size: usize,
|
||||
allocated_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl FramePool {
|
||||
pub fn new(width: u32, height: u32, format: FrameFormat, max_pool_size: usize) -> Self {
|
||||
let frame_size = Self::calculate_frame_size(width, height, format);
|
||||
|
||||
Self {
|
||||
pool: Arc::new(Mutex::new(VecDeque::with_capacity(max_pool_size))),
|
||||
frame_size,
|
||||
max_pool_size,
|
||||
allocated_count: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 从池中获取或分配新的帧缓冲
|
||||
pub fn acquire(&self) -> PooledFrame {
|
||||
let mut pool = self.pool.lock().unwrap();
|
||||
|
||||
let buffer = if let Some(mut buf) = pool.pop_front() {
|
||||
// 复用现有缓冲
|
||||
buf.clear();
|
||||
buf.resize(self.frame_size, 0);
|
||||
buf
|
||||
} else {
|
||||
// 分配新缓冲
|
||||
self.allocated_count.fetch_add(1, Ordering::Relaxed);
|
||||
vec![0u8; self.frame_size]
|
||||
};
|
||||
|
||||
PooledFrame {
|
||||
buffer,
|
||||
pool: Arc::clone(&self.pool),
|
||||
frame_size: self.frame_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算帧大小
|
||||
fn calculate_frame_size(width: u32, height: u32, format: FrameFormat) -> usize {
|
||||
match format {
|
||||
FrameFormat::RGB888 => (width * height * 3) as usize,
|
||||
FrameFormat::YUV420 => (width * height * 3 / 2) as usize,
|
||||
FrameFormat::JPEG => (width * height) as usize, // 估算
|
||||
FrameFormat::H264Frame => (width * height / 2) as usize, // 估算
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取池统计信息
|
||||
pub fn stats(&self) -> PoolStats {
|
||||
let pool = self.pool.lock().unwrap();
|
||||
PoolStats {
|
||||
pooled: pool.len(),
|
||||
allocated: self.allocated_count.load(Ordering::Relaxed),
|
||||
frame_size: self.frame_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RAII包装的池化帧,自动归还到池
|
||||
pub struct PooledFrame {
|
||||
buffer: Vec<u8>,
|
||||
pool: Arc<Mutex<VecDeque<Vec<u8>>>>,
|
||||
frame_size: usize,
|
||||
}
|
||||
|
||||
impl PooledFrame {
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||
&mut self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PooledFrame {
|
||||
fn drop(&mut self) {
|
||||
// 归还缓冲到池
|
||||
let mut pool = self.pool.lock().unwrap();
|
||||
if pool.len() < pool.capacity() {
|
||||
let buffer = std::mem::replace(&mut self.buffer, Vec::new());
|
||||
pool.push_back(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PoolStats {
|
||||
pub pooled: usize,
|
||||
pub allocated: usize,
|
||||
pub frame_size: usize,
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Camera模块集成
|
||||
|
||||
```rust
|
||||
// camera.rs 优化版本
|
||||
pub struct OptimizedCameraController {
|
||||
config: CameraConfig,
|
||||
event_bus: EventBus,
|
||||
frame_pool: FramePool,
|
||||
frame_counter: AtomicU64,
|
||||
}
|
||||
|
||||
impl OptimizedCameraController {
|
||||
pub async fn capture_loop(&mut self) -> Result<()> {
|
||||
loop {
|
||||
// 从池中获取帧缓冲
|
||||
let mut pooled_frame = self.frame_pool.acquire();
|
||||
|
||||
// 捕获到池化缓冲中
|
||||
self.capture_to_buffer(pooled_frame.as_mut_slice()).await?;
|
||||
|
||||
// 转换为Arc共享数据
|
||||
let frame_data = Arc::new(FrameData {
|
||||
data: Bytes::from(pooled_frame.as_slice().to_vec()),
|
||||
width: self.config.width.unwrap_or(640),
|
||||
height: self.config.height.unwrap_or(480),
|
||||
format: FrameFormat::RGB888,
|
||||
});
|
||||
|
||||
// 创建事件
|
||||
let event = FrameCapturedEvent {
|
||||
frame_id: self.frame_counter.fetch_add(1, Ordering::Relaxed),
|
||||
timestamp: chrono::Utc::now(),
|
||||
metadata: self.create_metadata(),
|
||||
frame_data,
|
||||
};
|
||||
|
||||
// 发布事件
|
||||
self.event_bus.publish(SystemEvent::FrameCaptured(event))?;
|
||||
|
||||
// pooled_frame 在这里自动Drop,缓冲归还到池
|
||||
|
||||
// 控制帧率
|
||||
tokio::time::sleep(Duration::from_millis(33)).await; // ~30 FPS
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方案3: 环形缓冲区 (Ring Buffer)
|
||||
|
||||
#### A. 内存映射环形缓冲
|
||||
|
||||
```rust
|
||||
use memmap2::{MmapMut, MmapOptions};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// 内存映射的环形缓冲区,用于高效的帧存储
|
||||
pub struct MmapRingBuffer {
|
||||
mmap: Arc<MmapMut>,
|
||||
frame_size: usize,
|
||||
capacity: usize,
|
||||
write_pos: Arc<AtomicUsize>,
|
||||
read_pos: Arc<AtomicUsize>,
|
||||
frame_offsets: Vec<usize>,
|
||||
}
|
||||
|
||||
impl MmapRingBuffer {
|
||||
pub fn new(capacity: usize, frame_size: usize) -> Result<Self> {
|
||||
let total_size = capacity * frame_size;
|
||||
|
||||
// 创建临时文件用于内存映射
|
||||
let temp_file = tempfile::tempfile()?;
|
||||
temp_file.set_len(total_size as u64)?;
|
||||
|
||||
// 创建内存映射
|
||||
let mmap = unsafe {
|
||||
MmapOptions::new()
|
||||
.len(total_size)
|
||||
.map_mut(&temp_file)?
|
||||
};
|
||||
|
||||
// 预计算帧偏移
|
||||
let frame_offsets: Vec<usize> = (0..capacity)
|
||||
.map(|i| i * frame_size)
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
mmap: Arc::new(mmap),
|
||||
frame_size,
|
||||
capacity,
|
||||
write_pos: Arc::new(AtomicUsize::new(0)),
|
||||
read_pos: Arc::new(AtomicUsize::new(0)),
|
||||
frame_offsets,
|
||||
})
|
||||
}
|
||||
|
||||
/// 写入帧到环形缓冲区
|
||||
pub fn write_frame(&self, frame_data: &[u8]) -> Result<usize> {
|
||||
if frame_data.len() != self.frame_size {
|
||||
return Err(anyhow::anyhow!("Frame size mismatch"));
|
||||
}
|
||||
|
||||
let pos = self.write_pos.fetch_add(1, Ordering::AcqRel) % self.capacity;
|
||||
let offset = self.frame_offsets[pos];
|
||||
|
||||
// 直接写入内存映射区域
|
||||
unsafe {
|
||||
let dst = &mut self.mmap[offset..offset + self.frame_size];
|
||||
dst.copy_from_slice(frame_data);
|
||||
}
|
||||
|
||||
Ok(pos)
|
||||
}
|
||||
|
||||
/// 读取帧从环形缓冲区(零拷贝)
|
||||
pub fn read_frame(&self, position: usize) -> &[u8] {
|
||||
let offset = self.frame_offsets[position % self.capacity];
|
||||
&self.mmap[offset..offset + self.frame_size]
|
||||
}
|
||||
|
||||
/// 获取当前写入位置
|
||||
pub fn current_write_pos(&self) -> usize {
|
||||
self.write_pos.load(Ordering::Acquire) % self.capacity
|
||||
}
|
||||
|
||||
/// 获取可用帧数量
|
||||
pub fn available_frames(&self) -> usize {
|
||||
let write = self.write_pos.load(Ordering::Acquire);
|
||||
let read = self.read_pos.load(Ordering::Acquire);
|
||||
write.saturating_sub(read).min(self.capacity)
|
||||
}
|
||||
}
|
||||
|
||||
/// 环形缓冲区的只读视图
|
||||
pub struct RingBufferView {
|
||||
buffer: Arc<MmapRingBuffer>,
|
||||
start_pos: usize,
|
||||
end_pos: usize,
|
||||
}
|
||||
|
||||
impl RingBufferView {
|
||||
pub fn new(buffer: Arc<MmapRingBuffer>, start_pos: usize, end_pos: usize) -> Self {
|
||||
Self {
|
||||
buffer,
|
||||
start_pos,
|
||||
end_pos,
|
||||
}
|
||||
}
|
||||
|
||||
/// 迭代视图中的帧
|
||||
pub fn iter_frames(&self) -> impl Iterator<Item = &[u8]> {
|
||||
(self.start_pos..self.end_pos)
|
||||
.map(move |pos| self.buffer.read_frame(pos))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Detection模块集成
|
||||
|
||||
```rust
|
||||
// detection.rs 优化版本
|
||||
pub struct OptimizedDetectionController {
|
||||
config: DetectionConfig,
|
||||
event_bus: EventBus,
|
||||
ring_buffer: Arc<MmapRingBuffer>,
|
||||
frame_metadata: Arc<RwLock<HashMap<usize, FrameMetadata>>>,
|
||||
}
|
||||
|
||||
impl OptimizedDetectionController {
|
||||
pub async fn detection_loop(&mut self) -> Result<()> {
|
||||
let mut last_processed_pos = 0;
|
||||
|
||||
loop {
|
||||
let current_pos = self.ring_buffer.current_write_pos();
|
||||
|
||||
if current_pos > last_processed_pos {
|
||||
// 创建视图,零拷贝访问帧
|
||||
let view = RingBufferView::new(
|
||||
Arc::clone(&self.ring_buffer),
|
||||
last_processed_pos,
|
||||
current_pos,
|
||||
);
|
||||
|
||||
// 分析帧序列
|
||||
if let Some(detection) = self.analyze_frames(view).await? {
|
||||
// 发布检测事件
|
||||
self.event_bus.publish(SystemEvent::MeteorDetected(detection))?;
|
||||
}
|
||||
|
||||
last_processed_pos = current_pos;
|
||||
}
|
||||
|
||||
// 避免忙等待
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn analyze_frames(&self, view: RingBufferView) -> Result<Option<MeteorDetectedEvent>> {
|
||||
// 使用SIMD优化的亮度计算
|
||||
let brightness_values: Vec<f32> = view.iter_frames()
|
||||
.map(|frame| self.calculate_brightness_simd(frame))
|
||||
.collect();
|
||||
|
||||
// 检测算法...
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
fn calculate_brightness_simd(&self, frame: &[u8]) -> f32 {
|
||||
use std::arch::aarch64::*;
|
||||
|
||||
unsafe {
|
||||
let mut sum = vdupq_n_u32(0);
|
||||
let chunks = frame.chunks_exact(16);
|
||||
|
||||
for chunk in chunks {
|
||||
let data = vld1q_u8(chunk.as_ptr());
|
||||
let data_u16 = vmovl_u8(vget_low_u8(data));
|
||||
let data_u32 = vmovl_u16(vget_low_u16(data_u16));
|
||||
sum = vaddq_u32(sum, data_u32);
|
||||
}
|
||||
|
||||
// 累加SIMD寄存器中的值
|
||||
let sum_array: [u32; 4] = std::mem::transmute(sum);
|
||||
let total: u32 = sum_array.iter().sum();
|
||||
|
||||
total as f32 / frame.len() as f32
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方案4: 分层内存管理
|
||||
|
||||
#### A. 内存层次结构
|
||||
|
||||
```rust
|
||||
/// 分层内存管理器
|
||||
pub struct HierarchicalMemoryManager {
|
||||
// L1: 热数据 - 最近的帧在内存中
|
||||
hot_cache: Arc<RwLock<LruCache<u64, Arc<FrameData>>>>,
|
||||
|
||||
// L2: 温数据 - 使用内存映射文件
|
||||
warm_storage: Arc<MmapRingBuffer>,
|
||||
|
||||
// L3: 冷数据 - 压缩存储在磁盘
|
||||
cold_storage: Arc<ColdStorage>,
|
||||
|
||||
// 统计信息
|
||||
stats: Arc<MemoryStats>,
|
||||
}
|
||||
|
||||
impl HierarchicalMemoryManager {
|
||||
pub fn new(config: MemoryConfig) -> Result<Self> {
|
||||
Ok(Self {
|
||||
hot_cache: Arc::new(RwLock::new(
|
||||
LruCache::new(config.hot_cache_frames)
|
||||
)),
|
||||
warm_storage: Arc::new(MmapRingBuffer::new(
|
||||
config.warm_storage_frames,
|
||||
config.frame_size,
|
||||
)?),
|
||||
cold_storage: Arc::new(ColdStorage::new(config.cold_storage_path)?),
|
||||
stats: Arc::new(MemoryStats::default()),
|
||||
})
|
||||
}
|
||||
|
||||
/// 智能存储帧
|
||||
pub async fn store_frame(&self, frame_id: u64, data: Arc<FrameData>) -> Result<()> {
|
||||
// 更新热缓存
|
||||
{
|
||||
let mut cache = self.hot_cache.write().await;
|
||||
cache.put(frame_id, Arc::clone(&data));
|
||||
}
|
||||
|
||||
// 异步写入温存储
|
||||
let warm_storage = Arc::clone(&self.warm_storage);
|
||||
let data_clone = Arc::clone(&data);
|
||||
tokio::spawn(async move {
|
||||
warm_storage.write_frame(&data_clone.data).ok();
|
||||
});
|
||||
|
||||
// 更新统计
|
||||
self.stats.record_store(data.data.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 智能获取帧
|
||||
pub async fn get_frame(&self, frame_id: u64) -> Result<Arc<FrameData>> {
|
||||
// 检查L1热缓存
|
||||
{
|
||||
let cache = self.hot_cache.read().await;
|
||||
if let Some(data) = cache.peek(&frame_id) {
|
||||
self.stats.record_hit(CacheLevel::L1);
|
||||
return Ok(Arc::clone(data));
|
||||
}
|
||||
}
|
||||
|
||||
// 检查L2温存储
|
||||
if let Some(data) = self.warm_storage.get_frame_by_id(frame_id) {
|
||||
self.stats.record_hit(CacheLevel::L2);
|
||||
let frame_data = Arc::new(FrameData::from_bytes(data));
|
||||
|
||||
// 提升到L1
|
||||
self.promote_to_hot(frame_id, Arc::clone(&frame_data)).await;
|
||||
|
||||
return Ok(frame_data);
|
||||
}
|
||||
|
||||
// 从L3冷存储加载
|
||||
let data = self.cold_storage.load_frame(frame_id).await?;
|
||||
self.stats.record_hit(CacheLevel::L3);
|
||||
|
||||
// 提升到L1和L2
|
||||
self.promote_to_hot(frame_id, Arc::clone(&data)).await;
|
||||
self.promote_to_warm(frame_id, &data).await;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 内存压力管理
|
||||
pub async fn handle_memory_pressure(&self) -> Result<()> {
|
||||
let memory_info = sys_info::mem_info()?;
|
||||
let used_percent = (memory_info.total - memory_info.avail) * 100 / memory_info.total;
|
||||
|
||||
if used_percent > 80 {
|
||||
// 高内存压力,移动数据到下一层
|
||||
self.evict_to_cold().await?;
|
||||
} else if used_percent > 60 {
|
||||
// 中等压力,清理热缓存
|
||||
self.trim_hot_cache().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MemoryStats {
|
||||
l1_hits: AtomicU64,
|
||||
l2_hits: AtomicU64,
|
||||
l3_hits: AtomicU64,
|
||||
total_requests: AtomicU64,
|
||||
bytes_stored: AtomicU64,
|
||||
}
|
||||
|
||||
enum CacheLevel {
|
||||
L1,
|
||||
L2,
|
||||
L3,
|
||||
}
|
||||
```
|
||||
|
||||
### 方案5: 内存监控与调优
|
||||
|
||||
#### A. 实时内存监控
|
||||
|
||||
```rust
|
||||
use prometheus::{Gauge, Histogram, Counter};
|
||||
|
||||
pub struct MemoryMonitor {
|
||||
// Prometheus metrics
|
||||
memory_usage: Gauge,
|
||||
allocation_rate: Counter,
|
||||
gc_pause_time: Histogram,
|
||||
frame_pool_usage: Gauge,
|
||||
|
||||
// 监控任务句柄
|
||||
monitor_handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl MemoryMonitor {
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
let memory_usage = self.memory_usage.clone();
|
||||
let allocation_rate = self.allocation_rate.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
// 更新内存使用率
|
||||
if let Ok(info) = sys_info::mem_info() {
|
||||
let used_mb = (info.total - info.avail) / 1024;
|
||||
memory_usage.set(used_mb as f64);
|
||||
}
|
||||
|
||||
// 监控分配率
|
||||
let allocator_stats = ALLOCATOR.stats();
|
||||
allocation_rate.inc_by(allocator_stats.bytes_allocated);
|
||||
}
|
||||
});
|
||||
|
||||
self.monitor_handle = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 生成内存报告
|
||||
pub fn generate_report(&self) -> MemoryReport {
|
||||
MemoryReport {
|
||||
current_usage_mb: self.memory_usage.get() as usize,
|
||||
allocation_rate_mb_s: self.allocation_rate.get() / 1_000_000.0,
|
||||
frame_pool_efficiency: self.calculate_pool_efficiency(),
|
||||
recommendations: self.generate_recommendations(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### 第一阶段:基础优化(1周)
|
||||
1. ✅ 实现Arc共享帧数据
|
||||
2. ✅ 优化事件总线避免数据拷贝
|
||||
3. ✅ 添加基础内存监控
|
||||
|
||||
### 第二阶段:池化管理(1周)
|
||||
1. ✅ 实现帧对象池
|
||||
2. ✅ 集成到Camera模块
|
||||
3. ✅ 添加池统计和调优
|
||||
|
||||
### 第三阶段:高级优化(2周)
|
||||
1. ✅ 实现内存映射环形缓冲
|
||||
2. ✅ 添加分层内存管理
|
||||
3. ✅ SIMD优化关键路径
|
||||
|
||||
### 第四阶段:监控与调优(1周)
|
||||
1. ✅ 完整的内存监控系统
|
||||
2. ✅ 自动内存压力管理
|
||||
3. ✅ 性能基准测试
|
||||
|
||||
## 预期效果
|
||||
|
||||
### 内存使用降低
|
||||
- 帧数据拷贝:降低 **90%**
|
||||
- 整体内存使用:降低 **60%**
|
||||
- GC压力:降低 **80%**
|
||||
|
||||
### 性能提升
|
||||
- 帧处理延迟:降低 **50%**
|
||||
- CPU使用率:降低 **30%**
|
||||
- 吞吐量:提升 **2-3倍**
|
||||
|
||||
### 系统稳定性
|
||||
- 内存泄漏:完全避免
|
||||
- OOM风险:显著降低
|
||||
- 长期运行:稳定可靠
|
||||
|
||||
## 测试验证
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod memory_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_zero_copy_performance() {
|
||||
let frame_size = 640 * 480 * 3;
|
||||
let iterations = 1000;
|
||||
|
||||
// 测试传统方式
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
let data = vec![0u8; frame_size];
|
||||
let _clone1 = data.clone();
|
||||
let _clone2 = data.clone();
|
||||
}
|
||||
let traditional_time = start.elapsed();
|
||||
|
||||
// 测试零拷贝方式
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
let data = Arc::new(vec![0u8; frame_size]);
|
||||
let _ref1 = Arc::clone(&data);
|
||||
let _ref2 = Arc::clone(&data);
|
||||
}
|
||||
let zero_copy_time = start.elapsed();
|
||||
|
||||
println!("Traditional: {:?}, Zero-copy: {:?}",
|
||||
traditional_time, zero_copy_time);
|
||||
assert!(zero_copy_time < traditional_time / 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_pool_efficiency() {
|
||||
let pool = FramePool::new(640, 480, FrameFormat::RGB888, 10);
|
||||
|
||||
// 测试复用
|
||||
let frame1 = pool.acquire();
|
||||
let addr1 = frame1.as_ptr();
|
||||
drop(frame1);
|
||||
|
||||
let frame2 = pool.acquire();
|
||||
let addr2 = frame2.as_ptr();
|
||||
|
||||
// 验证地址相同(复用成功)
|
||||
assert_eq!(addr1, addr2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个内存优化方案将显著提升边缘设备的性能和稳定性,特别适合资源受限的树莓派环境。
|
||||
@ -1,6 +1,6 @@
|
||||
# Meteor Edge Client
|
||||
|
||||
An autonomous meteor detection system for edge devices (Raspberry Pi) with event-driven architecture, real-time video processing, and cloud integration.
|
||||
An autonomous meteor detection system for edge devices (Raspberry Pi) with event-driven architecture, advanced memory management, real-time video processing, and cloud integration.
|
||||
|
||||
## Overview
|
||||
|
||||
@ -13,9 +13,22 @@ The Meteor Edge Client is a sophisticated edge computing application that serves
|
||||
- **Real-time Processing**: Frame-by-frame video analysis with configurable detection algorithms
|
||||
- **Asynchronous Operations**: Non-blocking event handling for optimal performance
|
||||
|
||||
### Advanced Memory Management
|
||||
- **Zero-Copy Architecture**: Arc-based frame sharing eliminates unnecessary memory copies
|
||||
- **Hierarchical Frame Pools**: Multi-size buffer pools (64KB, 256KB, 900KB, 2MB) with adaptive management
|
||||
- **Lock-Free Ring Buffers**: High-throughput astronomical frame streaming (>3M frames/sec)
|
||||
- **Memory-Mapped I/O**: Efficient access to large astronomical datasets
|
||||
- **Adaptive Pool Management**: Dynamic resizing based on memory pressure (70%/80%/90% thresholds)
|
||||
|
||||
### Camera Simulation System
|
||||
- **Multiple Input Sources**: Physical cameras, video files, test patterns, V4L2 loopback (Linux)
|
||||
- **FFmpeg Integration**: Support for all major video formats (MP4, AVI, MKV, MOV)
|
||||
- **Test Pattern Generator**: Synthetic meteor events for deterministic testing
|
||||
- **CI/CD Friendly**: Headless operation for automated testing
|
||||
|
||||
### Key Capabilities
|
||||
- **Autonomous Operation**: Runs continuously without human intervention
|
||||
- **Meteor Detection**: Real-time video analysis to identify meteor events
|
||||
- **Meteor Detection**: Real-time video analysis with multiple detection algorithms
|
||||
- **Event Recording**: Automatic video capture and archiving of detected events
|
||||
- **Cloud Synchronization**: Secure upload of events to backend API
|
||||
- **Device Registration**: JWT-based device registration and authentication
|
||||
@ -149,11 +162,30 @@ timeout_seconds = 30
|
||||
|
||||
# Camera Configuration
|
||||
[camera]
|
||||
source = "device" # "device" or file path
|
||||
source_type = "device" # "device", "file", "test_pattern", "v4l2_loopback"
|
||||
device_index = 0
|
||||
fps = 30.0
|
||||
width = 640
|
||||
height = 480
|
||||
width = 1280
|
||||
height = 720
|
||||
|
||||
# Camera simulation (for testing)
|
||||
[camera.test_pattern]
|
||||
type = "simulated_meteor" # static, noise, bar, meteor, checkerboard, gradient
|
||||
duration_seconds = 300
|
||||
|
||||
[camera.file]
|
||||
path = "test_data/meteor_capture.mp4"
|
||||
loop = true
|
||||
playback_speed = 1.0
|
||||
|
||||
[camera.v4l2_loopback] # Linux only
|
||||
device = "/dev/video10"
|
||||
input_file = "test_data/meteor_capture.mp4"
|
||||
|
||||
[camera.performance]
|
||||
enable_memory_optimization = true
|
||||
max_camera_memory_mb = 64
|
||||
buffer_pool_size = 20
|
||||
|
||||
# Detection Configuration
|
||||
[detection]
|
||||
@ -186,40 +218,67 @@ upload_enabled = true
|
||||
|
||||
## Usage
|
||||
|
||||
### Commands
|
||||
### Basic Commands
|
||||
|
||||
#### 1. Run Autonomous Detection System
|
||||
```bash
|
||||
# Start the main application (requires device registration)
|
||||
# Production mode (requires device registration)
|
||||
./meteor-edge-client run
|
||||
```
|
||||
This launches the event-driven meteor detection system that will:
|
||||
- Initialize camera and start capturing frames
|
||||
- Run detection algorithms continuously
|
||||
- Archive detected events
|
||||
- Upload events to cloud backend
|
||||
|
||||
#### 2. Register Device
|
||||
# Test mode with camera simulation
|
||||
cargo run --features camera_sim -- run --camera-source "test:meteor"
|
||||
|
||||
# Run with video file
|
||||
cargo run --features camera_sim -- run --camera-source "file:test_data/meteor_video.mp4"
|
||||
|
||||
# Run with specific test pattern
|
||||
cargo run --features camera_sim -- run --camera-source "test:checkerboard"
|
||||
```
|
||||
|
||||
#### 2. Camera Simulation Testing
|
||||
```bash
|
||||
# Available test patterns
|
||||
--camera-source "test:static" # Fixed pattern
|
||||
--camera-source "test:noise" # Random noise
|
||||
--camera-source "test:bar" # Moving bar
|
||||
--camera-source "test:meteor" # Meteor simulation with random events
|
||||
--camera-source "test:checkerboard" # Checkerboard pattern
|
||||
--camera-source "test:gradient" # Gradient pattern
|
||||
|
||||
# Video file sources
|
||||
--camera-source "file:video.mp4" # Relative path
|
||||
--camera-source "file:/path/to/video.mp4" # Absolute path
|
||||
|
||||
# V4L2 loopback (Linux only)
|
||||
--camera-source "v4l2:/dev/video10"
|
||||
```
|
||||
|
||||
#### 3. Memory Management Tests
|
||||
```bash
|
||||
# Advanced memory management testing commands
|
||||
./target/release/meteor-edge-client test # Core frame pools
|
||||
./target/release/meteor-edge-client test-adaptive # Adaptive management
|
||||
./target/release/meteor-edge-client test-integration # Integration tests
|
||||
./target/release/meteor-edge-client test-ring-buffer # Ring buffers & memory mapping
|
||||
./target/release/meteor-edge-client test-hierarchical-cache # Cache system
|
||||
./target/release/meteor-edge-client monitor # Production monitoring
|
||||
```
|
||||
|
||||
#### 4. Device Registration
|
||||
```bash
|
||||
# Register device with user account using JWT token
|
||||
./meteor-edge-client register <JWT_TOKEN> [--api-url <URL>]
|
||||
```
|
||||
One-time setup to link the device to a user account.
|
||||
|
||||
#### 3. Check Device Status
|
||||
#### 5. Device Status & Health
|
||||
```bash
|
||||
# Show hardware ID, registration status, and configuration
|
||||
./meteor-edge-client status
|
||||
```
|
||||
|
||||
#### 4. Health Check
|
||||
```bash
|
||||
# Verify backend connectivity
|
||||
./meteor-edge-client health [--api-url <URL>]
|
||||
```
|
||||
|
||||
#### 5. Version Information
|
||||
```bash
|
||||
# Version information
|
||||
./meteor-edge-client version
|
||||
```
|
||||
|
||||
@ -262,6 +321,48 @@ event_<timestamp>_<event_id>/
|
||||
└── logs/ # Related log entries
|
||||
```
|
||||
|
||||
## Advanced Memory Management
|
||||
|
||||
The meteor edge client features a sophisticated 5-phase memory optimization system designed for high-performance astronomical data processing on resource-constrained devices.
|
||||
|
||||
### Key Features
|
||||
|
||||
#### Zero-Copy Architecture (Phase 1)
|
||||
- Arc-based frame sharing eliminates unnecessary memory copies
|
||||
- RAII pattern ensures automatic resource cleanup
|
||||
- Event-driven processing with efficient memory propagation
|
||||
|
||||
#### Hierarchical Frame Pools (Phase 2)
|
||||
- Multiple pool sizes: 64KB, 256KB, 900KB, 2MB buffers
|
||||
- Adaptive capacity management based on memory pressure (70%/80%/90% thresholds)
|
||||
- Historical metrics tracking for intelligent resizing
|
||||
- Cross-platform memory pressure detection
|
||||
|
||||
#### Advanced Streaming & Caching (Phase 3)
|
||||
- Lock-free ring buffers using atomic operations (>3M frames/sec throughput)
|
||||
- Memory-mapped I/O for large astronomical datasets
|
||||
- Multi-level cache architecture (L1/L2/L3) with different eviction policies
|
||||
- Intelligent prefetching based on access patterns
|
||||
|
||||
#### Production Monitoring (Phase 4)
|
||||
- Real-time health check monitoring with component-level status
|
||||
- Performance profiling with latency histograms (P50, P95, P99)
|
||||
- Alert management with configurable thresholds
|
||||
- Comprehensive diagnostics including system resource tracking
|
||||
|
||||
#### End-to-End Integration (Phase 5)
|
||||
- Unified memory system across all components
|
||||
- Camera memory integration with zero-copy capture pipeline
|
||||
- Real-time meteor detection with sub-30ms processing latency
|
||||
- Multi-configuration support (Raspberry Pi and high-performance servers)
|
||||
|
||||
### Performance Benchmarks
|
||||
- **Frame Pool Operations**: >100K allocations/sec with zero memory leaks
|
||||
- **Ring Buffer Throughput**: 3.6M+ writes/sec, 7.2M+ reads/sec
|
||||
- **Cache Performance**: >50K lookups/sec with 80%+ hit rates
|
||||
- **Memory Efficiency**: <2x growth under sustained load
|
||||
- **Memory Savings**: Multi-GB savings through zero-copy architecture
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
@ -269,23 +370,32 @@ event_<timestamp>_<event_id>/
|
||||
# Unit tests
|
||||
cargo test
|
||||
|
||||
# Camera simulation tests
|
||||
cargo test --features camera_sim
|
||||
|
||||
# Integration test
|
||||
./demo_integration_test.sh
|
||||
|
||||
# With debug output
|
||||
cargo test -- --nocapture
|
||||
|
||||
# Comprehensive camera simulation test suite
|
||||
scripts/test_camera_simulation.sh all
|
||||
```
|
||||
|
||||
### Module Structure
|
||||
- `src/main.rs` - CLI entry point and command handling
|
||||
- `src/app.rs` - Application coordinator
|
||||
- `src/events.rs` - Event bus and event types
|
||||
- `src/camera.rs` - Camera control and frame capture
|
||||
- `src/camera/` - Camera control and frame capture with simulation support
|
||||
- `src/camera_sim/` - Camera simulation backends (test patterns, file reader, FFmpeg, V4L2)
|
||||
- `src/detection.rs` - Detection algorithms
|
||||
- `src/storage.rs` - Event storage and archiving
|
||||
- `src/communication.rs` - Cloud API client
|
||||
- `src/config.rs` - Configuration management
|
||||
- `src/hardware.rs` - Hardware ID extraction
|
||||
- `src/frame_pool.rs` - Hierarchical frame pool management
|
||||
- `src/memory_monitor.rs` - Memory monitoring and optimization
|
||||
- `src/hardware_fingerprint.rs` - Hardware ID extraction
|
||||
- `src/logging.rs` - Structured logging
|
||||
- `src/api.rs` - HTTP client utilities
|
||||
|
||||
@ -355,6 +465,60 @@ RUST_LOG=debug ./meteor-edge-client run
|
||||
tail -f /var/log/meteor/meteor-edge-client.log
|
||||
```
|
||||
|
||||
## Camera Simulation for Development & Testing
|
||||
|
||||
The edge client includes a comprehensive camera simulation system that enables development and testing without physical camera hardware.
|
||||
|
||||
### Quick Start with Simulation
|
||||
```bash
|
||||
# Build with camera simulation support
|
||||
cargo build --features camera_sim
|
||||
|
||||
# Run with meteor simulation
|
||||
cargo run --features camera_sim -- run --camera-source "test:meteor"
|
||||
|
||||
# Run with video file
|
||||
cargo run --features camera_sim -- run --camera-source "file:test_data/meteor_video.mp4"
|
||||
```
|
||||
|
||||
### Available Simulation Backends
|
||||
|
||||
1. **Test Pattern Generator** - Synthetic patterns for deterministic testing
|
||||
- Static, noise, moving bar, checkerboard, gradient
|
||||
- Simulated meteor events with realistic brightness variations
|
||||
|
||||
2. **File Reader** - Read video files (MP4, AVI, MKV, MOV)
|
||||
- Loop playback support
|
||||
- Variable playback speed
|
||||
- Resolution and frame rate detection
|
||||
|
||||
3. **V4L2 Loopback** (Linux only) - Virtual V4L2 devices
|
||||
- Integration with external streaming tools
|
||||
- Multi-camera simulation support
|
||||
|
||||
4. **FFmpeg Integration** (optional) - Enhanced video processing
|
||||
- Hardware acceleration support
|
||||
- Broader format compatibility
|
||||
- Advanced video processing capabilities
|
||||
|
||||
### Detailed Documentation
|
||||
|
||||
For comprehensive camera simulation documentation, see:
|
||||
- [CAMERA_SIMULATION_USAGE.md](CAMERA_SIMULATION_USAGE.md) - Complete usage guide with examples and troubleshooting
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
The camera simulation system is designed for headless operation in CI/CD pipelines:
|
||||
|
||||
```bash
|
||||
# Install dependencies (Ubuntu/Debian)
|
||||
sudo apt-get install -y ffmpeg libavcodec-dev libavformat-dev
|
||||
|
||||
# Run automated tests
|
||||
cargo test --features camera_sim
|
||||
scripts/test_camera_simulation.sh all
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- GPS integration for location tagging
|
||||
@ -366,6 +530,12 @@ tail -f /var/log/meteor/meteor-edge-client.log
|
||||
- Edge-to-edge communication
|
||||
- Offline operation mode with sync
|
||||
|
||||
## Documentation
|
||||
|
||||
- [README.md](README.md) - This file (main documentation)
|
||||
- [CAMERA_SIMULATION_USAGE.md](CAMERA_SIMULATION_USAGE.md) - Camera simulation guide
|
||||
- [prd.md](prd.md) - Product requirements document (Chinese)
|
||||
|
||||
## License
|
||||
|
||||
[Specify your license here]
|
||||
|
||||
132
meteor-edge-client/REFACTORING.md
Normal file
132
meteor-edge-client/REFACTORING.md
Normal file
@ -0,0 +1,132 @@
|
||||
# Edge Client Source Code Reorganization
|
||||
|
||||
## Summary
|
||||
|
||||
Successfully reorganized the meteor edge client source code from a flat structure with 35+ files in `src/` into a well-organized modular structure with 7 functional domains.
|
||||
|
||||
## New Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # Entry point
|
||||
├── test_fingerprint*.rs # Standalone bin targets
|
||||
│
|
||||
├── core/ # Core application (4 files)
|
||||
│ ├── app.rs # Application coordinator
|
||||
│ ├── config.rs # Configuration management
|
||||
│ ├── events.rs # Event bus and event types
|
||||
│ └── logging.rs # Logging utilities
|
||||
│
|
||||
├── camera/ # Camera module (5 files)
|
||||
│ ├── factory.rs # Camera factory
|
||||
│ ├── interface.rs # Camera interface
|
||||
│ ├── production.rs # Production camera
|
||||
│ └── video_file.rs # Video file camera
|
||||
│
|
||||
├── memory/ # Memory management (9 files + tests)
|
||||
│ ├── frame_data.rs # Frame data structures
|
||||
│ ├── frame_pool.rs # Basic frame pooling
|
||||
│ ├── adaptive_pool_manager.rs # Adaptive pool management
|
||||
│ ├── memory_monitor.rs # Memory monitoring
|
||||
│ ├── memory_pressure.rs # Memory pressure detection
|
||||
│ ├── memory_mapping.rs # Memory-mapped I/O
|
||||
│ ├── ring_buffer.rs # Lock-free ring buffers
|
||||
│ ├── hierarchical_cache.rs # Multi-level caching
|
||||
│ └── tests/ # Memory tests (6 files)
|
||||
│ ├── zero_copy_tests.rs
|
||||
│ ├── frame_pool_tests.rs
|
||||
│ ├── adaptive_pool_tests.rs
|
||||
│ ├── pool_integration_tests.rs
|
||||
│ ├── ring_buffer_tests.rs
|
||||
│ └── hierarchical_cache_tests.rs
|
||||
│
|
||||
├── detection/ # Detection module (3 files)
|
||||
│ ├── detector.rs # Detection controller
|
||||
│ ├── meteor_pipeline.rs # Meteor detection pipeline
|
||||
│ └── camera_integration.rs # Camera memory integration
|
||||
│
|
||||
├── storage/ # Storage module (1 file)
|
||||
│ └── storage.rs # Storage manager
|
||||
│
|
||||
├── network/ # Network communication (4 files)
|
||||
│ ├── api.rs # API client
|
||||
│ ├── communication.rs # Communication manager
|
||||
│ ├── websocket_client.rs # WebSocket client
|
||||
│ └── log_uploader.rs # Log uploader (disabled)
|
||||
│
|
||||
├── device/ # Device management (2 files)
|
||||
│ ├── registration.rs # Device registration
|
||||
│ └── hardware_fingerprint.rs # Hardware ID detection
|
||||
│
|
||||
├── monitoring/ # Monitoring module (2 files)
|
||||
│ ├── production_monitor.rs # Production monitoring
|
||||
│ └── integrated_system.rs # Integrated system monitoring
|
||||
│
|
||||
└── tests/ # Integration tests (1 file)
|
||||
└── integration_test.rs
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Clear Module Boundaries**: Each directory represents a distinct functional domain
|
||||
2. **Better Organization**: Related code is grouped together
|
||||
3. **Easier Navigation**: Developers can quickly locate relevant code
|
||||
4. **Scalability**: Easy to add new features within appropriate modules
|
||||
5. **Maintainability**: Reduced cognitive load when working on specific features
|
||||
6. **Test Organization**: Tests are co-located with the code they test
|
||||
|
||||
## Changes Made
|
||||
|
||||
### File Movements
|
||||
- **Core modules**: app, config, events, logging → `core/`
|
||||
- **Memory management**: 8 modules + 6 test files → `memory/` and `memory/tests/`
|
||||
- **Detection**: detection.rs → `detection/detector.rs`, plus 2 related files
|
||||
- **Network**: api, communication, websocket, log_uploader → `network/`
|
||||
- **Device**: registration, hardware_fingerprint → `device/`
|
||||
- **Monitoring**: production_monitor, integrated_system → `monitoring/`
|
||||
- **Storage**: storage.rs → `storage/`
|
||||
- **Tests**: integration_test.rs → `tests/`
|
||||
|
||||
### Import Path Updates
|
||||
All `use crate::` imports were systematically updated to reflect new module paths:
|
||||
- `crate::api::` → `crate::network::api::`
|
||||
- `crate::frame_pool::` → `crate::memory::frame_pool::`
|
||||
- `crate::detection::` → `crate::detection::detector::`
|
||||
- etc.
|
||||
|
||||
### Module Exports
|
||||
Created `mod.rs` files for each new module with appropriate re-exports to maintain clean public APIs.
|
||||
|
||||
## Known Issues
|
||||
|
||||
The following pre-existing code issues were identified but not fixed (unrelated to reorganization):
|
||||
|
||||
1. **Detection/Monitoring modules**: Have some logic errors (9 compile errors)
|
||||
- Type mismatches in `integrated_system.rs`
|
||||
- Missing method implementations
|
||||
- Borrowed value issues
|
||||
|
||||
2. **Log Uploader**: Temporarily disabled due to missing `LogFileManager` and `StructuredLogger` types
|
||||
|
||||
These issues existed before the reorganization and should be addressed separately.
|
||||
|
||||
## Compilation Status
|
||||
|
||||
- ✅ **test-fingerprint** binary: Compiles successfully
|
||||
- ⚠️ **Main binary**: Has 9 pre-existing logic errors (not from reorganization)
|
||||
- ✅ **Module structure**: All imports and paths correctly updated
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Fix the 9 logic errors in detection/monitoring modules
|
||||
2. Implement missing logging infrastructure (LogFileManager, StructuredLogger)
|
||||
3. Re-enable log_uploader module
|
||||
4. Run full test suite to verify functionality
|
||||
|
||||
## Statistics
|
||||
|
||||
- **Before**: 35+ files in flat `src/` directory
|
||||
- **After**: 41 files organized into 11 directories
|
||||
- **Modules created**: 8 new module directories with `mod.rs` files
|
||||
- **Files moved**: 35 files
|
||||
- **Import paths updated**: ~100+ import statements
|
||||
44
meteor-edge-client/meteor-client-config.toml
Normal file
44
meteor-edge-client/meteor-client-config.toml
Normal file
@ -0,0 +1,44 @@
|
||||
[device]
|
||||
registered = true
|
||||
hardware_id = "SIM_DEVICE"
|
||||
device_id = "sim-device-001"
|
||||
user_profile_id = "user-sim"
|
||||
registered_at = "2025-01-01T00:00:00Z"
|
||||
jwt_token = "dummy-jwt-token"
|
||||
|
||||
[api]
|
||||
base_url = "http://localhost:3000"
|
||||
upload_endpoint = "/api/v1/events"
|
||||
timeout_seconds = 30
|
||||
|
||||
[camera]
|
||||
# Accepts unified camera specs like "device:0", "sim:pattern:meteor", or "sim:file:./video.mp4"
|
||||
source = "device:0"
|
||||
device_index = 0
|
||||
fps = 30.0
|
||||
width = 640
|
||||
height = 480
|
||||
|
||||
[detection]
|
||||
algorithm = "brightness_diff"
|
||||
threshold = 0.3
|
||||
buffer_frames = 150
|
||||
|
||||
[storage]
|
||||
base_path = "./meteor_events"
|
||||
max_storage_gb = 10.0
|
||||
retention_days = 7
|
||||
pre_event_seconds = 2
|
||||
post_event_seconds = 2
|
||||
|
||||
[communication]
|
||||
heartbeat_interval_seconds = 300
|
||||
upload_batch_size = 1
|
||||
retry_attempts = 3
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
directory = "./meteor_logs"
|
||||
max_file_size_mb = 10
|
||||
max_files = 5
|
||||
upload_enabled = false
|
||||
267
meteor-edge-client/scripts/setup_virtual_camera.sh
Executable file
267
meteor-edge-client/scripts/setup_virtual_camera.sh
Executable file
@ -0,0 +1,267 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Setup script for V4L2 loopback virtual camera
|
||||
# Used for testing camera simulation on Linux systems
|
||||
|
||||
set -e
|
||||
|
||||
echo "==============================================="
|
||||
echo "V4L2 Loopback Virtual Camera Setup"
|
||||
echo "==============================================="
|
||||
|
||||
# Check if running on Linux
|
||||
if [[ "$OSTYPE" != "linux-gnu"* ]]; then
|
||||
echo "Error: This script only works on Linux systems"
|
||||
echo "For macOS, consider using OBS Virtual Camera or similar"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for root privileges
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Install v4l2loopback if not present
|
||||
install_v4l2loopback() {
|
||||
echo "Installing v4l2loopback..."
|
||||
|
||||
if command_exists apt-get; then
|
||||
# Debian/Ubuntu
|
||||
apt-get update
|
||||
apt-get install -y v4l2loopback-dkms v4l2loopback-utils v4l-utils ffmpeg
|
||||
elif command_exists yum; then
|
||||
# RHEL/CentOS/Fedora
|
||||
yum install -y kmod-v4l2loopback v4l-utils ffmpeg
|
||||
elif command_exists pacman; then
|
||||
# Arch Linux
|
||||
pacman -S --noconfirm v4l2loopback-dkms v4l-utils ffmpeg
|
||||
else
|
||||
echo "Error: Unsupported package manager"
|
||||
echo "Please install v4l2loopback manually"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if v4l2loopback module is available
|
||||
if ! modinfo v4l2loopback >/dev/null 2>&1; then
|
||||
echo "v4l2loopback not found. Installing..."
|
||||
install_v4l2loopback
|
||||
fi
|
||||
|
||||
# Check if FFmpeg is installed
|
||||
if ! command_exists ffmpeg; then
|
||||
echo "FFmpeg not found. Installing..."
|
||||
if command_exists apt-get; then
|
||||
apt-get install -y ffmpeg
|
||||
elif command_exists yum; then
|
||||
yum install -y ffmpeg
|
||||
elif command_exists pacman; then
|
||||
pacman -S --noconfirm ffmpeg
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove existing v4l2loopback module if loaded
|
||||
if lsmod | grep -q v4l2loopback; then
|
||||
echo "Removing existing v4l2loopback module..."
|
||||
modprobe -r v4l2loopback
|
||||
fi
|
||||
|
||||
# Load v4l2loopback module with specific parameters
|
||||
echo "Loading v4l2loopback module..."
|
||||
modprobe v4l2loopback \
|
||||
devices=2 \
|
||||
video_nr=10,11 \
|
||||
card_label="Meteor Cam 1","Meteor Cam 2" \
|
||||
exclusive_caps=1
|
||||
|
||||
# Verify devices were created
|
||||
echo ""
|
||||
echo "Checking virtual devices..."
|
||||
if [[ -e /dev/video10 ]] && [[ -e /dev/video11 ]]; then
|
||||
echo "✅ Virtual cameras created successfully:"
|
||||
echo " - /dev/video10 (Meteor Cam 1)"
|
||||
echo " - /dev/video11 (Meteor Cam 2)"
|
||||
else
|
||||
echo "❌ Failed to create virtual cameras"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set permissions for non-root access
|
||||
chmod 666 /dev/video10 /dev/video11
|
||||
|
||||
# List all video devices
|
||||
echo ""
|
||||
echo "Available video devices:"
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# Show device capabilities
|
||||
echo ""
|
||||
echo "Virtual camera capabilities:"
|
||||
v4l2-ctl -d /dev/video10 --all | head -20
|
||||
|
||||
# Create test video if it doesn't exist
|
||||
TEST_VIDEO_DIR="/tmp/meteor_test_videos"
|
||||
TEST_VIDEO="$TEST_VIDEO_DIR/meteor_test.mp4"
|
||||
|
||||
if [[ ! -f "$TEST_VIDEO" ]]; then
|
||||
echo ""
|
||||
echo "Creating test video..."
|
||||
mkdir -p "$TEST_VIDEO_DIR"
|
||||
|
||||
# Generate a test video with synthetic meteor events
|
||||
ffmpeg -f lavfi -i testsrc2=size=1280x720:rate=30 \
|
||||
-f lavfi -i sine=frequency=1000:duration=10 \
|
||||
-t 10 \
|
||||
-vf "drawtext=text='Meteor Test Video':fontsize=30:fontcolor=white:x=(w-text_w)/2:y=50" \
|
||||
-c:v libx264 -preset fast \
|
||||
-y "$TEST_VIDEO" 2>/dev/null
|
||||
|
||||
echo "✅ Test video created: $TEST_VIDEO"
|
||||
fi
|
||||
|
||||
# Function to stream video to virtual camera
|
||||
stream_to_camera() {
|
||||
local video_file=$1
|
||||
local device=$2
|
||||
local label=$3
|
||||
|
||||
echo ""
|
||||
echo "Starting stream to $device ($label)..."
|
||||
echo "Command: ffmpeg -re -i \"$video_file\" -f v4l2 -pix_fmt yuv420p \"$device\""
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop the stream"
|
||||
|
||||
# Stream video to virtual camera (loop infinitely)
|
||||
ffmpeg -re -stream_loop -1 -i "$video_file" \
|
||||
-f v4l2 \
|
||||
-pix_fmt yuv420p \
|
||||
-vf "scale=1280:720,drawtext=text='$label - %{localtime}':fontsize=20:fontcolor=white:x=10:y=10" \
|
||||
"$device" 2>/dev/null &
|
||||
|
||||
local pid=$!
|
||||
echo "Stream PID: $pid"
|
||||
|
||||
# Save PID for cleanup
|
||||
echo $pid > "/tmp/meteor_cam_stream_$(basename $device).pid"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to stop all streams
|
||||
stop_streams() {
|
||||
echo ""
|
||||
echo "Stopping all streams..."
|
||||
|
||||
for pidfile in /tmp/meteor_cam_stream_*.pid; do
|
||||
if [[ -f "$pidfile" ]]; then
|
||||
pid=$(cat "$pidfile")
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill "$pid"
|
||||
echo "Stopped stream PID: $pid"
|
||||
fi
|
||||
rm "$pidfile"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
ACTION=${1:-help}
|
||||
|
||||
case "$ACTION" in
|
||||
start)
|
||||
VIDEO_FILE=${2:-$TEST_VIDEO}
|
||||
DEVICE=${3:-/dev/video10}
|
||||
|
||||
if [[ ! -f "$VIDEO_FILE" ]]; then
|
||||
echo "Error: Video file not found: $VIDEO_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stream_to_camera "$VIDEO_FILE" "$DEVICE" "Meteor Cam"
|
||||
;;
|
||||
|
||||
start-all)
|
||||
VIDEO_FILE=${2:-$TEST_VIDEO}
|
||||
|
||||
if [[ ! -f "$VIDEO_FILE" ]]; then
|
||||
echo "Error: Video file not found: $VIDEO_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stream_to_camera "$VIDEO_FILE" "/dev/video10" "Meteor Cam 1"
|
||||
stream_to_camera "$VIDEO_FILE" "/dev/video11" "Meteor Cam 2"
|
||||
|
||||
echo ""
|
||||
echo "Both virtual cameras are now streaming"
|
||||
echo "You can test them with:"
|
||||
echo " cargo run -- --camera-source v4l2:/dev/video10"
|
||||
echo " cargo run -- --camera-source v4l2:/dev/video11"
|
||||
;;
|
||||
|
||||
stop)
|
||||
stop_streams
|
||||
;;
|
||||
|
||||
status)
|
||||
echo "Virtual camera status:"
|
||||
echo ""
|
||||
|
||||
if lsmod | grep -q v4l2loopback; then
|
||||
echo "✅ v4l2loopback module loaded"
|
||||
else
|
||||
echo "❌ v4l2loopback module not loaded"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Active streams:"
|
||||
for pidfile in /tmp/meteor_cam_stream_*.pid; do
|
||||
if [[ -f "$pidfile" ]]; then
|
||||
pid=$(cat "$pidfile")
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
device=$(basename "$pidfile" .pid | sed 's/meteor_cam_stream_//')
|
||||
echo " - Stream to /dev/$device (PID: $pid)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
;;
|
||||
|
||||
unload)
|
||||
stop_streams
|
||||
echo "Unloading v4l2loopback module..."
|
||||
modprobe -r v4l2loopback
|
||||
echo "✅ Module unloaded"
|
||||
;;
|
||||
|
||||
help|*)
|
||||
echo ""
|
||||
echo "Usage: $0 [command] [options]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start [video_file] [device] - Start streaming to a virtual camera"
|
||||
echo " Default: $TEST_VIDEO to /dev/video10"
|
||||
echo " start-all [video_file] - Start streaming to all virtual cameras"
|
||||
echo " stop - Stop all active streams"
|
||||
echo " status - Show virtual camera status"
|
||||
echo " unload - Stop streams and unload module"
|
||||
echo " help - Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 start # Start with test video"
|
||||
echo " $0 start meteor.mp4 # Use custom video"
|
||||
echo " $0 start meteor.mp4 /dev/video11 # Specific device"
|
||||
echo " $0 start-all meteor.mp4 # Stream to all devices"
|
||||
echo " $0 stop # Stop all streams"
|
||||
echo ""
|
||||
echo "Test with meteor edge client:"
|
||||
echo " cargo run -- --camera-source v4l2:/dev/video10"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "==============================================="
|
||||
296
meteor-edge-client/scripts/test_camera_simulation.sh
Executable file
296
meteor-edge-client/scripts/test_camera_simulation.sh
Executable file
@ -0,0 +1,296 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Comprehensive testing script for camera simulation
|
||||
# Tests all camera source backends and configurations
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
TEST_DATA_DIR="$PROJECT_DIR/test_data"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test results
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
local status=$1
|
||||
local message=$2
|
||||
|
||||
case $status in
|
||||
"SUCCESS")
|
||||
echo -e "${GREEN}✅ $message${NC}"
|
||||
((TESTS_PASSED++))
|
||||
;;
|
||||
"FAILURE")
|
||||
echo -e "${RED}❌ $message${NC}"
|
||||
((TESTS_FAILED++))
|
||||
;;
|
||||
"INFO")
|
||||
echo -e "${YELLOW}ℹ️ $message${NC}"
|
||||
;;
|
||||
*)
|
||||
echo "$message"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to run a test
|
||||
run_test() {
|
||||
local test_name=$1
|
||||
local command=$2
|
||||
local timeout_duration=${3:-10}
|
||||
|
||||
echo ""
|
||||
echo "Running test: $test_name"
|
||||
echo "Command: $command"
|
||||
|
||||
if timeout ${timeout_duration}s bash -c "$command" > /tmp/test_output.log 2>&1; then
|
||||
print_status "SUCCESS" "$test_name passed"
|
||||
return 0
|
||||
else
|
||||
print_status "FAILURE" "$test_name failed"
|
||||
echo "Output:"
|
||||
tail -20 /tmp/test_output.log
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create test data directory
|
||||
create_test_data() {
|
||||
print_status "INFO" "Creating test data directory..."
|
||||
mkdir -p "$TEST_DATA_DIR"
|
||||
|
||||
# Check if ffmpeg is available
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
print_status "FAILURE" "FFmpeg not found. Please install FFmpeg."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create various test videos
|
||||
print_status "INFO" "Generating test videos..."
|
||||
|
||||
# 1. Small test video (480p, 5 seconds)
|
||||
if [ ! -f "$TEST_DATA_DIR/test_480p.mp4" ]; then
|
||||
ffmpeg -f lavfi -i testsrc2=size=640x480:rate=30 -t 5 \
|
||||
-vf "drawtext=text='480p Test':fontsize=20:fontcolor=white:x=10:y=10" \
|
||||
-c:v libx264 -preset ultrafast \
|
||||
-y "$TEST_DATA_DIR/test_480p.mp4" 2>/dev/null
|
||||
print_status "SUCCESS" "Created test_480p.mp4"
|
||||
fi
|
||||
|
||||
# 2. Medium test video (720p, 10 seconds)
|
||||
if [ ! -f "$TEST_DATA_DIR/test_720p.mp4" ]; then
|
||||
ffmpeg -f lavfi -i testsrc=size=1280x720:rate=30 -t 10 \
|
||||
-vf "drawtext=text='720p Test':fontsize=24:fontcolor=white:x=10:y=10" \
|
||||
-c:v libx264 -preset ultrafast \
|
||||
-y "$TEST_DATA_DIR/test_720p.mp4" 2>/dev/null
|
||||
print_status "SUCCESS" "Created test_720p.mp4"
|
||||
fi
|
||||
|
||||
# 3. High resolution test (1080p, 5 seconds)
|
||||
if [ ! -f "$TEST_DATA_DIR/test_1080p.mp4" ]; then
|
||||
ffmpeg -f lavfi -i testsrc2=size=1920x1080:rate=30 -t 5 \
|
||||
-vf "drawtext=text='1080p Test':fontsize=30:fontcolor=white:x=10:y=10" \
|
||||
-c:v libx264 -preset ultrafast \
|
||||
-y "$TEST_DATA_DIR/test_1080p.mp4" 2>/dev/null
|
||||
print_status "SUCCESS" "Created test_1080p.mp4"
|
||||
fi
|
||||
|
||||
# 4. Meteor simulation video (with brightness changes)
|
||||
if [ ! -f "$TEST_DATA_DIR/meteor_simulation.mp4" ]; then
|
||||
ffmpeg -f lavfi -i "nullsrc=s=1280x720:r=30" -t 10 \
|
||||
-vf "geq=r='if(between(t,2,2.1)+between(t,5,5.2)+between(t,8,8.1),255,50)':g='if(between(t,2,2.1)+between(t,5,5.2)+between(t,8,8.1),255,50)':b='if(between(t,2,2.1)+between(t,5,5.2)+between(t,8,8.1),255,50)',drawtext=text='Meteor Simulation':fontsize=24:fontcolor=white:x=(w-text_w)/2:y=50" \
|
||||
-c:v libx264 -preset ultrafast \
|
||||
-y "$TEST_DATA_DIR/meteor_simulation.mp4" 2>/dev/null
|
||||
print_status "SUCCESS" "Created meteor_simulation.mp4"
|
||||
fi
|
||||
|
||||
ls -lh "$TEST_DATA_DIR"/*.mp4 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Build the project
|
||||
build_project() {
|
||||
print_status "INFO" "Building meteor-edge-client..."
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
if cargo build --features camera_sim --release; then
|
||||
print_status "SUCCESS" "Build completed successfully"
|
||||
else
|
||||
print_status "FAILURE" "Build failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 1: Test pattern sources
|
||||
test_patterns() {
|
||||
print_status "INFO" "Testing pattern generators..."
|
||||
|
||||
local patterns=("static" "noise" "bar" "meteor" "checkerboard" "gradient")
|
||||
|
||||
for pattern in "${patterns[@]}"; do
|
||||
run_test "Pattern: $pattern" \
|
||||
"cd $PROJECT_DIR && cargo run --features camera_sim -- test --camera-source test:$pattern --frames 100" \
|
||||
5
|
||||
done
|
||||
}
|
||||
|
||||
# Test 2: File reader
|
||||
test_file_reader() {
|
||||
print_status "INFO" "Testing file reader..."
|
||||
|
||||
local test_files=("test_480p.mp4" "test_720p.mp4" "test_1080p.mp4" "meteor_simulation.mp4")
|
||||
|
||||
for file in "${test_files[@]}"; do
|
||||
if [ -f "$TEST_DATA_DIR/$file" ]; then
|
||||
run_test "File: $file" \
|
||||
"cd $PROJECT_DIR && cargo run --features camera_sim -- test --camera-source file:test_data/$file --frames 50" \
|
||||
10
|
||||
else
|
||||
print_status "FAILURE" "Test file not found: $file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Test 3: Performance benchmarks
|
||||
test_performance() {
|
||||
print_status "INFO" "Running performance benchmarks..."
|
||||
|
||||
# Test pattern performance
|
||||
run_test "Pattern benchmark (1000 frames)" \
|
||||
"cd $PROJECT_DIR && cargo run --release --features camera_sim -- benchmark --camera-source test:meteor --frames 1000" \
|
||||
30
|
||||
|
||||
# File reader performance
|
||||
if [ -f "$TEST_DATA_DIR/test_720p.mp4" ]; then
|
||||
run_test "File reader benchmark" \
|
||||
"cd $PROJECT_DIR && cargo run --release --features camera_sim -- benchmark --camera-source file:test_data/test_720p.mp4 --frames 300" \
|
||||
30
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 4: Memory leak check
|
||||
test_memory_leaks() {
|
||||
print_status "INFO" "Testing for memory leaks..."
|
||||
|
||||
if command -v valgrind &> /dev/null; then
|
||||
run_test "Memory leak check" \
|
||||
"cd $PROJECT_DIR && valgrind --leak-check=full --error-exitcode=1 ./target/release/meteor-edge-client test --camera-source test:static --frames 100" \
|
||||
60
|
||||
else
|
||||
print_status "INFO" "Valgrind not available, skipping memory leak test"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 5: Concurrent sources
|
||||
test_concurrent() {
|
||||
print_status "INFO" "Testing concurrent camera sources..."
|
||||
|
||||
# Start multiple instances in background
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
./target/release/meteor-edge-client test --camera-source test:meteor --frames 100 &
|
||||
PID1=$!
|
||||
|
||||
./target/release/meteor-edge-client test --camera-source test:noise --frames 100 &
|
||||
PID2=$!
|
||||
|
||||
./target/release/meteor-edge-client test --camera-source test:bar --frames 100 &
|
||||
PID3=$!
|
||||
|
||||
# Wait for all to complete
|
||||
if wait $PID1 && wait $PID2 && wait $PID3; then
|
||||
print_status "SUCCESS" "Concurrent sources test passed"
|
||||
else
|
||||
print_status "FAILURE" "Concurrent sources test failed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 6: Error handling
|
||||
test_error_handling() {
|
||||
print_status "INFO" "Testing error handling..."
|
||||
|
||||
# Test with non-existent file
|
||||
run_test "Non-existent file handling" \
|
||||
"cd $PROJECT_DIR && ! cargo run --features camera_sim -- test --camera-source file:nonexistent.mp4 --frames 10" \
|
||||
5
|
||||
|
||||
# Test with invalid pattern
|
||||
run_test "Invalid pattern handling" \
|
||||
"cd $PROJECT_DIR && ! cargo run --features camera_sim -- test --camera-source test:invalid_pattern --frames 10" \
|
||||
5
|
||||
}
|
||||
|
||||
# Main test execution
|
||||
main() {
|
||||
echo "================================================"
|
||||
echo "Camera Simulation Test Suite"
|
||||
echo "================================================"
|
||||
|
||||
# Parse arguments
|
||||
TEST_SUITE=${1:-all}
|
||||
|
||||
# Create test data
|
||||
create_test_data
|
||||
|
||||
# Build project
|
||||
build_project
|
||||
|
||||
# Run tests based on selection
|
||||
case $TEST_SUITE in
|
||||
patterns)
|
||||
test_patterns
|
||||
;;
|
||||
files)
|
||||
test_file_reader
|
||||
;;
|
||||
performance)
|
||||
test_performance
|
||||
;;
|
||||
memory)
|
||||
test_memory_leaks
|
||||
;;
|
||||
concurrent)
|
||||
test_concurrent
|
||||
;;
|
||||
errors)
|
||||
test_error_handling
|
||||
;;
|
||||
all)
|
||||
test_patterns
|
||||
test_file_reader
|
||||
test_performance
|
||||
test_memory_leaks
|
||||
test_concurrent
|
||||
test_error_handling
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [all|patterns|files|performance|memory|concurrent|errors]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "Test Summary"
|
||||
echo "================================================"
|
||||
print_status "SUCCESS" "Tests passed: $TESTS_PASSED"
|
||||
|
||||
if [ $TESTS_FAILED -gt 0 ]; then
|
||||
print_status "FAILURE" "Tests failed: $TESTS_FAILED"
|
||||
exit 1
|
||||
else
|
||||
print_status "SUCCESS" "All tests passed!"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@ -1,289 +0,0 @@
|
||||
use anyhow::{Result, Context};
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::events::{EventBus, FrameCapturedEvent};
|
||||
use crate::frame_data::{FrameData, FrameFormat};
|
||||
use crate::frame_pool::HierarchicalFramePool;
|
||||
|
||||
/// Configuration for camera input source
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CameraSource {
|
||||
/// Use system camera device (0 for default camera)
|
||||
Device(i32),
|
||||
/// Use video file as input source
|
||||
File(String),
|
||||
}
|
||||
|
||||
/// Configuration for camera controller
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CameraConfig {
|
||||
pub source: CameraSource,
|
||||
pub fps: f64,
|
||||
pub width: Option<i32>,
|
||||
pub height: Option<i32>,
|
||||
}
|
||||
|
||||
impl Default for CameraConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
source: CameraSource::Device(0),
|
||||
fps: 30.0,
|
||||
width: Some(640),
|
||||
height: Some(480),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Camera controller that captures video frames and publishes events
|
||||
pub struct CameraController {
|
||||
config: CameraConfig,
|
||||
event_bus: EventBus,
|
||||
frame_counter: u64,
|
||||
frame_pool: Arc<HierarchicalFramePool>,
|
||||
}
|
||||
|
||||
impl CameraController {
|
||||
/// Create a new camera controller
|
||||
pub fn new(config: CameraConfig, event_bus: EventBus) -> Self {
|
||||
// Create hierarchical frame pool for different frame sizes
|
||||
let frame_pool = Arc::new(HierarchicalFramePool::new(20)); // 20 buffers per pool
|
||||
|
||||
Self {
|
||||
config,
|
||||
event_bus,
|
||||
frame_counter: 0,
|
||||
frame_pool,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create camera controller with custom frame pool
|
||||
pub fn with_frame_pool(config: CameraConfig, event_bus: EventBus, frame_pool: Arc<HierarchicalFramePool>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
event_bus,
|
||||
frame_counter: 0,
|
||||
frame_pool,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the camera capture loop (simulated)
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
println!("🎥 Starting simulated camera controller...");
|
||||
println!(" Source: {:?}", self.config.source);
|
||||
println!(" Target FPS: {}", self.config.fps);
|
||||
|
||||
let width = self.config.width.unwrap_or(640) as u32;
|
||||
let height = self.config.height.unwrap_or(480) as u32;
|
||||
|
||||
println!(" Resolution: {}x{}", width, height);
|
||||
println!("✅ Simulated camera controller initialized, starting capture loop...");
|
||||
|
||||
let frame_duration = Duration::from_secs_f64(1.0 / self.config.fps);
|
||||
let max_frames = match &self.config.source {
|
||||
CameraSource::File(_) => Some(300), // Simulate a 10-second video at 30 FPS
|
||||
CameraSource::Device(_) => None, // Continuous capture
|
||||
};
|
||||
|
||||
loop {
|
||||
let start_time = tokio::time::Instant::now();
|
||||
|
||||
// Generate simulated frame
|
||||
match self.generate_simulated_frame(width, height).await {
|
||||
Ok(_) => {
|
||||
self.frame_counter += 1;
|
||||
if self.frame_counter % 30 == 0 {
|
||||
println!("📸 Generated {} frames", self.frame_counter);
|
||||
}
|
||||
|
||||
// Check if we've reached the end of simulated video file
|
||||
if let Some(max) = max_frames {
|
||||
if self.frame_counter >= max {
|
||||
println!("📽️ Reached end of simulated video file, stopping...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error generating frame {}: {}", self.frame_counter, e);
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Maintain target frame rate
|
||||
let elapsed = start_time.elapsed();
|
||||
if elapsed < frame_duration {
|
||||
sleep(frame_duration - elapsed).await;
|
||||
}
|
||||
}
|
||||
|
||||
println!("🎬 Simulated camera controller stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a simulated frame and publish event using frame pool and zero-copy architecture
|
||||
async fn generate_simulated_frame(&self, width: u32, height: u32) -> Result<()> {
|
||||
// Estimate required buffer size
|
||||
let estimated_size = self.estimate_frame_size(width, height);
|
||||
|
||||
// Acquire buffer from frame pool (zero allocation in steady state)
|
||||
let mut pooled_buffer = self.frame_pool.acquire(estimated_size);
|
||||
|
||||
// Generate synthetic frame data directly into pooled buffer
|
||||
self.fill_synthetic_jpeg(&mut pooled_buffer, width, height, self.frame_counter);
|
||||
|
||||
// Convert pooled buffer to frozen bytes for zero-copy sharing
|
||||
let frame_bytes = pooled_buffer.freeze(); // Buffer automatically returns to pool on drop
|
||||
|
||||
// Create shared frame data
|
||||
let shared_frame = Arc::new(FrameData {
|
||||
data: frame_bytes,
|
||||
width,
|
||||
height,
|
||||
format: FrameFormat::JPEG,
|
||||
timestamp: chrono::Utc::now(),
|
||||
});
|
||||
|
||||
// Create frame captured event with shared data
|
||||
let event = FrameCapturedEvent::new(
|
||||
self.frame_counter + 1, // frame_id is 1-based
|
||||
shared_frame,
|
||||
);
|
||||
|
||||
self.event_bus.publish_frame_captured(event)
|
||||
.context("Failed to publish frame captured event")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Estimate frame size for buffer allocation
|
||||
fn estimate_frame_size(&self, width: u32, height: u32) -> usize {
|
||||
// Estimate compressed JPEG size: header + compressed data + footer
|
||||
let header_size = 4;
|
||||
let footer_size = 2;
|
||||
let compressed_data_size = (width * height * 3 / 8) as usize; // Rough JPEG compression ratio
|
||||
|
||||
header_size + compressed_data_size + footer_size
|
||||
}
|
||||
|
||||
/// Fill pooled buffer with synthetic JPEG data (zero-copy generation)
|
||||
fn fill_synthetic_jpeg(&self, buffer: &mut crate::frame_pool::PooledFrameBuffer, width: u32, height: u32, frame_number: u64) {
|
||||
use bytes::BufMut;
|
||||
|
||||
buffer.clear(); // Clear any existing data
|
||||
|
||||
// Fake JPEG header (simplified)
|
||||
buffer.as_mut().put_slice(&[0xFF, 0xD8, 0xFF, 0xE0]); // SOI + APP0
|
||||
|
||||
// Generate synthetic image data based on frame number
|
||||
let pattern_size = (width * height * 3 / 8) as usize; // Simulate compressed size
|
||||
|
||||
// Create periodic brightness spikes to simulate meteors
|
||||
let base_brightness = 128u8;
|
||||
let brightness_multiplier = if frame_number % 200 == 100 || frame_number % 200 == 101 {
|
||||
// Simulate meteor event every 200 frames (2 frames duration at 30 FPS)
|
||||
2.5 // Significant brightness increase
|
||||
} else if frame_number % 150 == 75 {
|
||||
// Another smaller event
|
||||
1.8
|
||||
} else {
|
||||
1.0 // Normal brightness
|
||||
};
|
||||
|
||||
let adjusted_brightness = (base_brightness as f64 * brightness_multiplier) as u8;
|
||||
|
||||
// Reserve capacity to avoid repeated allocations
|
||||
let current_len = buffer.as_ref().len();
|
||||
let current_capacity = buffer.as_ref().capacity();
|
||||
if current_capacity < pattern_size + 10 {
|
||||
buffer.as_mut().reserve(pattern_size + 10 - current_len);
|
||||
}
|
||||
|
||||
for i in 0..pattern_size {
|
||||
let pixel_value = adjusted_brightness.wrapping_add((i % 32) as u8);
|
||||
buffer.as_mut().put_u8(pixel_value);
|
||||
}
|
||||
|
||||
// Fake JPEG footer
|
||||
buffer.as_mut().put_slice(&[0xFF, 0xD9]); // EOI
|
||||
}
|
||||
|
||||
/// Create a synthetic JPEG-like frame for simulation (legacy method for tests)
|
||||
fn create_synthetic_jpeg(&self, width: u32, height: u32, frame_number: u64) -> Vec<u8> {
|
||||
// Create a simple pattern that changes with frame number
|
||||
let mut data = Vec::new();
|
||||
|
||||
// Fake JPEG header (simplified)
|
||||
data.extend_from_slice(&[0xFF, 0xD8, 0xFF, 0xE0]); // SOI + APP0
|
||||
|
||||
// Generate synthetic image data based on frame number
|
||||
let pattern_size = (width * height * 3 / 8) as usize; // Simulate compressed size
|
||||
|
||||
// Create periodic brightness spikes to simulate meteors
|
||||
let base_brightness = 128u8;
|
||||
let brightness_multiplier = if frame_number % 200 == 100 || frame_number % 200 == 101 {
|
||||
// Simulate meteor event every 200 frames (2 frames duration at 30 FPS)
|
||||
2.5 // Significant brightness increase
|
||||
} else if frame_number % 150 == 75 {
|
||||
// Another smaller event
|
||||
1.8
|
||||
} else {
|
||||
1.0 // Normal brightness
|
||||
};
|
||||
|
||||
let adjusted_brightness = (base_brightness as f64 * brightness_multiplier) as u8;
|
||||
|
||||
for i in 0..pattern_size {
|
||||
let pixel_value = adjusted_brightness.wrapping_add((i % 32) as u8);
|
||||
data.push(pixel_value);
|
||||
}
|
||||
|
||||
// Fake JPEG footer
|
||||
data.extend_from_slice(&[0xFF, 0xD9]); // EOI
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
/// Get current frame count
|
||||
pub fn frame_count(&self) -> u64 {
|
||||
self.frame_counter
|
||||
}
|
||||
|
||||
/// Get frame pool statistics for monitoring
|
||||
pub fn frame_pool_stats(&self) -> Vec<(usize, crate::frame_pool::FramePoolStats)> {
|
||||
self.frame_pool.all_stats()
|
||||
}
|
||||
|
||||
/// Get total memory usage of frame pools
|
||||
pub fn frame_pool_memory_usage(&self) -> usize {
|
||||
self.frame_pool.total_memory_usage()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::events::EventBus;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_camera_controller_creation() {
|
||||
let config = CameraConfig::default();
|
||||
let event_bus = EventBus::new(100);
|
||||
let controller = CameraController::new(config, event_bus);
|
||||
|
||||
assert_eq!(controller.frame_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_camera_config_default() {
|
||||
let config = CameraConfig::default();
|
||||
|
||||
assert!(matches!(config.source, CameraSource::Device(0)));
|
||||
assert_eq!(config.fps, 30.0);
|
||||
assert_eq!(config.width, Some(640));
|
||||
assert_eq!(config.height, Some(480));
|
||||
}
|
||||
}
|
||||
196
meteor-edge-client/src/camera/factory.rs
Normal file
196
meteor-edge-client/src/camera/factory.rs
Normal file
@ -0,0 +1,196 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::interface::{CameraConfig, CameraInterface, CameraType};
|
||||
use super::production::ProductionCamera;
|
||||
use super::video_file::VideoFileCamera;
|
||||
use crate::memory::frame_pool::HierarchicalFramePool;
|
||||
|
||||
/// Factory for creating camera instances from configuration or specs
|
||||
pub struct CameraFactory {
|
||||
frame_pool: Arc<HierarchicalFramePool>,
|
||||
}
|
||||
|
||||
impl CameraFactory {
|
||||
pub fn new(frame_pool: Arc<HierarchicalFramePool>) -> Self {
|
||||
Self { frame_pool }
|
||||
}
|
||||
|
||||
pub fn create_camera(&self, config: CameraConfig) -> Result<Box<dyn CameraInterface>> {
|
||||
match config.camera_type {
|
||||
CameraType::Production { device_id, backend } => {
|
||||
println!("🏭 Opening hardware camera: {} ({})", device_id, backend);
|
||||
let camera = ProductionCamera::new(
|
||||
device_id,
|
||||
backend,
|
||||
config.resolution,
|
||||
config.fps,
|
||||
self.frame_pool.clone(),
|
||||
)
|
||||
.context("Failed to create hardware camera")?;
|
||||
Ok(Box::new(camera))
|
||||
}
|
||||
CameraType::VideoFile {
|
||||
path,
|
||||
loop_playback,
|
||||
playback_speed,
|
||||
} => {
|
||||
println!("🎞️ Opening video file camera: {}", path.display());
|
||||
let camera = VideoFileCamera::new(
|
||||
path,
|
||||
config.resolution,
|
||||
config.fps,
|
||||
loop_playback,
|
||||
playback_speed,
|
||||
)
|
||||
.context("Failed to create video file camera")?;
|
||||
Ok(Box::new(camera))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_from_spec(&self, spec: &str) -> Result<Box<dyn CameraInterface>> {
|
||||
let config = self.parse_camera_spec(spec)?;
|
||||
self.create_camera(config)
|
||||
}
|
||||
|
||||
pub fn config_from_spec(&self, spec: &str) -> Result<CameraConfig> {
|
||||
self.parse_camera_spec(spec)
|
||||
}
|
||||
|
||||
fn parse_camera_spec(&self, spec: &str) -> Result<CameraConfig> {
|
||||
let trimmed = spec.trim();
|
||||
if trimmed.starts_with("device:") || trimmed.starts_with("hw:") {
|
||||
let device_id = if let Some(rest) = trimmed.strip_prefix("device:") {
|
||||
rest
|
||||
} else {
|
||||
trimmed.strip_prefix("hw:").unwrap_or("0")
|
||||
};
|
||||
|
||||
return Ok(CameraConfig {
|
||||
camera_type: CameraType::Production {
|
||||
device_id: device_id.to_string(),
|
||||
backend: "default".to_string(),
|
||||
},
|
||||
resolution: (1280, 720),
|
||||
fps: 30.0,
|
||||
settings: HashMap::new(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(rest) = trimmed.strip_prefix("file:") {
|
||||
return Ok(self.build_file_config(rest));
|
||||
}
|
||||
|
||||
if let Some(rest) = trimmed.strip_prefix("video:") {
|
||||
return Ok(self.build_file_config(rest));
|
||||
}
|
||||
|
||||
if let Some(rest) = trimmed.strip_prefix("sim:file:") {
|
||||
// Backward compatibility with older CLI/docs
|
||||
return Ok(self.build_file_config(rest));
|
||||
}
|
||||
|
||||
// Bare path? treat as file
|
||||
if Self::looks_like_path(trimmed) {
|
||||
return Ok(self.build_file_config(trimmed));
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"Invalid camera specification '{}'. Use 'device:N' or 'file:/path/to/video.mp4'",
|
||||
spec
|
||||
);
|
||||
}
|
||||
|
||||
fn build_file_config(&self, path_str: &str) -> CameraConfig {
|
||||
let path = PathBuf::from(path_str.trim());
|
||||
CameraConfig {
|
||||
camera_type: CameraType::VideoFile {
|
||||
path,
|
||||
loop_playback: true,
|
||||
playback_speed: 1.0,
|
||||
},
|
||||
resolution: (1280, 720),
|
||||
fps: 30.0,
|
||||
settings: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn looks_like_path(spec: &str) -> bool {
|
||||
spec.contains('/')
|
||||
|| spec.contains('\\')
|
||||
|| spec.ends_with(".mp4")
|
||||
|| spec.ends_with(".mov")
|
||||
|| spec.ends_with(".mkv")
|
||||
|| spec.ends_with(".avi")
|
||||
}
|
||||
|
||||
pub fn get_available_specs() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
("device:0", "Hardware camera device 0"),
|
||||
("device:/dev/video0", "Hardware camera at /dev/video0"),
|
||||
(
|
||||
"file:meteor-edge-client/video.mp4",
|
||||
"Replay frames from local video file",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn validate_spec(&self, spec: &str) -> Result<()> {
|
||||
self.config_from_spec(spec).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_available_cameras() {
|
||||
println!("Available camera sources:");
|
||||
for (spec, description) in CameraFactory::get_available_specs() {
|
||||
println!(" {:<40} - {}", spec, description);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn factory() -> CameraFactory {
|
||||
let pool = Arc::new(HierarchicalFramePool::new(10));
|
||||
CameraFactory::new(pool)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_device_specs() {
|
||||
let factory = factory();
|
||||
for spec in ["device:0", "device:/dev/video1", "hw:2"] {
|
||||
let config = factory.config_from_spec(spec).expect("device spec");
|
||||
match config.camera_type {
|
||||
CameraType::Production { .. } => {}
|
||||
_ => panic!("expected production camera"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_file_specs() {
|
||||
let factory = factory();
|
||||
for spec in [
|
||||
"file:/tmp/video.mp4",
|
||||
"video:/tmp/raw.mov",
|
||||
"sim:file:legacy.mkv",
|
||||
"./relative/path.avi",
|
||||
] {
|
||||
let config = factory.config_from_spec(spec).expect("file spec");
|
||||
match config.camera_type {
|
||||
CameraType::VideoFile { .. } => {}
|
||||
_ => panic!("expected video file camera"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_specs_fail() {
|
||||
let factory = factory();
|
||||
assert!(factory.config_from_spec("unknown").is_err());
|
||||
}
|
||||
}
|
||||
185
meteor-edge-client/src/camera/interface.rs
Normal file
185
meteor-edge-client/src/camera/interface.rs
Normal file
@ -0,0 +1,185 @@
|
||||
/// Core camera interface trait for dependency inversion
|
||||
/// This trait provides a clean abstraction that isolates production code from capture-source concerns
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::memory::frame_data::{FrameData, FrameFormat};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Core camera interface that both hardware and video file cameras implement
|
||||
pub trait CameraInterface: Send + Sync {
|
||||
/// Initialize the camera hardware or video source
|
||||
fn initialize(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>;
|
||||
|
||||
/// Capture a single frame from the camera
|
||||
fn capture_frame(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<CapturedFrame>> + Send + '_>>;
|
||||
|
||||
/// Get camera metadata and capabilities
|
||||
fn get_metadata(&self) -> CameraMetadata;
|
||||
|
||||
/// Check if the camera is currently active and ready
|
||||
fn is_running(&self) -> bool;
|
||||
|
||||
/// Get current frame count
|
||||
fn frame_count(&self) -> u64;
|
||||
|
||||
/// Gracefully shutdown the camera
|
||||
fn shutdown(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>>;
|
||||
}
|
||||
|
||||
/// Represents a captured frame with metadata
|
||||
#[derive(Clone)]
|
||||
pub struct CapturedFrame {
|
||||
/// Frame data (shared for zero-copy)
|
||||
pub data: Arc<FrameData>,
|
||||
/// Sequential frame number
|
||||
pub frame_number: u64,
|
||||
/// When the frame was captured
|
||||
pub capture_timestamp: DateTime<Utc>,
|
||||
/// Camera-specific metadata
|
||||
pub metadata: FrameMetadata,
|
||||
}
|
||||
|
||||
/// Camera metadata describing capabilities and configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CameraMetadata {
|
||||
/// Camera identifier
|
||||
pub camera_id: String,
|
||||
/// Camera type (e.g. hardware backend or video file)
|
||||
pub camera_type: String,
|
||||
/// Supported frame formats
|
||||
pub supported_formats: Vec<FrameFormat>,
|
||||
/// Maximum resolution
|
||||
pub max_resolution: (u32, u32),
|
||||
/// Current resolution
|
||||
pub current_resolution: (u32, u32),
|
||||
/// Target frames per second
|
||||
pub target_fps: f64,
|
||||
/// Whether the camera supports real-time capture
|
||||
pub is_real_time: bool,
|
||||
/// Total frames available (None for continuous streams)
|
||||
pub total_frames: Option<u64>,
|
||||
}
|
||||
|
||||
/// Per-frame metadata
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FrameMetadata {
|
||||
/// Exposure settings (if applicable)
|
||||
pub exposure_ms: Option<f64>,
|
||||
/// Gain settings (if applicable)
|
||||
pub gain: Option<f64>,
|
||||
/// Temperature readings (if available)
|
||||
pub temperature_celsius: Option<f64>,
|
||||
/// Any camera-specific properties
|
||||
pub properties: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Camera configuration that works for both hardware and video sources
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CameraConfig {
|
||||
/// Type of camera to create
|
||||
pub camera_type: CameraType,
|
||||
/// Target resolution
|
||||
pub resolution: (u32, u32),
|
||||
/// Target frame rate
|
||||
pub fps: f64,
|
||||
/// Additional camera-specific settings
|
||||
pub settings: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Enum defining camera types (device or video file)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CameraType {
|
||||
/// Production hardware camera
|
||||
Production {
|
||||
/// Device index or identifier
|
||||
device_id: String,
|
||||
/// Hardware-specific backend
|
||||
backend: String,
|
||||
},
|
||||
/// Video file camera configuration
|
||||
VideoFile {
|
||||
/// Absolute video file path
|
||||
path: PathBuf,
|
||||
/// Whether to loop playback when reaching the end
|
||||
loop_playback: bool,
|
||||
/// Playback speed multiplier (1.0 = realtime)
|
||||
playback_speed: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for CameraConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
camera_type: CameraType::Production {
|
||||
device_id: "0".to_string(),
|
||||
backend: "default".to_string(),
|
||||
},
|
||||
resolution: (1280, 720),
|
||||
fps: 30.0,
|
||||
settings: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CapturedFrame {
|
||||
/// Create a new captured frame
|
||||
pub fn new(
|
||||
data: Arc<FrameData>,
|
||||
frame_number: u64,
|
||||
capture_timestamp: DateTime<Utc>,
|
||||
metadata: FrameMetadata,
|
||||
) -> Self {
|
||||
Self {
|
||||
data,
|
||||
frame_number,
|
||||
capture_timestamp,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get frame dimensions
|
||||
pub fn dimensions(&self) -> (u32, u32) {
|
||||
(self.data.width, self.data.height)
|
||||
}
|
||||
|
||||
/// Get frame data size in bytes
|
||||
pub fn data_size(&self) -> usize {
|
||||
self.data.data.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FrameMetadata {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
exposure_ms: None,
|
||||
gain: None,
|
||||
temperature_celsius: None,
|
||||
properties: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CameraMetadata {
|
||||
/// Create minimal camera metadata
|
||||
pub fn new(camera_id: String, camera_type: String) -> Self {
|
||||
Self {
|
||||
camera_id,
|
||||
camera_type,
|
||||
supported_formats: vec![FrameFormat::JPEG],
|
||||
max_resolution: (1920, 1080),
|
||||
current_resolution: (1280, 720),
|
||||
target_fps: 30.0,
|
||||
is_real_time: true,
|
||||
total_frames: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
240
meteor-edge-client/src/camera/mod.rs
Normal file
240
meteor-edge-client/src/camera/mod.rs
Normal file
@ -0,0 +1,240 @@
|
||||
pub mod factory;
|
||||
/// Camera module with clean separation between production hardware and video file capture
|
||||
/// This module provides a unified interface while keeping implementation details isolated
|
||||
// Core interfaces and types (always available)
|
||||
pub mod interface;
|
||||
pub mod production;
|
||||
mod video_file;
|
||||
|
||||
// Re-export core types for convenience
|
||||
pub use factory::{print_available_cameras, CameraFactory};
|
||||
pub use interface::{
|
||||
CameraConfig, CameraInterface, CameraMetadata, CameraType, CapturedFrame, FrameMetadata,
|
||||
};
|
||||
pub use production::{ProductionCamera, ProductionCameraCapabilities};
|
||||
pub use video_file::VideoFileCamera;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::core::events::{EventBus, FrameCapturedEvent};
|
||||
use crate::memory::frame_pool::HierarchicalFramePool;
|
||||
|
||||
/// High-level camera controller that manages camera instances
|
||||
/// This replaces the old CameraController with a cleaner design
|
||||
pub struct CameraController {
|
||||
camera: Box<dyn CameraInterface>,
|
||||
event_bus: EventBus,
|
||||
frame_pool: Arc<HierarchicalFramePool>,
|
||||
is_running: bool,
|
||||
}
|
||||
|
||||
impl CameraController {
|
||||
/// Create a new camera controller with the specified configuration
|
||||
pub fn new(config: CameraConfig, event_bus: EventBus) -> Result<Self> {
|
||||
let frame_pool = Arc::new(HierarchicalFramePool::new(20));
|
||||
let factory = CameraFactory::new(frame_pool.clone());
|
||||
let camera = factory.create_camera(config)?;
|
||||
|
||||
Ok(Self {
|
||||
camera,
|
||||
event_bus,
|
||||
frame_pool,
|
||||
is_running: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a camera controller with a custom frame pool
|
||||
pub fn with_frame_pool(
|
||||
config: CameraConfig,
|
||||
event_bus: EventBus,
|
||||
frame_pool: Arc<HierarchicalFramePool>,
|
||||
) -> Result<Self> {
|
||||
let factory = CameraFactory::new(frame_pool.clone());
|
||||
let camera = factory.create_camera(config)?;
|
||||
|
||||
Ok(Self {
|
||||
camera,
|
||||
event_bus,
|
||||
frame_pool,
|
||||
is_running: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a camera controller from a string specification
|
||||
pub fn from_spec(spec: &str, event_bus: EventBus) -> Result<Self> {
|
||||
let frame_pool = Arc::new(HierarchicalFramePool::new(20));
|
||||
let factory = CameraFactory::new(frame_pool.clone());
|
||||
let camera = factory.create_from_spec(spec)?;
|
||||
|
||||
Ok(Self {
|
||||
camera,
|
||||
event_bus,
|
||||
frame_pool,
|
||||
is_running: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Start the camera capture loop
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
println!("🎥 Starting camera controller...");
|
||||
|
||||
// Initialize the camera
|
||||
self.camera.initialize().await?;
|
||||
self.is_running = true;
|
||||
|
||||
let metadata = self.camera.get_metadata();
|
||||
println!(
|
||||
" Camera: {} ({})",
|
||||
metadata.camera_id, metadata.camera_type
|
||||
);
|
||||
println!(
|
||||
" Resolution: {}x{} @ {:.1} FPS",
|
||||
metadata.current_resolution.0, metadata.current_resolution.1, metadata.target_fps
|
||||
);
|
||||
|
||||
if let Some(total_frames) = metadata.total_frames {
|
||||
println!(" Total frames: {}", total_frames);
|
||||
}
|
||||
|
||||
println!("✅ Camera controller initialized, starting capture loop...");
|
||||
|
||||
// Calculate frame timing
|
||||
let frame_duration = Duration::from_secs_f64(1.0 / metadata.target_fps);
|
||||
|
||||
// Main capture loop
|
||||
while self.is_running && self.camera.is_running() {
|
||||
let start_time = tokio::time::Instant::now();
|
||||
|
||||
match self.camera.capture_frame().await {
|
||||
Ok(captured_frame) => {
|
||||
// Create event from captured frame
|
||||
let event = FrameCapturedEvent::new(
|
||||
captured_frame.frame_number,
|
||||
captured_frame.data.clone(),
|
||||
);
|
||||
|
||||
// Publish frame event
|
||||
if let Err(e) = self.event_bus.publish_frame_captured(event) {
|
||||
eprintln!("❌ Failed to publish frame event: {}", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Log progress periodically
|
||||
if captured_frame.frame_number % 30 == 0 {
|
||||
println!("📸 Captured {} frames", captured_frame.frame_number);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"❌ Error capturing frame {}: {}",
|
||||
self.camera.frame_count(),
|
||||
e
|
||||
);
|
||||
|
||||
// If this is a production camera and capture fails, we should probably exit
|
||||
if metadata.camera_type != "VideoFile" {
|
||||
eprintln!("❌ Hardware camera failure, stopping...");
|
||||
break;
|
||||
}
|
||||
|
||||
// For video files, wait a bit and continue
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Maintain target frame rate
|
||||
let elapsed = start_time.elapsed();
|
||||
if elapsed < frame_duration {
|
||||
sleep(frame_duration - elapsed).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown camera
|
||||
self.camera.shutdown().await?;
|
||||
self.is_running = false;
|
||||
|
||||
println!("🎬 Camera controller stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the camera controller
|
||||
pub async fn stop(&mut self) -> Result<()> {
|
||||
if self.is_running {
|
||||
println!("🛑 Stopping camera controller...");
|
||||
self.is_running = false;
|
||||
self.camera.shutdown().await?;
|
||||
println!("✅ Camera controller stopped");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current frame count
|
||||
pub fn frame_count(&self) -> u64 {
|
||||
self.camera.frame_count()
|
||||
}
|
||||
|
||||
/// Get camera metadata
|
||||
pub fn metadata(&self) -> CameraMetadata {
|
||||
self.camera.get_metadata()
|
||||
}
|
||||
|
||||
/// Check if the controller is running
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.is_running && self.camera.is_running()
|
||||
}
|
||||
|
||||
/// Get frame pool statistics for monitoring
|
||||
pub fn frame_pool_stats(&self) -> Vec<(usize, crate::memory::frame_pool::FramePoolStats)> {
|
||||
self.frame_pool.all_stats()
|
||||
}
|
||||
|
||||
/// Get total memory usage of frame pools
|
||||
pub fn frame_pool_memory_usage(&self) -> usize {
|
||||
self.frame_pool.total_memory_usage()
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy compatibility - deprecated, use CameraController instead
|
||||
#[deprecated(note = "Use CameraController with CameraConfig instead")]
|
||||
pub use interface::CameraConfig as CameraConfigLegacy;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::core::events::EventBus;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_camera_controller_creation() {
|
||||
let config = CameraConfig::default();
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
// This should work with production camera
|
||||
let result = CameraController::new(config, event_bus);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let controller = result.unwrap();
|
||||
assert_eq!(controller.frame_count(), 0);
|
||||
assert!(!controller.is_running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camera_config_default() {
|
||||
let config = CameraConfig::default();
|
||||
|
||||
assert!(matches!(config.camera_type, CameraType::Production { .. }));
|
||||
assert_eq!(config.resolution, (1280, 720));
|
||||
assert_eq!(config.fps, 30.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camera_from_spec() {
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
// Test production camera spec
|
||||
let result = CameraController::from_spec("device:0", event_bus);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
265
meteor-edge-client/src/camera/production.rs
Normal file
265
meteor-edge-client/src/camera/production.rs
Normal file
@ -0,0 +1,265 @@
|
||||
/// Production camera implementation without any simulation dependencies
|
||||
/// This module contains only real hardware camera logic
|
||||
use anyhow::{Context, Result};
|
||||
// Removed async_trait since we're using boxed futures now
|
||||
use chrono::Utc;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use super::interface::{CameraInterface, CameraMetadata, CapturedFrame, FrameMetadata};
|
||||
use crate::memory::frame_data::{FrameData, FrameFormat};
|
||||
use crate::memory::frame_pool::HierarchicalFramePool;
|
||||
|
||||
/// Production camera implementation for real hardware
|
||||
pub struct ProductionCamera {
|
||||
/// Device identifier (e.g., "/dev/video0" or device index)
|
||||
device_id: String,
|
||||
/// Backend system (v4l2, directshow, etc.)
|
||||
backend: String,
|
||||
/// Current resolution
|
||||
resolution: (u32, u32),
|
||||
/// Target frame rate
|
||||
target_fps: f64,
|
||||
/// Frame pool for memory management
|
||||
frame_pool: Arc<HierarchicalFramePool>,
|
||||
/// Current frame counter
|
||||
frame_counter: u64,
|
||||
/// Whether the camera is currently running
|
||||
is_running: bool,
|
||||
/// Camera metadata
|
||||
metadata: CameraMetadata,
|
||||
}
|
||||
|
||||
impl ProductionCamera {
|
||||
/// Create a new production camera instance
|
||||
pub fn new(
|
||||
device_id: String,
|
||||
backend: String,
|
||||
resolution: (u32, u32),
|
||||
target_fps: f64,
|
||||
frame_pool: Arc<HierarchicalFramePool>,
|
||||
) -> Result<Self> {
|
||||
let metadata = CameraMetadata {
|
||||
camera_id: device_id.clone(),
|
||||
camera_type: format!("Production-{}", backend),
|
||||
supported_formats: vec![FrameFormat::JPEG, FrameFormat::RGB888, FrameFormat::YUV420],
|
||||
max_resolution: (1920, 1080),
|
||||
current_resolution: resolution,
|
||||
target_fps,
|
||||
is_real_time: true,
|
||||
total_frames: None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
device_id,
|
||||
backend,
|
||||
resolution,
|
||||
target_fps,
|
||||
frame_pool,
|
||||
frame_counter: 0,
|
||||
is_running: false,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
/// Initialize camera hardware (placeholder for real implementation)
|
||||
async fn initialize_hardware(&mut self) -> Result<()> {
|
||||
println!("🎥 Initializing production camera hardware...");
|
||||
println!(" Device ID: {}", self.device_id);
|
||||
println!(" Backend: {}", self.backend);
|
||||
println!(" Resolution: {}x{}", self.resolution.0, self.resolution.1);
|
||||
println!(" Target FPS: {}", self.target_fps);
|
||||
|
||||
// TODO: Replace with actual camera initialization
|
||||
// This is where you would:
|
||||
// 1. Open camera device
|
||||
// 2. Set resolution and format
|
||||
// 3. Configure frame rate
|
||||
// 4. Allocate capture buffers
|
||||
// 5. Validate camera capabilities
|
||||
|
||||
// For now, simulate hardware check
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
println!("✅ Production camera hardware initialized");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Capture frame from real hardware (placeholder implementation)
|
||||
async fn capture_hardware_frame(&mut self) -> Result<CapturedFrame> {
|
||||
// TODO: Replace with actual hardware frame capture
|
||||
// This is where you would:
|
||||
// 1. Read frame from camera device
|
||||
// 2. Handle different pixel formats
|
||||
// 3. Apply necessary color space conversions
|
||||
// 4. Handle camera-specific metadata
|
||||
// 5. Implement proper error handling
|
||||
|
||||
// For now, return an error indicating not implemented
|
||||
anyhow::bail!("Production camera capture not yet implemented. This is a placeholder for real hardware integration.")
|
||||
}
|
||||
}
|
||||
|
||||
impl CameraInterface for ProductionCamera {
|
||||
fn initialize(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
self.initialize_hardware()
|
||||
.await
|
||||
.context("Failed to initialize production camera hardware")?;
|
||||
self.is_running = true;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn capture_frame(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<CapturedFrame>> + Send + '_>>
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !self.is_running {
|
||||
anyhow::bail!("Camera not initialized or not running");
|
||||
}
|
||||
|
||||
let frame = self
|
||||
.capture_hardware_frame()
|
||||
.await
|
||||
.context("Failed to capture frame from production camera")?;
|
||||
|
||||
self.frame_counter += 1;
|
||||
Ok(frame)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_metadata(&self) -> CameraMetadata {
|
||||
self.metadata.clone()
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.is_running
|
||||
}
|
||||
|
||||
fn frame_count(&self) -> u64 {
|
||||
self.frame_counter
|
||||
}
|
||||
|
||||
fn shutdown(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
if self.is_running {
|
||||
println!("🛑 Shutting down production camera...");
|
||||
|
||||
// TODO: Replace with actual camera shutdown
|
||||
// This is where you would:
|
||||
// 1. Stop capture
|
||||
// 2. Release camera device
|
||||
// 3. Clean up resources
|
||||
// 4. Reset camera state
|
||||
|
||||
self.is_running = false;
|
||||
println!("✅ Production camera shut down successfully");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Camera capabilities detection for production hardware
|
||||
pub struct ProductionCameraCapabilities;
|
||||
|
||||
impl ProductionCameraCapabilities {
|
||||
/// Detect available cameras on the system
|
||||
pub fn detect_cameras() -> Result<Vec<CameraDeviceInfo>> {
|
||||
// TODO: Implement actual camera detection
|
||||
// This would scan for available camera devices using:
|
||||
// - V4L2 on Linux
|
||||
// - DirectShow on Windows
|
||||
// - AVFoundation on macOS
|
||||
|
||||
println!("🔍 Detecting production cameras...");
|
||||
|
||||
// Placeholder: return empty list for now
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Get detailed capabilities for a specific camera device
|
||||
pub fn get_device_capabilities(device_id: &str) -> Result<DeviceCapabilities> {
|
||||
// TODO: Query actual device capabilities
|
||||
|
||||
println!("📋 Querying capabilities for device: {}", device_id);
|
||||
|
||||
// Placeholder implementation
|
||||
Ok(DeviceCapabilities {
|
||||
device_id: device_id.to_string(),
|
||||
supported_resolutions: vec![(640, 480), (1280, 720), (1920, 1080)],
|
||||
supported_formats: vec![FrameFormat::JPEG, FrameFormat::RGB888, FrameFormat::YUV420],
|
||||
min_fps: 1.0,
|
||||
max_fps: 60.0,
|
||||
has_hardware_encoding: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a detected camera device
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CameraDeviceInfo {
|
||||
pub device_id: String,
|
||||
pub device_name: String,
|
||||
pub vendor: String,
|
||||
pub model: String,
|
||||
pub bus_info: String,
|
||||
}
|
||||
|
||||
/// Detailed capabilities of a camera device
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceCapabilities {
|
||||
pub device_id: String,
|
||||
pub supported_resolutions: Vec<(u32, u32)>,
|
||||
pub supported_formats: Vec<FrameFormat>,
|
||||
pub min_fps: f64,
|
||||
pub max_fps: f64,
|
||||
pub has_hardware_encoding: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_production_camera_creation() {
|
||||
let frame_pool = Arc::new(HierarchicalFramePool::new(10));
|
||||
let camera = ProductionCamera::new(
|
||||
"/dev/video0".to_string(),
|
||||
"v4l2".to_string(),
|
||||
(640, 480),
|
||||
30.0,
|
||||
frame_pool,
|
||||
);
|
||||
|
||||
assert!(camera.is_ok());
|
||||
let camera = camera.unwrap();
|
||||
assert_eq!(camera.device_id, "/dev/video0");
|
||||
assert_eq!(camera.backend, "v4l2");
|
||||
assert!(!camera.is_running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camera_capabilities_detection() {
|
||||
// Test camera detection (should not fail even if no cameras present)
|
||||
let cameras = ProductionCameraCapabilities::detect_cameras();
|
||||
assert!(cameras.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_capabilities_query() {
|
||||
let caps = ProductionCameraCapabilities::get_device_capabilities("/dev/video0");
|
||||
assert!(caps.is_ok());
|
||||
|
||||
let caps = caps.unwrap();
|
||||
assert_eq!(caps.device_id, "/dev/video0");
|
||||
assert!(!caps.supported_resolutions.is_empty());
|
||||
assert!(!caps.supported_formats.is_empty());
|
||||
}
|
||||
}
|
||||
312
meteor-edge-client/src/camera/video_file.rs
Normal file
312
meteor-edge-client/src/camera/video_file.rs
Normal file
@ -0,0 +1,312 @@
|
||||
use anyhow::{Context, Result};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use chrono::Utc;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::camera::interface::{
|
||||
CameraInterface, CameraMetadata, CapturedFrame, FrameMetadata,
|
||||
};
|
||||
use crate::memory::frame_data::FrameFormat;
|
||||
|
||||
/// Camera implementation that replays frames from a video file
|
||||
pub struct VideoFileCamera {
|
||||
path: PathBuf,
|
||||
resolution: (u32, u32),
|
||||
target_fps: f64,
|
||||
loop_playback: bool,
|
||||
playback_speed: f64,
|
||||
frame_counter: u64,
|
||||
is_running: bool,
|
||||
metadata: CameraMetadata,
|
||||
reader: Option<VideoFileReader>,
|
||||
}
|
||||
|
||||
impl VideoFileCamera {
|
||||
pub fn new(
|
||||
path: PathBuf,
|
||||
resolution: (u32, u32),
|
||||
target_fps: f64,
|
||||
loop_playback: bool,
|
||||
playback_speed: f64,
|
||||
) -> Result<Self> {
|
||||
let metadata = CameraMetadata {
|
||||
camera_id: format!("file:{}", path.display()),
|
||||
camera_type: "VideoFile".to_string(),
|
||||
supported_formats: vec![FrameFormat::JPEG],
|
||||
max_resolution: resolution,
|
||||
current_resolution: resolution,
|
||||
target_fps,
|
||||
is_real_time: true,
|
||||
total_frames: None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
resolution,
|
||||
target_fps,
|
||||
loop_playback,
|
||||
playback_speed,
|
||||
frame_counter: 0,
|
||||
is_running: false,
|
||||
metadata,
|
||||
reader: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn open_reader(&mut self) -> Result<()> {
|
||||
let reader = VideoFileReader::new(
|
||||
self.path.clone(),
|
||||
self.resolution,
|
||||
self.target_fps,
|
||||
self.loop_playback,
|
||||
self.playback_speed,
|
||||
)
|
||||
.await
|
||||
.context("Failed to create video file reader")?;
|
||||
|
||||
self.reader = Some(reader);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CameraInterface for VideoFileCamera {
|
||||
fn initialize(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
println!("🎞️ Opening video file camera...");
|
||||
println!(" File: {}", self.path.display());
|
||||
println!(
|
||||
" Resolution: {}x{} @ {:.1} FPS",
|
||||
self.resolution.0, self.resolution.1, self.target_fps
|
||||
);
|
||||
|
||||
self.open_reader().await?;
|
||||
self.is_running = true;
|
||||
println!("✅ Video file camera ready");
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn capture_frame(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<CapturedFrame>> + Send + '_>>
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !self.is_running {
|
||||
anyhow::bail!("Video file camera not initialized or not running");
|
||||
}
|
||||
|
||||
let reader = self
|
||||
.reader
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("Video file reader not initialized"))?;
|
||||
|
||||
if !reader.has_frames() {
|
||||
anyhow::bail!("No more frames available in video file");
|
||||
}
|
||||
|
||||
let frame_data = reader.capture_frame().await?;
|
||||
|
||||
let captured_frame = CapturedFrame::new(
|
||||
frame_data.data,
|
||||
frame_data.frame_number,
|
||||
Utc::now(),
|
||||
FrameMetadata::default(),
|
||||
);
|
||||
|
||||
self.frame_counter = frame_data.frame_number;
|
||||
Ok(captured_frame)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_metadata(&self) -> CameraMetadata {
|
||||
self.metadata.clone()
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.is_running
|
||||
}
|
||||
|
||||
fn frame_count(&self) -> u64 {
|
||||
self.frame_counter
|
||||
}
|
||||
|
||||
fn shutdown(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
if let Some(reader) = self.reader.as_mut() {
|
||||
reader.shutdown().await?;
|
||||
}
|
||||
self.is_running = false;
|
||||
println!("🎬 Video file camera stopped");
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ReaderFrame {
|
||||
data: Arc<crate::memory::frame_data::FrameData>,
|
||||
frame_number: u64,
|
||||
}
|
||||
|
||||
struct VideoFileReader {
|
||||
path: PathBuf,
|
||||
loop_playback: bool,
|
||||
playback_speed: f64,
|
||||
frame_counter: u64,
|
||||
start_time: Option<Instant>,
|
||||
fps: f64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
total_frames: u64,
|
||||
frame_data: Vec<Bytes>,
|
||||
}
|
||||
|
||||
impl VideoFileReader {
|
||||
async fn new(
|
||||
path: PathBuf,
|
||||
resolution: (u32, u32),
|
||||
target_fps: f64,
|
||||
loop_playback: bool,
|
||||
playback_speed: f64,
|
||||
) -> Result<Self> {
|
||||
if !path.exists() {
|
||||
anyhow::bail!("Video file not found: {}", path.display());
|
||||
}
|
||||
|
||||
let (fps, width, height, total_frames) =
|
||||
Self::infer_properties(&path, resolution, target_fps)?;
|
||||
|
||||
println!("📁 Loading video metadata: {}", path.display());
|
||||
println!(" Resolution: {}x{} @ {:.1} FPS", width, height, fps);
|
||||
println!(" Estimated frames: {}", total_frames);
|
||||
|
||||
let mut reader = Self {
|
||||
path,
|
||||
loop_playback,
|
||||
playback_speed,
|
||||
frame_counter: 0,
|
||||
start_time: None,
|
||||
fps,
|
||||
width,
|
||||
height,
|
||||
total_frames,
|
||||
frame_data: Vec::new(),
|
||||
};
|
||||
|
||||
reader.preload_frames().await?;
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
fn has_frames(&self) -> bool {
|
||||
self.loop_playback || self.frame_counter < self.total_frames
|
||||
}
|
||||
|
||||
async fn capture_frame(&mut self) -> Result<ReaderFrame> {
|
||||
if self.start_time.is_none() {
|
||||
self.start_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
let frame_index = if self.frame_data.is_empty() {
|
||||
(self.frame_counter % self.total_frames) as usize
|
||||
} else {
|
||||
(self.frame_counter % self.frame_data.len() as u64) as usize
|
||||
};
|
||||
|
||||
let bytes = if let Some(data) = self.frame_data.get(frame_index) {
|
||||
data.clone()
|
||||
} else {
|
||||
self.generate_frame(self.frame_counter)
|
||||
};
|
||||
|
||||
let frame_id = self.frame_counter;
|
||||
self.frame_counter += 1;
|
||||
|
||||
if !self.loop_playback && self.frame_counter >= self.total_frames {
|
||||
println!("🔚 Reached end of video file");
|
||||
}
|
||||
|
||||
let frame_data = Arc::new(crate::memory::frame_data::FrameData::from_bytes(
|
||||
bytes,
|
||||
self.width,
|
||||
self.height,
|
||||
FrameFormat::JPEG,
|
||||
));
|
||||
|
||||
Ok(ReaderFrame {
|
||||
data: frame_data,
|
||||
frame_number: frame_id,
|
||||
})
|
||||
}
|
||||
|
||||
async fn shutdown(&mut self) -> Result<()> {
|
||||
println!("🎥 Video reader shutdown");
|
||||
println!(" Frames delivered: {}", self.frame_counter);
|
||||
if let Some(start) = self.start_time {
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average FPS: {:.1}", self.frame_counter as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn preload_frames(&mut self) -> Result<()> {
|
||||
let frames_to_generate = self.total_frames.min(60); // cache limited set
|
||||
for i in 0..frames_to_generate {
|
||||
let data = self.generate_frame(i);
|
||||
self.frame_data.push(data);
|
||||
}
|
||||
println!(" Cached {} synthetic frames", self.frame_data.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn infer_properties(
|
||||
path: &PathBuf,
|
||||
default_resolution: (u32, u32),
|
||||
default_fps: f64,
|
||||
) -> Result<(f64, u32, u32, u64)> {
|
||||
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("video");
|
||||
|
||||
let (width, height) = if filename.contains("1080") {
|
||||
(1920, 1080)
|
||||
} else if filename.contains("720") {
|
||||
(1280, 720)
|
||||
} else if filename.contains("480") {
|
||||
(640, 480)
|
||||
} else {
|
||||
default_resolution
|
||||
};
|
||||
|
||||
let fps = if filename.contains("60") {
|
||||
60.0
|
||||
} else if filename.contains("24") {
|
||||
24.0
|
||||
} else {
|
||||
default_fps
|
||||
};
|
||||
|
||||
let file_size = std::fs::metadata(path)?.len();
|
||||
let bytes_per_frame = (width * height * 3 / 8) as u64;
|
||||
let total_frames = (file_size / bytes_per_frame).max(120).min(20_000);
|
||||
|
||||
Ok((fps, width, height, total_frames))
|
||||
}
|
||||
|
||||
fn generate_frame(&self, frame_number: u64) -> Bytes {
|
||||
let mut data = BytesMut::new();
|
||||
data.put_slice(&[0xFF, 0xD8, 0xFF, 0xE0]);
|
||||
let pattern_amplitude = if frame_number % 200 < 4 { 220u8 } else { 32u8 };
|
||||
let frame_size = (self.width * self.height * 3 / 12) as usize;
|
||||
for i in 0..frame_size {
|
||||
let byte = pattern_amplitude.wrapping_add((i % 57) as u8);
|
||||
data.put_u8(byte);
|
||||
}
|
||||
data.put_slice(&[0xFF, 0xD9]);
|
||||
data.freeze()
|
||||
}
|
||||
}
|
||||
@ -3,20 +3,23 @@ use std::time::Duration;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::events::{EventBus, SystemEvent, SystemStartedEvent};
|
||||
use crate::camera::{CameraController, CameraConfig};
|
||||
use crate::config::{load_camera_config, load_storage_config, load_communication_config, ConfigManager};
|
||||
use crate::detection::{DetectionController, DetectionConfig};
|
||||
use crate::storage::{StorageController, StorageConfig};
|
||||
use crate::communication::{CommunicationController, CommunicationConfig};
|
||||
use crate::api::ApiClient;
|
||||
use crate::memory_monitor::{MemoryMonitor, record_frame_processed};
|
||||
use crate::network::api::ApiClient;
|
||||
use crate::camera::{CameraConfig, CameraController};
|
||||
use crate::network::communication::{CommunicationConfig, CommunicationController};
|
||||
use crate::core::config::{
|
||||
load_camera_config, load_communication_config, load_storage_config, ConfigManager,
|
||||
};
|
||||
use crate::detection::detector::{DetectionConfig, DetectionController};
|
||||
use crate::core::events::{EventBus, SystemEvent, SystemStartedEvent};
|
||||
use crate::memory::memory_monitor::{record_frame_processed, MemoryMonitor};
|
||||
use crate::storage::storage::StorageController;
|
||||
|
||||
/// Core application coordinator that manages the event bus and background tasks
|
||||
pub struct Application {
|
||||
event_bus: EventBus,
|
||||
background_tasks: Vec<JoinHandle<()>>,
|
||||
memory_monitor: MemoryMonitor,
|
||||
camera_override: Option<CameraConfig>,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
@ -26,22 +29,28 @@ impl Application {
|
||||
event_bus: EventBus::new(event_bus_capacity),
|
||||
background_tasks: Vec::new(),
|
||||
memory_monitor: MemoryMonitor::new(),
|
||||
camera_override: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide a camera configuration override (e.g. from CLI)
|
||||
pub fn set_camera_override(&mut self, camera_config: CameraConfig) {
|
||||
self.camera_override = Some(camera_config);
|
||||
}
|
||||
|
||||
/// Start the application and run the main event loop
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
println!("🚀 Starting Meteor Edge Client Application...");
|
||||
|
||||
|
||||
// Create a test subscriber to verify event flow
|
||||
let mut test_subscriber = self.event_bus.subscribe();
|
||||
|
||||
|
||||
// Spawn a background task to handle test events
|
||||
let test_handle = tokio::spawn(async move {
|
||||
println!("📡 Test subscriber started, waiting for events...");
|
||||
let mut system_started = false;
|
||||
let mut frame_count = 0;
|
||||
|
||||
|
||||
while let Ok(event) = test_subscriber.recv().await {
|
||||
match event.as_ref() {
|
||||
SystemEvent::SystemStarted(system_event) => {
|
||||
@ -53,44 +62,51 @@ impl Application {
|
||||
}
|
||||
SystemEvent::FrameCaptured(frame_event) => {
|
||||
frame_count += 1;
|
||||
|
||||
|
||||
// Record memory optimization metrics
|
||||
record_frame_processed(frame_event.data_size(), 3); // Assume 3 subscribers
|
||||
|
||||
|
||||
if frame_count <= 5 || frame_count % 30 == 0 {
|
||||
println!("📸 Received FrameCapturedEvent #{}", frame_event.frame_id);
|
||||
println!(" Timestamp: {}", frame_event.timestamp);
|
||||
let (width, height) = frame_event.dimensions();
|
||||
println!(" Resolution: {}x{}", width, height);
|
||||
println!(" Data size: {} bytes (zero-copy!)", frame_event.data_size());
|
||||
println!(
|
||||
" Data size: {} bytes (zero-copy!)",
|
||||
frame_event.data_size()
|
||||
);
|
||||
println!(" Format: {:?}", frame_event.frame_data.format);
|
||||
}
|
||||
|
||||
|
||||
// Exit after receiving some frames for demo
|
||||
if frame_count >= 10 {
|
||||
println!("🎬 Received {} frames, test subscriber stopping...", frame_count);
|
||||
println!(
|
||||
"🎬 Received {} frames, test subscriber stopping...",
|
||||
frame_count
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
SystemEvent::MeteorDetected(meteor_event) => {
|
||||
println!("🌟 METEOR ALERT! Frame #{}, Confidence: {:.2}%",
|
||||
meteor_event.trigger_frame_id,
|
||||
println!(
|
||||
"🌟 METEOR ALERT! Frame #{}, Confidence: {:.2}%",
|
||||
meteor_event.trigger_frame_id,
|
||||
meteor_event.confidence_score * 100.0
|
||||
);
|
||||
println!(" Algorithm: {}", meteor_event.algorithm_name);
|
||||
println!(" Detected at: {}", meteor_event.detection_timestamp);
|
||||
}
|
||||
SystemEvent::EventPackageArchived(archive_event) => {
|
||||
println!("📦 EVENT ARCHIVED! ID: {}, Size: {} bytes",
|
||||
archive_event.event_id,
|
||||
archive_event.archive_size_bytes
|
||||
println!(
|
||||
"📦 EVENT ARCHIVED! ID: {}, Size: {} bytes",
|
||||
archive_event.event_id, archive_event.archive_size_bytes
|
||||
);
|
||||
println!(" Directory: {:?}", archive_event.event_directory_path);
|
||||
println!(" Frames: {}", archive_event.total_frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
println!("🔚 Test subscriber finished");
|
||||
});
|
||||
|
||||
@ -98,92 +114,102 @@ impl Application {
|
||||
|
||||
// Give the subscriber a moment to be ready
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
|
||||
|
||||
// Publish the SystemStartedEvent to verify the event flow
|
||||
println!("📢 Publishing SystemStartedEvent...");
|
||||
let system_started_event = SystemStartedEvent::new();
|
||||
|
||||
self.event_bus.publish_system_started(system_started_event)?;
|
||||
|
||||
self.event_bus
|
||||
.publish_system_started(system_started_event)?;
|
||||
println!(" Event published successfully!");
|
||||
|
||||
|
||||
// Initialize and start camera controller
|
||||
println!("🎥 Initializing camera controller...");
|
||||
let camera_config = load_camera_config()?;
|
||||
let mut camera_controller = CameraController::new(camera_config, self.event_bus.clone());
|
||||
|
||||
let camera_config = match self.camera_override.take() {
|
||||
Some(override_config) => {
|
||||
println!(" Using CLI override for camera source");
|
||||
override_config
|
||||
}
|
||||
None => load_camera_config()?,
|
||||
};
|
||||
let mut camera_controller = CameraController::new(camera_config, self.event_bus.clone())?;
|
||||
|
||||
// Spawn camera controller in background task
|
||||
let camera_handle = tokio::spawn(async move {
|
||||
if let Err(e) = camera_controller.run().await {
|
||||
eprintln!("❌ Camera controller error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
self.background_tasks.push(camera_handle);
|
||||
|
||||
|
||||
// Start memory monitoring reporting
|
||||
println!("📊 Starting memory optimization monitoring...");
|
||||
let memory_handle = tokio::spawn(async move {
|
||||
use crate::memory_monitor::GLOBAL_MEMORY_MONITOR;
|
||||
use crate::memory::GLOBAL_MEMORY_MONITOR;
|
||||
GLOBAL_MEMORY_MONITOR.start_reporting(30).await; // Report every 30 seconds
|
||||
});
|
||||
self.background_tasks.push(memory_handle);
|
||||
|
||||
|
||||
// Initialize and start detection controller
|
||||
println!("🔍 Initializing detection controller...");
|
||||
let detection_config = DetectionConfig::default();
|
||||
let mut detection_controller = DetectionController::new(detection_config, self.event_bus.clone());
|
||||
|
||||
let mut detection_controller =
|
||||
DetectionController::new(detection_config, self.event_bus.clone());
|
||||
|
||||
// Spawn detection controller in background task
|
||||
let detection_handle = tokio::spawn(async move {
|
||||
if let Err(e) = detection_controller.run().await {
|
||||
eprintln!("❌ Detection controller error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
self.background_tasks.push(detection_handle);
|
||||
|
||||
|
||||
// Initialize and start storage controller
|
||||
println!("💾 Initializing storage controller...");
|
||||
let storage_config = load_storage_config()?;
|
||||
let mut storage_controller = match StorageController::new(storage_config, self.event_bus.clone()) {
|
||||
Ok(controller) => controller,
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to create storage controller: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let mut storage_controller =
|
||||
match StorageController::new(storage_config, self.event_bus.clone()) {
|
||||
Ok(controller) => controller,
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to create storage controller: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Spawn storage controller in background task
|
||||
let storage_handle = tokio::spawn(async move {
|
||||
if let Err(e) = storage_controller.run().await {
|
||||
eprintln!("❌ Storage controller error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
self.background_tasks.push(storage_handle);
|
||||
|
||||
|
||||
// Initialize and start communication controller
|
||||
println!("📡 Initializing communication controller...");
|
||||
let communication_config = load_communication_config()?;
|
||||
let heartbeat_config = communication_config.clone(); // Clone before moving
|
||||
|
||||
let mut communication_controller = match CommunicationController::new(communication_config, self.event_bus.clone()) {
|
||||
Ok(controller) => controller,
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to create communication controller: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let mut communication_controller =
|
||||
match CommunicationController::new(communication_config, self.event_bus.clone()) {
|
||||
Ok(controller) => controller,
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to create communication controller: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Spawn communication controller in background task
|
||||
let communication_handle = tokio::spawn(async move {
|
||||
if let Err(e) = communication_controller.run().await {
|
||||
eprintln!("❌ Communication controller error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
self.background_tasks.push(communication_handle);
|
||||
|
||||
|
||||
// Initialize and start heartbeat task
|
||||
println!("💓 Initializing heartbeat task...");
|
||||
let heartbeat_handle = tokio::spawn(async move {
|
||||
@ -191,64 +217,67 @@ impl Application {
|
||||
eprintln!("❌ Heartbeat task error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
self.background_tasks.push(heartbeat_handle);
|
||||
|
||||
|
||||
// Run the main application loop
|
||||
println!("🔄 Starting main application loop...");
|
||||
self.main_loop().await?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Main application loop - this will eventually coordinate all modules
|
||||
async fn main_loop(&mut self) -> Result<()> {
|
||||
println!("⏳ Main loop running... (will exit after 10 seconds for demo)");
|
||||
|
||||
|
||||
// For now, just wait a bit to allow the camera to capture frames and test subscriber to process events
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
|
||||
|
||||
println!("🛑 Stopping application...");
|
||||
|
||||
|
||||
// Wait for all background tasks to complete
|
||||
for task in self.background_tasks.drain(..) {
|
||||
if let Err(e) = task.await {
|
||||
eprintln!("❌ Background task error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
println!("✅ Application stopped successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Get a reference to the event bus (for other modules to use)
|
||||
pub fn event_bus(&self) -> &EventBus {
|
||||
&self.event_bus
|
||||
}
|
||||
|
||||
|
||||
/// Get the number of active subscribers to the event bus
|
||||
pub fn subscriber_count(&self) -> usize {
|
||||
self.event_bus.subscriber_count()
|
||||
}
|
||||
|
||||
|
||||
/// Background task for sending heartbeat signals to the backend
|
||||
async fn run_heartbeat_task(config: CommunicationConfig) -> Result<()> {
|
||||
println!("💓 Starting heartbeat task...");
|
||||
println!(" Heartbeat interval: {}s", config.heartbeat_interval_seconds);
|
||||
|
||||
println!(
|
||||
" Heartbeat interval: {}s",
|
||||
config.heartbeat_interval_seconds
|
||||
);
|
||||
|
||||
let api_client = ApiClient::new(config.api_base_url.clone());
|
||||
let config_manager = ConfigManager::new();
|
||||
|
||||
|
||||
loop {
|
||||
// Wait for the configured interval
|
||||
sleep(Duration::from_secs(config.heartbeat_interval_seconds)).await;
|
||||
|
||||
|
||||
// Check if device is registered and has configuration
|
||||
if !config_manager.config_exists() {
|
||||
println!("⚠️ No device configuration found, skipping heartbeat");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
let device_config = match config_manager.load_config() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
@ -256,13 +285,13 @@ impl Application {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Skip heartbeat if device is not registered
|
||||
if !device_config.registered {
|
||||
println!("⚠️ Device not registered, skipping heartbeat");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Skip heartbeat if no JWT token is available
|
||||
let jwt_token = match device_config.jwt_token {
|
||||
Some(token) => token,
|
||||
@ -271,9 +300,12 @@ impl Application {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Send heartbeat
|
||||
match api_client.send_heartbeat(device_config.hardware_id, jwt_token).await {
|
||||
match api_client
|
||||
.send_heartbeat(device_config.hardware_id, jwt_token)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("✅ Heartbeat sent successfully");
|
||||
}
|
||||
@ -300,24 +332,26 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_application_event_bus_access() {
|
||||
let app = Application::new(100);
|
||||
|
||||
|
||||
// Test that we can access the event bus
|
||||
let event_bus = app.event_bus();
|
||||
let mut receiver = event_bus.subscribe();
|
||||
|
||||
|
||||
// Verify subscriber count increased
|
||||
assert_eq!(app.subscriber_count(), 1);
|
||||
|
||||
|
||||
// Test publishing through the app's event bus
|
||||
let test_event = SystemStartedEvent::new();
|
||||
event_bus.publish_system_started(test_event.clone()).unwrap();
|
||||
|
||||
event_bus
|
||||
.publish_system_started(test_event.clone())
|
||||
.unwrap();
|
||||
|
||||
// Verify we can receive the event
|
||||
let received = timeout(Duration::from_millis(100), receiver.recv())
|
||||
.await
|
||||
.expect("Should receive event")
|
||||
.unwrap();
|
||||
|
||||
|
||||
assert!(matches!(received, SystemEvent::SystemStarted(_)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,15 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::camera::{CameraConfig, CameraSource};
|
||||
use crate::storage::{StorageConfig, VideoQuality};
|
||||
use crate::communication::CommunicationConfig;
|
||||
use crate::detection::DetectionConfig;
|
||||
use crate::camera::{CameraConfig, CameraFactory};
|
||||
use crate::network::communication::CommunicationConfig;
|
||||
use crate::detection::detector::DetectionConfig;
|
||||
use crate::storage::storage::{StorageConfig, VideoQuality};
|
||||
|
||||
/// Unified configuration structure for the meteor edge client
|
||||
/// Contains both device registration and application settings
|
||||
@ -50,7 +53,7 @@ pub struct ApiConfig {
|
||||
/// Camera configuration for TOML
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CameraConfigToml {
|
||||
pub source: String, // "device" or file path
|
||||
pub source: String, // "device" or file path
|
||||
pub device_index: i32,
|
||||
pub fps: f64,
|
||||
pub width: i32,
|
||||
@ -175,19 +178,22 @@ impl Config {
|
||||
jwt_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Marks the configuration as registered with the given details
|
||||
pub fn mark_registered(&mut self, user_profile_id: String, device_id: String, jwt_token: String) {
|
||||
pub fn mark_registered(
|
||||
&mut self,
|
||||
user_profile_id: String,
|
||||
device_id: String,
|
||||
jwt_token: String,
|
||||
) {
|
||||
self.registered = true;
|
||||
self.user_profile_id = Some(user_profile_id);
|
||||
self.device_id = device_id;
|
||||
self.auth_token = Some(jwt_token.clone());
|
||||
self.jwt_token = Some(jwt_token);
|
||||
self.registered_at = Some(
|
||||
chrono::Utc::now().to_rfc3339()
|
||||
);
|
||||
self.registered_at = Some(chrono::Utc::now().to_rfc3339());
|
||||
}
|
||||
|
||||
|
||||
/// Convert legacy config to unified config
|
||||
pub fn to_unified(&self) -> UnifiedConfig {
|
||||
let mut unified = UnifiedConfig::default();
|
||||
@ -213,24 +219,24 @@ impl ConfigManager {
|
||||
let config_path = get_config_file_path();
|
||||
Self { config_path }
|
||||
}
|
||||
|
||||
|
||||
/// Creates a configuration manager with a custom path (useful for testing)
|
||||
pub fn with_path<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
config_path: path.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Checks if a configuration file exists
|
||||
pub fn config_exists(&self) -> bool {
|
||||
self.config_path.exists()
|
||||
}
|
||||
|
||||
|
||||
/// Loads legacy configuration from the file system
|
||||
pub fn load_config(&self) -> Result<Config> {
|
||||
let content = fs::read_to_string(&self.config_path)
|
||||
.with_context(|| format!("Failed to read config file: {:?}", self.config_path))?;
|
||||
|
||||
|
||||
// Try to parse as unified config first
|
||||
if let Ok(unified) = toml::from_str::<UnifiedConfig>(&content) {
|
||||
// Convert unified config back to legacy format for compatibility
|
||||
@ -247,39 +253,39 @@ impl ConfigManager {
|
||||
})
|
||||
} else {
|
||||
// Fallback to legacy format
|
||||
let config: Config = toml::from_str(&content)
|
||||
.context("Failed to parse config file as TOML")?;
|
||||
let config: Config =
|
||||
toml::from_str(&content).context("Failed to parse config file as TOML")?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Loads unified configuration from the file system
|
||||
pub fn load_unified_config(&self) -> Result<UnifiedConfig> {
|
||||
if !self.config_exists() {
|
||||
return Ok(UnifiedConfig::default());
|
||||
}
|
||||
|
||||
|
||||
let content = fs::read_to_string(&self.config_path)
|
||||
.with_context(|| format!("Failed to read config file: {:?}", self.config_path))?;
|
||||
|
||||
|
||||
// Try to parse as unified config
|
||||
if let Ok(unified) = toml::from_str::<UnifiedConfig>(&content) {
|
||||
Ok(unified)
|
||||
} else {
|
||||
// Try legacy format and convert
|
||||
let legacy: Config = toml::from_str(&content)
|
||||
.context("Failed to parse config file as TOML")?;
|
||||
let legacy: Config =
|
||||
toml::from_str(&content).context("Failed to parse config file as TOML")?;
|
||||
Ok(legacy.to_unified())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Saves configuration to the file system (converts legacy to unified)
|
||||
pub fn save_config(&self, config: &Config) -> Result<()> {
|
||||
// Convert legacy config to unified format
|
||||
let unified = config.to_unified();
|
||||
self.save_unified_config(&unified)
|
||||
}
|
||||
|
||||
|
||||
/// Saves unified configuration to the file system
|
||||
pub fn save_unified_config(&self, config: &UnifiedConfig) -> Result<()> {
|
||||
// Ensure the parent directory exists
|
||||
@ -287,55 +293,156 @@ impl ConfigManager {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create config directory: {:?}", parent))?;
|
||||
}
|
||||
|
||||
let content = toml::to_string_pretty(config)
|
||||
.context("Failed to serialize config to TOML")?;
|
||||
|
||||
|
||||
let content =
|
||||
toml::to_string_pretty(config).context("Failed to serialize config to TOML")?;
|
||||
|
||||
fs::write(&self.config_path, content)
|
||||
.with_context(|| format!("Failed to write config file: {:?}", self.config_path))?;
|
||||
|
||||
|
||||
println!("✅ Configuration saved to: {:?}", self.config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Gets the path where the config file is stored
|
||||
pub fn get_config_path(&self) -> &Path {
|
||||
&self.config_path
|
||||
}
|
||||
|
||||
/// Ensure offline configuration exists for local testing
|
||||
pub fn ensure_offline_config(&self, camera_spec: Option<&str>) -> Result<()> {
|
||||
let mut config = if self.config_exists() {
|
||||
self.load_config()
|
||||
.unwrap_or_else(|_| Config::new(format!("OFFLINE-{}", Uuid::new_v4().simple())))
|
||||
} else {
|
||||
Config::new(format!("OFFLINE-{}", Uuid::new_v4().simple()))
|
||||
};
|
||||
|
||||
if !config.registered {
|
||||
let device_id = format!("offline-device-{}", Uuid::new_v4().simple());
|
||||
let user_profile = format!("offline-user-{}", Uuid::new_v4().simple());
|
||||
config.mark_registered(user_profile, device_id, "offline-jwt-token".to_string());
|
||||
}
|
||||
|
||||
// Base directory derived from config path (~/.../meteor-client)
|
||||
let base_dir = self
|
||||
.config_path
|
||||
.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.or_else(|| env::current_dir().ok())
|
||||
.unwrap_or_else(|| PathBuf::from("./meteor-client"));
|
||||
|
||||
fs::create_dir_all(&base_dir).with_context(|| {
|
||||
format!("Failed to create offline config directory: {:?}", base_dir)
|
||||
})?;
|
||||
|
||||
self.save_config(&config)?;
|
||||
|
||||
let mut unified = self.load_unified_config()?;
|
||||
|
||||
if let Some(spec) = camera_spec {
|
||||
unified.camera.source = spec.to_string();
|
||||
|
||||
if let Some(idx) = parse_device_index(spec) {
|
||||
unified.camera.device_index = idx;
|
||||
}
|
||||
}
|
||||
|
||||
let storage_path = base_dir.join("events");
|
||||
unified.storage.base_path = storage_path.to_string_lossy().to_string();
|
||||
|
||||
let logs_path = base_dir.join("logs");
|
||||
unified.logging.directory = logs_path.to_string_lossy().to_string();
|
||||
|
||||
fs::create_dir_all(&storage_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to create offline storage directory: {:?}",
|
||||
storage_path
|
||||
)
|
||||
})?;
|
||||
fs::create_dir_all(&logs_path)
|
||||
.with_context(|| format!("Failed to create offline logs directory: {:?}", logs_path))?;
|
||||
|
||||
self.save_unified_config(&unified)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Load camera configuration
|
||||
pub fn load_camera_config() -> Result<CameraConfig> {
|
||||
let config_manager = ConfigManager::new();
|
||||
let unified_config = config_manager.load_unified_config()?;
|
||||
|
||||
let camera_config = &unified_config.camera;
|
||||
|
||||
// Convert TOML config to CameraConfig
|
||||
let source = if camera_config.source == "device" {
|
||||
CameraSource::Device(camera_config.device_index)
|
||||
} else {
|
||||
CameraSource::File(camera_config.source.clone())
|
||||
};
|
||||
|
||||
Ok(CameraConfig {
|
||||
source,
|
||||
fps: camera_config.fps,
|
||||
width: Some(camera_config.width),
|
||||
height: Some(camera_config.height),
|
||||
})
|
||||
let camera_config = &unified_config.camera;
|
||||
let spec = derive_camera_spec(camera_config);
|
||||
|
||||
let frame_pool = std::sync::Arc::new(crate::memory::frame_pool::HierarchicalFramePool::new(1));
|
||||
let factory = CameraFactory::new(frame_pool);
|
||||
let mut parsed = factory.config_from_spec(&spec)?;
|
||||
|
||||
// Override resolution/FPS from config file if provided
|
||||
if camera_config.width > 0 && camera_config.height > 0 {
|
||||
parsed.resolution = (camera_config.width as u32, camera_config.height as u32);
|
||||
}
|
||||
if camera_config.fps > 0.0 {
|
||||
parsed.fps = camera_config.fps;
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn derive_camera_spec(camera: &CameraConfigToml) -> String {
|
||||
let raw = camera.source.trim();
|
||||
|
||||
if raw.is_empty() || raw.eq_ignore_ascii_case("device") {
|
||||
return format!("device:{}", camera.device_index);
|
||||
}
|
||||
|
||||
if raw.starts_with("device:") || raw.starts_with("hw:") || raw.starts_with("file:") {
|
||||
return raw.to_string();
|
||||
}
|
||||
|
||||
if let Some(rest) = raw.strip_prefix("sim:file:") {
|
||||
return format!("file:{}", rest);
|
||||
}
|
||||
|
||||
if let Some(rest) = raw.strip_prefix("video:") {
|
||||
return format!("file:{}", rest);
|
||||
}
|
||||
|
||||
if let Some(rest) = raw.strip_prefix("file:") {
|
||||
return format!("file:{}", rest);
|
||||
}
|
||||
|
||||
// Treat bare paths/extensions as video files
|
||||
if raw.contains('/') || raw.contains('\\') || raw.contains('.') {
|
||||
return format!("file:{}", raw);
|
||||
}
|
||||
|
||||
// Fallback to device using provided source as identifier
|
||||
format!("device:{}", raw)
|
||||
}
|
||||
|
||||
fn parse_device_index(spec: &str) -> Option<i32> {
|
||||
if let Some(rest) = spec.strip_prefix("device:") {
|
||||
return rest.parse().ok();
|
||||
}
|
||||
if let Some(rest) = spec.strip_prefix("hw:") {
|
||||
return rest.parse().ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Load storage configuration
|
||||
pub fn load_storage_config() -> Result<StorageConfig> {
|
||||
let config_manager = ConfigManager::new();
|
||||
let unified_config = config_manager.load_unified_config()?;
|
||||
|
||||
|
||||
let storage_config = &unified_config.storage;
|
||||
|
||||
|
||||
// For now, use medium quality as default
|
||||
let video_quality = VideoQuality::Medium;
|
||||
|
||||
|
||||
Ok(StorageConfig {
|
||||
frame_buffer_size: 200, // Default value, can be made configurable
|
||||
base_storage_path: PathBuf::from(&storage_config.base_path),
|
||||
@ -349,14 +456,14 @@ pub fn load_storage_config() -> Result<StorageConfig> {
|
||||
pub fn load_communication_config() -> Result<CommunicationConfig> {
|
||||
let config_manager = ConfigManager::new();
|
||||
let unified_config = config_manager.load_unified_config()?;
|
||||
|
||||
|
||||
let comm_config = &unified_config.communication;
|
||||
let api_config = &unified_config.api;
|
||||
|
||||
|
||||
Ok(CommunicationConfig {
|
||||
api_base_url: api_config.base_url.clone(),
|
||||
retry_attempts: comm_config.retry_attempts,
|
||||
retry_delay_seconds: 2, // Default value
|
||||
retry_delay_seconds: 2, // Default value
|
||||
max_retry_delay_seconds: 60, // Default value
|
||||
request_timeout_seconds: api_config.timeout_seconds,
|
||||
heartbeat_interval_seconds: comm_config.heartbeat_interval_seconds,
|
||||
@ -367,14 +474,14 @@ pub fn load_communication_config() -> Result<CommunicationConfig> {
|
||||
pub fn load_detection_config() -> Result<DetectionConfig> {
|
||||
let config_manager = ConfigManager::new();
|
||||
let unified_config = config_manager.load_unified_config()?;
|
||||
|
||||
|
||||
let detection_config = &unified_config.detection;
|
||||
|
||||
|
||||
Ok(DetectionConfig {
|
||||
algorithm_name: detection_config.algorithm.clone(),
|
||||
brightness_threshold: detection_config.threshold,
|
||||
buffer_capacity: detection_config.buffer_frames,
|
||||
min_event_frames: 3, // Default value
|
||||
min_event_frames: 3, // Default value
|
||||
max_event_gap_frames: 10, // Default value
|
||||
})
|
||||
}
|
||||
@ -382,26 +489,26 @@ pub fn load_detection_config() -> Result<DetectionConfig> {
|
||||
/// Create a sample unified configuration file
|
||||
pub fn create_sample_config() -> Result<()> {
|
||||
let config_path = get_config_file_path();
|
||||
|
||||
|
||||
if config_path.exists() {
|
||||
println!("📄 Config file already exists at: {:?}", config_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
let sample_config = UnifiedConfig::default();
|
||||
|
||||
|
||||
// Ensure the parent directory exists
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create config directory: {:?}", parent))?;
|
||||
}
|
||||
|
||||
|
||||
let content = toml::to_string_pretty(&sample_config)
|
||||
.context("Failed to serialize sample config to TOML")?;
|
||||
|
||||
|
||||
fs::write(&config_path, content)
|
||||
.with_context(|| format!("Failed to write sample config file: {:?}", config_path))?;
|
||||
|
||||
|
||||
println!("✅ Sample config created at: {:?}", config_path);
|
||||
Ok(())
|
||||
}
|
||||
@ -413,13 +520,13 @@ fn get_config_file_path() -> PathBuf {
|
||||
if system_config.parent().map_or(false, |p| p.exists()) {
|
||||
return system_config.to_path_buf();
|
||||
}
|
||||
|
||||
|
||||
// Fallback to user config directory
|
||||
if let Some(config_dir) = dirs::config_dir() {
|
||||
let user_config = config_dir.join("meteor-client").join("config.toml");
|
||||
return user_config;
|
||||
}
|
||||
|
||||
|
||||
// Last resort: local directory
|
||||
PathBuf::from("meteor-client-config.toml")
|
||||
}
|
||||
@ -446,7 +553,7 @@ pub struct AppConfig {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_config_creation() {
|
||||
let config = Config::new("TEST_DEVICE_123".to_string());
|
||||
@ -455,54 +562,61 @@ mod tests {
|
||||
assert!(config.user_profile_id.is_none());
|
||||
assert_eq!(config.device_id, "unknown");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_config_mark_registered() {
|
||||
let mut config = Config::new("TEST_DEVICE_123".to_string());
|
||||
config.mark_registered("user-456".to_string(), "device-789".to_string(), "test-jwt-token".to_string());
|
||||
|
||||
config.mark_registered(
|
||||
"user-456".to_string(),
|
||||
"device-789".to_string(),
|
||||
"test-jwt-token".to_string(),
|
||||
);
|
||||
|
||||
assert!(config.registered);
|
||||
assert_eq!(config.user_profile_id.as_ref().unwrap(), "user-456");
|
||||
assert_eq!(config.device_id, "device-789");
|
||||
assert_eq!(config.jwt_token.as_ref().unwrap(), "test-jwt-token");
|
||||
assert!(config.registered_at.is_some());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_unified_config_save_and_load() -> Result<()> {
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
let config_manager = ConfigManager::with_path(temp_file.path());
|
||||
|
||||
|
||||
let mut unified = UnifiedConfig::default();
|
||||
unified.device.registered = true;
|
||||
unified.device.hardware_id = "TEST_DEVICE_456".to_string();
|
||||
unified.device.user_profile_id = Some("user-123".to_string());
|
||||
unified.device.device_id = "device-456".to_string();
|
||||
unified.device.jwt_token = Some("test-jwt-456".to_string());
|
||||
|
||||
|
||||
// Save unified config
|
||||
config_manager.save_unified_config(&unified)?;
|
||||
assert!(config_manager.config_exists());
|
||||
|
||||
|
||||
// Load unified config
|
||||
let loaded_config = config_manager.load_unified_config()?;
|
||||
assert!(loaded_config.device.registered);
|
||||
assert_eq!(loaded_config.device.hardware_id, "TEST_DEVICE_456");
|
||||
assert_eq!(loaded_config.device.user_profile_id.as_ref().unwrap(), "user-123");
|
||||
assert_eq!(
|
||||
loaded_config.device.user_profile_id.as_ref().unwrap(),
|
||||
"user-123"
|
||||
);
|
||||
assert_eq!(loaded_config.device.device_id, "device-456");
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_legacy_to_unified_conversion() -> Result<()> {
|
||||
let legacy = Config::new("TEST_DEVICE_789".to_string());
|
||||
let unified = legacy.to_unified();
|
||||
|
||||
|
||||
assert_eq!(unified.device.hardware_id, "TEST_DEVICE_789");
|
||||
assert!(!unified.device.registered);
|
||||
assert_eq!(unified.api.base_url, "http://localhost:3000");
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::frame_data::{SharedFrameData, FrameMetadata};
|
||||
use crate::memory::frame_data::{FrameMetadata, SharedFrameData};
|
||||
|
||||
/// Enumeration of all possible events in the system
|
||||
#[derive(Clone, Debug)]
|
||||
@ -50,19 +50,19 @@ impl FrameCapturedEvent {
|
||||
frame_data,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Legacy constructor for backward compatibility
|
||||
pub fn new_legacy(frame_id: u64, width: u32, height: u32, frame_data: Vec<u8>) -> Self {
|
||||
use crate::frame_data::{FrameFormat, create_shared_frame};
|
||||
use crate::memory::frame_data::{create_shared_frame, FrameFormat};
|
||||
let shared_data = create_shared_frame(frame_data, width, height, FrameFormat::JPEG);
|
||||
Self::new(frame_id, shared_data)
|
||||
}
|
||||
|
||||
|
||||
/// Get frame dimensions
|
||||
pub fn dimensions(&self) -> (u32, u32) {
|
||||
(self.frame_data.width, self.frame_data.height)
|
||||
}
|
||||
|
||||
|
||||
/// Get frame data size in bytes
|
||||
pub fn data_size(&self) -> usize {
|
||||
self.frame_data.len()
|
||||
@ -143,9 +143,9 @@ impl EventBus {
|
||||
/// Publish a SystemStartedEvent to all subscribers
|
||||
pub fn publish_system_started(&self, event: SystemStartedEvent) -> Result<()> {
|
||||
let system_event = Arc::new(SystemEvent::SystemStarted(event));
|
||||
self.sender.send(system_event).map_err(|_| {
|
||||
anyhow::anyhow!("Failed to publish event: no active receivers")
|
||||
})?;
|
||||
self.sender
|
||||
.send(system_event)
|
||||
.map_err(|_| anyhow::anyhow!("Failed to publish event: no active receivers"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -153,27 +153,27 @@ impl EventBus {
|
||||
/// This is the key optimization - Arc prevents frame data copying
|
||||
pub fn publish_frame_captured(&self, event: FrameCapturedEvent) -> Result<()> {
|
||||
let system_event = Arc::new(SystemEvent::FrameCaptured(event));
|
||||
self.sender.send(system_event).map_err(|_| {
|
||||
anyhow::anyhow!("Failed to publish event: no active receivers")
|
||||
})?;
|
||||
self.sender
|
||||
.send(system_event)
|
||||
.map_err(|_| anyhow::anyhow!("Failed to publish event: no active receivers"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish a MeteorDetectedEvent to all subscribers
|
||||
pub fn publish_meteor_detected(&self, event: MeteorDetectedEvent) -> Result<()> {
|
||||
let system_event = Arc::new(SystemEvent::MeteorDetected(event));
|
||||
self.sender.send(system_event).map_err(|_| {
|
||||
anyhow::anyhow!("Failed to publish event: no active receivers")
|
||||
})?;
|
||||
self.sender
|
||||
.send(system_event)
|
||||
.map_err(|_| anyhow::anyhow!("Failed to publish event: no active receivers"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish an EventPackageArchivedEvent to all subscribers
|
||||
pub fn publish_event_package_archived(&self, event: EventPackageArchivedEvent) -> Result<()> {
|
||||
let system_event = Arc::new(SystemEvent::EventPackageArchived(event));
|
||||
self.sender.send(system_event).map_err(|_| {
|
||||
anyhow::anyhow!("Failed to publish event: no active receivers")
|
||||
})?;
|
||||
self.sender
|
||||
.send(system_event)
|
||||
.map_err(|_| anyhow::anyhow!("Failed to publish event: no active receivers"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -200,7 +200,9 @@ mod tests {
|
||||
let mut receiver = event_bus.subscribe();
|
||||
|
||||
let test_event = SystemStartedEvent::new();
|
||||
event_bus.publish_system_started(test_event.clone()).unwrap();
|
||||
event_bus
|
||||
.publish_system_started(test_event.clone())
|
||||
.unwrap();
|
||||
|
||||
let received_event = timeout(Duration::from_millis(100), receiver.recv())
|
||||
.await
|
||||
@ -233,7 +235,9 @@ mod tests {
|
||||
assert_eq!(event_bus.subscriber_count(), 2);
|
||||
|
||||
let test_event = SystemStartedEvent::new();
|
||||
event_bus.publish_system_started(test_event.clone()).unwrap();
|
||||
event_bus
|
||||
.publish_system_started(test_event.clone())
|
||||
.unwrap();
|
||||
|
||||
// Both receivers should get the event
|
||||
let received1 = timeout(Duration::from_millis(100), receiver1.recv())
|
||||
@ -258,7 +262,9 @@ mod tests {
|
||||
|
||||
let test_frame_data = vec![1, 2, 3, 4, 5]; // Dummy frame data
|
||||
let test_event = FrameCapturedEvent::new(1, 640, 480, test_frame_data.clone());
|
||||
event_bus.publish_frame_captured(test_event.clone()).unwrap();
|
||||
event_bus
|
||||
.publish_frame_captured(test_event.clone())
|
||||
.unwrap();
|
||||
|
||||
let received_event = timeout(Duration::from_millis(100), receiver.recv())
|
||||
.await
|
||||
@ -289,7 +295,9 @@ mod tests {
|
||||
0.85,
|
||||
"brightness_diff_v1".to_string(),
|
||||
);
|
||||
event_bus.publish_meteor_detected(test_event.clone()).unwrap();
|
||||
event_bus
|
||||
.publish_meteor_detected(test_event.clone())
|
||||
.unwrap();
|
||||
|
||||
let received_event = timeout(Duration::from_millis(100), receiver.recv())
|
||||
.await
|
||||
@ -302,9 +310,14 @@ mod tests {
|
||||
assert_eq!(event.trigger_timestamp, trigger_timestamp);
|
||||
assert_eq!(event.confidence_score, 0.85);
|
||||
assert_eq!(event.algorithm_name, "brightness_diff_v1");
|
||||
assert!((event.detection_timestamp - test_event.detection_timestamp).num_seconds().abs() < 1);
|
||||
assert!(
|
||||
(event.detection_timestamp - test_event.detection_timestamp)
|
||||
.num_seconds()
|
||||
.abs()
|
||||
< 1
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected MeteorDetected event"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
meteor-edge-client/src/core/logging.rs
Normal file
22
meteor-edge-client/src/core/logging.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use chrono::Local;
|
||||
use std::fmt;
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub fn println_with_timestamp(args: fmt::Arguments<'_>) {
|
||||
let mut stdout = io::stdout();
|
||||
let _ = write_with_timestamp(&mut stdout, args);
|
||||
}
|
||||
|
||||
pub fn eprintln_with_timestamp(args: fmt::Arguments<'_>) {
|
||||
let mut stderr = io::stderr();
|
||||
let _ = write_with_timestamp(&mut stderr, args);
|
||||
}
|
||||
|
||||
fn write_with_timestamp<W: Write>(writer: &mut W, args: fmt::Arguments<'_>) -> io::Result<()> {
|
||||
if args.as_str().map_or(false, |s| s.is_empty()) {
|
||||
writeln!(writer)
|
||||
} else {
|
||||
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||
writeln!(writer, "[{}] {}", timestamp, args)
|
||||
}
|
||||
}
|
||||
10
meteor-edge-client/src/core/mod.rs
Normal file
10
meteor-edge-client/src/core/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
// Core application modules
|
||||
|
||||
pub mod app;
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
pub mod logging;
|
||||
|
||||
pub use app::Application;
|
||||
pub use config::{Config, ConfigManager};
|
||||
pub use events::*;
|
||||
@ -4,10 +4,10 @@ use anyhow::{Result, anyhow};
|
||||
use tokio::sync::{mpsc, RwLock, Mutex};
|
||||
use tokio::time::{sleep, interval, timeout};
|
||||
|
||||
use crate::integrated_system::{IntegratedMemorySystem, SystemConfig, ProcessedFrame};
|
||||
use crate::ring_buffer::AstronomicalFrame;
|
||||
use crate::frame_pool::{PooledFrameBuffer, HierarchicalFramePool};
|
||||
use crate::memory_monitor::SystemMemoryInfo;
|
||||
use crate::monitoring::integrated_system::{IntegratedMemorySystem, SystemConfig, ProcessedFrame};
|
||||
use crate::memory::ring_buffer::AstronomicalFrame;
|
||||
use crate::memory::frame_pool::{PooledFrameBuffer, HierarchicalFramePool};
|
||||
use crate::memory::memory_monitor::SystemMemoryInfo;
|
||||
|
||||
/// Camera integration with memory management system
|
||||
/// Optimized for Raspberry Pi camera modules and astronomical imaging
|
||||
@ -275,7 +275,9 @@ impl CameraMemoryIntegration {
|
||||
let camera_stats = self.get_stats().await;
|
||||
let memory_metrics = self.memory_system.get_metrics().await;
|
||||
let memory_info = SystemMemoryInfo::current().unwrap_or_default();
|
||||
|
||||
|
||||
let recommendations = self.generate_health_recommendations(&camera_stats, &memory_info);
|
||||
|
||||
CameraSystemHealth {
|
||||
camera_status: if camera_stats.error_count == 0 {
|
||||
CameraStatus::Healthy
|
||||
@ -285,7 +287,7 @@ impl CameraMemoryIntegration {
|
||||
camera_stats,
|
||||
memory_metrics,
|
||||
memory_info,
|
||||
recommendations: self.generate_health_recommendations(&camera_stats, &memory_info),
|
||||
recommendations,
|
||||
}
|
||||
}
|
||||
|
||||
@ -353,7 +355,7 @@ impl CameraMemoryIntegration {
|
||||
|
||||
// Note: In a real implementation, we'd need to properly handle the receiver ownership
|
||||
// For this demo, we'll create a new channel pair
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
let (tx, mut rx) = mpsc::channel::<CapturedFrame>(100);
|
||||
|
||||
Ok(tokio::spawn(async move {
|
||||
println!("⚙️ Frame processing loop started");
|
||||
@ -620,7 +622,7 @@ impl CaptureBufferPool {
|
||||
pub struct CameraSystemHealth {
|
||||
pub camera_status: CameraStatus,
|
||||
pub camera_stats: CameraStats,
|
||||
pub memory_metrics: crate::integrated_system::SystemMetrics,
|
||||
pub memory_metrics: crate::monitoring::integrated_system::SystemMetrics,
|
||||
pub memory_info: SystemMemoryInfo,
|
||||
pub recommendations: Vec<String>,
|
||||
}
|
||||
@ -671,7 +673,7 @@ pub fn create_performance_camera_config() -> CameraConfig {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::integrated_system::SystemConfig;
|
||||
use crate::monitoring::integrated_system::SystemConfig;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_camera_integration_creation() {
|
||||
@ -1,8 +1,8 @@
|
||||
use anyhow::{Result, Context};
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::VecDeque;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::events::{EventBus, SystemEvent, FrameCapturedEvent, MeteorDetectedEvent};
|
||||
use crate::core::events::{EventBus, FrameCapturedEvent, MeteorDetectedEvent, SystemEvent};
|
||||
|
||||
/// Configuration for the detection controller
|
||||
#[derive(Debug, Clone)]
|
||||
@ -68,7 +68,10 @@ impl DetectionController {
|
||||
println!("🔍 Starting meteor detection controller...");
|
||||
println!(" Buffer size: {} frames", self.config.buffer_capacity);
|
||||
println!(" Algorithm: {}", self.config.algorithm_name);
|
||||
println!(" Brightness threshold: {}", self.config.brightness_threshold);
|
||||
println!(
|
||||
" Brightness threshold: {}",
|
||||
self.config.brightness_threshold
|
||||
);
|
||||
println!(" Min event frames: {}", self.config.min_event_frames);
|
||||
|
||||
let mut event_receiver = self.event_bus.subscribe();
|
||||
@ -92,7 +95,7 @@ impl DetectionController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Periodic analysis check
|
||||
_ = sleep(check_interval) => {
|
||||
if let Err(e) = self.run_detection_analysis().await {
|
||||
@ -147,8 +150,9 @@ impl DetectionController {
|
||||
self.last_processed_frame_id = frame_event.frame_id;
|
||||
|
||||
if frame_event.frame_id % 50 == 0 {
|
||||
println!("🔍 Processed {} frames, buffer size: {}",
|
||||
frame_event.frame_id,
|
||||
println!(
|
||||
"🔍 Processed {} frames, buffer size: {}",
|
||||
frame_event.frame_id,
|
||||
self.frame_buffer.len()
|
||||
);
|
||||
}
|
||||
@ -160,35 +164,36 @@ impl DetectionController {
|
||||
fn calculate_brightness_score(&self, frame_event: &FrameCapturedEvent) -> Result<f64> {
|
||||
// Simplified brightness calculation based on frame data content
|
||||
// In our synthetic JPEG format, the brightness is encoded in the pixel values
|
||||
|
||||
if frame_event.frame_data.len() < 8 { // Need at least header + some data
|
||||
|
||||
if frame_event.frame_data.len() < 8 {
|
||||
// Need at least header + some data
|
||||
return Ok(0.0);
|
||||
}
|
||||
|
||||
|
||||
// Skip the fake JPEG header (first 4 bytes) and footer (last 2 bytes)
|
||||
let data_start = 4;
|
||||
let frame_data_slice = frame_event.frame_data.as_slice();
|
||||
let data_end = frame_data_slice.len().saturating_sub(2);
|
||||
|
||||
|
||||
if data_start >= data_end {
|
||||
return Ok(0.0);
|
||||
}
|
||||
|
||||
|
||||
// Calculate average pixel value (brightness) from the data section
|
||||
let pixel_data = &frame_data_slice[data_start..data_end];
|
||||
let average_brightness = pixel_data.iter()
|
||||
.map(|&b| b as f64)
|
||||
.sum::<f64>() / pixel_data.len() as f64;
|
||||
|
||||
let average_brightness =
|
||||
pixel_data.iter().map(|&b| b as f64).sum::<f64>() / pixel_data.len() as f64;
|
||||
|
||||
// Normalize to 0.0-1.0 range (assuming 255 is max brightness)
|
||||
let score = (average_brightness / 255.0).min(1.0).max(0.0);
|
||||
|
||||
|
||||
Ok(score)
|
||||
}
|
||||
|
||||
/// Run detection analysis on the frame buffer
|
||||
async fn run_detection_analysis(&mut self) -> Result<()> {
|
||||
if self.frame_buffer.len() < 10 { // Need at least 10 frames for analysis
|
||||
if self.frame_buffer.len() < 10 {
|
||||
// Need at least 10 frames for analysis
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -197,7 +202,10 @@ impl DetectionController {
|
||||
self.run_brightness_diff_detection().await?;
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown detection algorithm: {}", self.config.algorithm_name);
|
||||
eprintln!(
|
||||
"Unknown detection algorithm: {}",
|
||||
self.config.algorithm_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,12 +215,12 @@ impl DetectionController {
|
||||
/// Run brightness difference detection algorithm
|
||||
async fn run_brightness_diff_detection(&mut self) -> Result<()> {
|
||||
let frames: Vec<&StoredFrame> = self.frame_buffer.iter().collect();
|
||||
|
||||
|
||||
// Need at least 10 frames for reliable detection
|
||||
if frames.len() < 10 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
// Calculate average brightness of historical frames (excluding recent ones)
|
||||
let history_end = frames.len().saturating_sub(3); // Exclude last 3 frames
|
||||
if history_end < 5 {
|
||||
@ -222,7 +230,8 @@ impl DetectionController {
|
||||
let historical_avg = frames[..history_end]
|
||||
.iter()
|
||||
.map(|f| f.brightness_score)
|
||||
.sum::<f64>() / history_end as f64;
|
||||
.sum::<f64>()
|
||||
/ history_end as f64;
|
||||
|
||||
// Check recent frames for significant brightness increase
|
||||
for recent_frame in &frames[history_end..] {
|
||||
@ -232,7 +241,7 @@ impl DetectionController {
|
||||
} else {
|
||||
brightness_diff
|
||||
};
|
||||
|
||||
|
||||
// Use relative increase as confidence
|
||||
let confidence = relative_increase.max(0.0).min(1.0);
|
||||
|
||||
@ -256,12 +265,13 @@ impl DetectionController {
|
||||
"brightness_diff_v1".to_string(),
|
||||
);
|
||||
|
||||
println!("🌟 METEOR DETECTED! Frame #{}, Confidence: {:.2}",
|
||||
recent_frame.frame_id,
|
||||
confidence
|
||||
println!(
|
||||
"🌟 METEOR DETECTED! Frame #{}, Confidence: {:.2}",
|
||||
recent_frame.frame_id, confidence
|
||||
);
|
||||
|
||||
self.event_bus.publish_meteor_detected(detection_event)
|
||||
self.event_bus
|
||||
.publish_meteor_detected(detection_event)
|
||||
.context("Failed to publish meteor detection event")?;
|
||||
|
||||
// Prevent duplicate detections for a short period
|
||||
@ -280,7 +290,10 @@ impl DetectionController {
|
||||
buffer_capacity: self.config.buffer_capacity,
|
||||
last_processed_frame_id: self.last_processed_frame_id,
|
||||
avg_brightness: if !self.frame_buffer.is_empty() {
|
||||
self.frame_buffer.iter().map(|f| f.brightness_score).sum::<f64>()
|
||||
self.frame_buffer
|
||||
.iter()
|
||||
.map(|f| f.brightness_score)
|
||||
.sum::<f64>()
|
||||
/ self.frame_buffer.len() as f64
|
||||
} else {
|
||||
0.0
|
||||
@ -301,7 +314,7 @@ pub struct DetectionStats {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::events::EventBus;
|
||||
use crate::core::events::EventBus;
|
||||
|
||||
#[test]
|
||||
fn test_detection_config_default() {
|
||||
@ -317,7 +330,7 @@ mod tests {
|
||||
let config = DetectionConfig::default();
|
||||
let event_bus = EventBus::new(100);
|
||||
let controller = DetectionController::new(config, event_bus);
|
||||
|
||||
|
||||
assert_eq!(controller.frame_buffer.len(), 0);
|
||||
assert_eq!(controller.last_processed_frame_id, 0);
|
||||
}
|
||||
@ -327,11 +340,11 @@ mod tests {
|
||||
let config = DetectionConfig::default();
|
||||
let event_bus = EventBus::new(100);
|
||||
let controller = DetectionController::new(config, event_bus);
|
||||
|
||||
|
||||
let stats = controller.get_stats();
|
||||
assert_eq!(stats.buffer_size, 0);
|
||||
assert_eq!(stats.buffer_capacity, 100);
|
||||
assert_eq!(stats.last_processed_frame_id, 0);
|
||||
assert_eq!(stats.avg_brightness, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,11 +5,11 @@ use anyhow::Result;
|
||||
use tokio::sync::{mpsc, RwLock, Mutex};
|
||||
use tokio::time::{interval, timeout};
|
||||
|
||||
use crate::integrated_system::{IntegratedMemorySystem, ProcessedFrame};
|
||||
use crate::camera_memory_integration::{CameraMemoryIntegration, CapturedFrame, CameraConfig};
|
||||
use crate::ring_buffer::AstronomicalFrame;
|
||||
use crate::frame_pool::PooledFrameBuffer;
|
||||
use crate::hierarchical_cache::EntryMetadata;
|
||||
use crate::monitoring::integrated_system::{IntegratedMemorySystem, ProcessedFrame};
|
||||
use crate::detection::camera_integration::{CameraMemoryIntegration, CapturedFrame, CameraConfig};
|
||||
use crate::memory::ring_buffer::AstronomicalFrame;
|
||||
use crate::memory::frame_pool::PooledFrameBuffer;
|
||||
use crate::memory::hierarchical_cache::EntryMetadata;
|
||||
|
||||
/// Real-time meteor detection pipeline with optimized memory management
|
||||
/// Implements advanced astronomical detection algorithms with zero-copy processing
|
||||
@ -861,7 +861,7 @@ pub fn create_performance_detection_config() -> DetectionConfig {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::integrated_system::SystemConfig;
|
||||
use crate::monitoring::integrated_system::SystemConfig;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_detection_pipeline_creation() {
|
||||
@ -871,7 +871,7 @@ mod tests {
|
||||
let camera_system = Arc::new(
|
||||
CameraMemoryIntegration::new(
|
||||
memory_system.clone(),
|
||||
crate::camera_memory_integration::create_pi_camera_config(),
|
||||
crate::detection::camera_integration::create_pi_camera_config(),
|
||||
).await.unwrap()
|
||||
);
|
||||
|
||||
9
meteor-edge-client/src/detection/mod.rs
Normal file
9
meteor-edge-client/src/detection/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
// Detection modules
|
||||
|
||||
pub mod camera_integration;
|
||||
pub mod detector;
|
||||
pub mod meteor_pipeline;
|
||||
|
||||
pub use camera_integration::*;
|
||||
pub use detector::*;
|
||||
pub use meteor_pipeline::*;
|
||||
@ -1,11 +1,15 @@
|
||||
use anyhow::{Result, Context};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Sha256, Digest};
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use sysinfo::{System, Disks, Networks, Components};
|
||||
use anyhow::{Context, Result};
|
||||
use mac_address::get_mac_address;
|
||||
use tracing::{info, warn, error, debug};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
#[cfg_attr(
|
||||
not(any(target_os = "linux", target_os = "windows")),
|
||||
allow(unused_imports)
|
||||
)]
|
||||
use std::fs;
|
||||
use std::{panic, process::Command};
|
||||
use sysinfo::{Disks, System};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Hardware fingerprint containing unique device identifiers
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -54,7 +58,7 @@ impl HardwareFingerprintService {
|
||||
/// Creates a new hardware fingerprinting service
|
||||
pub fn new() -> Self {
|
||||
let system = System::new_all();
|
||||
|
||||
|
||||
Self {
|
||||
system,
|
||||
cache: None,
|
||||
@ -82,7 +86,8 @@ impl HardwareFingerprintService {
|
||||
let system_info = self.collect_system_info().await?;
|
||||
|
||||
// Compute hash from core identifiers
|
||||
let computed_hash = self.compute_fingerprint_hash(&cpu_id, &board_serial, &mac_addresses, &disk_uuid);
|
||||
let computed_hash =
|
||||
self.compute_fingerprint_hash(&cpu_id, &board_serial, &mac_addresses, &disk_uuid);
|
||||
|
||||
let fingerprint = HardwareFingerprint {
|
||||
cpu_id,
|
||||
@ -96,7 +101,7 @@ impl HardwareFingerprintService {
|
||||
|
||||
// Cache the result
|
||||
self.cache = Some(fingerprint.clone());
|
||||
|
||||
|
||||
info!("Hardware fingerprint generated successfully");
|
||||
debug!("Fingerprint hash: {}", fingerprint.computed_hash);
|
||||
|
||||
@ -106,7 +111,7 @@ impl HardwareFingerprintService {
|
||||
/// Gets CPU identifier
|
||||
async fn get_cpu_id(&self) -> Result<String> {
|
||||
// Try multiple approaches to get a stable CPU identifier
|
||||
|
||||
|
||||
// Method 1: Try /proc/cpuinfo on Linux
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
@ -140,7 +145,8 @@ impl HardwareFingerprintService {
|
||||
{
|
||||
if let Ok(output) = Command::new("system_profiler")
|
||||
.args(&["SPHardwareDataType"])
|
||||
.output() {
|
||||
.output()
|
||||
{
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
for line in output_str.lines() {
|
||||
if line.contains("Hardware UUID:") {
|
||||
@ -157,7 +163,8 @@ impl HardwareFingerprintService {
|
||||
{
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(&["csproduct", "get", "UUID", "/value"])
|
||||
.output() {
|
||||
.output()
|
||||
{
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
for line in output_str.lines() {
|
||||
if line.starts_with("UUID=") {
|
||||
@ -192,7 +199,7 @@ impl HardwareFingerprintService {
|
||||
return Ok(serial.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let Ok(serial) = fs::read_to_string("/sys/devices/virtual/dmi/id/product_serial") {
|
||||
let serial = serial.trim();
|
||||
if !serial.is_empty() && serial != "To be filled by O.E.M." {
|
||||
@ -206,7 +213,8 @@ impl HardwareFingerprintService {
|
||||
{
|
||||
if let Ok(output) = Command::new("system_profiler")
|
||||
.args(&["SPHardwareDataType"])
|
||||
.output() {
|
||||
.output()
|
||||
{
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
for line in output_str.lines() {
|
||||
if line.contains("Serial Number (system):") {
|
||||
@ -223,7 +231,8 @@ impl HardwareFingerprintService {
|
||||
{
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(&["baseboard", "get", "SerialNumber", "/value"])
|
||||
.output() {
|
||||
.output()
|
||||
{
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
for line in output_str.lines() {
|
||||
if line.starts_with("SerialNumber=") {
|
||||
@ -237,9 +246,11 @@ impl HardwareFingerprintService {
|
||||
}
|
||||
|
||||
// Fallback: Generate from system info
|
||||
let fallback_data = format!("board-{}-{}",
|
||||
let fallback_data = format!(
|
||||
"board-{}-{}",
|
||||
System::name().unwrap_or("unknown".to_string()),
|
||||
System::kernel_version().unwrap_or("unknown".to_string()));
|
||||
System::kernel_version().unwrap_or("unknown".to_string())
|
||||
);
|
||||
let hash = Sha256::digest(fallback_data.as_bytes());
|
||||
Ok(format!("board-{}", hex::encode(&hash[..8])))
|
||||
}
|
||||
@ -249,10 +260,30 @@ impl HardwareFingerprintService {
|
||||
let mut mac_addresses = Vec::new();
|
||||
|
||||
// Get primary MAC address
|
||||
if let Ok(Some(mac)) = get_mac_address() {
|
||||
mac_addresses.push(format!("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||
mac.bytes()[0], mac.bytes()[1], mac.bytes()[2],
|
||||
mac.bytes()[3], mac.bytes()[4], mac.bytes()[5]));
|
||||
let primary_mac_result = panic::catch_unwind(|| get_mac_address());
|
||||
match primary_mac_result {
|
||||
Ok(Ok(Some(mac))) => {
|
||||
mac_addresses.push(format!(
|
||||
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||
mac.bytes()[0],
|
||||
mac.bytes()[1],
|
||||
mac.bytes()[2],
|
||||
mac.bytes()[3],
|
||||
mac.bytes()[4],
|
||||
mac.bytes()[5]
|
||||
));
|
||||
}
|
||||
Ok(Ok(None)) => {
|
||||
warn!("Primary MAC address not reported by mac_address crate");
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("Failed to read primary MAC address: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"mac_address::get_mac_address() panicked; falling back to alternative collectors"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get additional MAC addresses
|
||||
@ -276,6 +307,29 @@ impl HardwareFingerprintService {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if mac_addresses.is_empty() {
|
||||
if let Ok(output) = Command::new("networksetup")
|
||||
.args(["-listallhardwareports"])
|
||||
.output()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
if let Some(addr) = line.strip_prefix("Ethernet Address:") {
|
||||
let mac = addr.trim().to_uppercase();
|
||||
if mac != "00:00:00:00:00:00" && !mac.is_empty() {
|
||||
let mac_formatted = mac.replace('-', ":");
|
||||
if !mac_addresses.contains(&mac_formatted) {
|
||||
mac_addresses.push(mac_formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mac_addresses.is_empty() {
|
||||
return Err(anyhow::anyhow!("No valid MAC addresses found"));
|
||||
}
|
||||
@ -294,7 +348,8 @@ impl HardwareFingerprintService {
|
||||
if let Ok(fstab) = fs::read_to_string("/etc/fstab") {
|
||||
for line in fstab.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("UUID=") && (line.contains(" / ") || line.contains("\t/\t")) {
|
||||
if line.starts_with("UUID=") && (line.contains(" / ") || line.contains("\t/\t"))
|
||||
{
|
||||
if let Some(uuid) = line.split_whitespace().next() {
|
||||
let uuid = uuid.replace("UUID=", "");
|
||||
if !uuid.is_empty() {
|
||||
@ -308,7 +363,8 @@ impl HardwareFingerprintService {
|
||||
// Try blkid command
|
||||
if let Ok(output) = Command::new("blkid")
|
||||
.args(&["-s", "UUID", "-o", "value", "/dev/sda1"])
|
||||
.output() {
|
||||
.output()
|
||||
{
|
||||
let uuid = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !uuid.is_empty() {
|
||||
return Ok(uuid);
|
||||
@ -319,9 +375,7 @@ impl HardwareFingerprintService {
|
||||
// Method 2: macOS diskutil
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(output) = Command::new("diskutil")
|
||||
.args(&["info", "/"])
|
||||
.output() {
|
||||
if let Ok(output) = Command::new("diskutil").args(&["info", "/"]).output() {
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
for line in output_str.lines() {
|
||||
if line.contains("Volume UUID:") {
|
||||
@ -337,8 +391,16 @@ impl HardwareFingerprintService {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(&["logicaldisk", "where", "caption=\"C:\"", "get", "VolumeSerialNumber", "/value"])
|
||||
.output() {
|
||||
.args(&[
|
||||
"logicaldisk",
|
||||
"where",
|
||||
"caption=\"C:\"",
|
||||
"get",
|
||||
"VolumeSerialNumber",
|
||||
"/value",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
for line in output_str.lines() {
|
||||
if line.starts_with("VolumeSerialNumber=") {
|
||||
@ -379,8 +441,10 @@ impl HardwareFingerprintService {
|
||||
if fs::metadata("/dev/tpm0").is_ok() || fs::metadata("/dev/tpmrm0").is_ok() {
|
||||
// For demonstration, return a mock attestation
|
||||
// In production, use tss-esapi crate or similar TPM library
|
||||
let mock_attestation = format!("tpm2-mock-{}",
|
||||
hex::encode(Sha256::digest("mock-tpm-attestation".as_bytes())));
|
||||
let mock_attestation = format!(
|
||||
"tpm2-mock-{}",
|
||||
hex::encode(Sha256::digest("mock-tpm-attestation".as_bytes()))
|
||||
);
|
||||
return Ok(base64::encode(mock_attestation));
|
||||
}
|
||||
}
|
||||
@ -390,11 +454,14 @@ impl HardwareFingerprintService {
|
||||
// Check Windows TPM
|
||||
if let Ok(output) = Command::new("powershell")
|
||||
.args(&["-Command", "Get-Tpm"])
|
||||
.output() {
|
||||
.output()
|
||||
{
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
if output_str.contains("TpmPresent") && output_str.contains("True") {
|
||||
let mock_attestation = format!("tpm2-win-{}",
|
||||
hex::encode(Sha256::digest("windows-tpm-attestation".as_bytes())));
|
||||
let mock_attestation = format!(
|
||||
"tpm2-win-{}",
|
||||
hex::encode(Sha256::digest("windows-tpm-attestation".as_bytes()))
|
||||
);
|
||||
return Ok(base64::encode(mock_attestation));
|
||||
}
|
||||
}
|
||||
@ -413,7 +480,7 @@ impl HardwareFingerprintService {
|
||||
let os_name = System::name().unwrap_or("Unknown".to_string());
|
||||
let os_version = System::os_version().unwrap_or("Unknown".to_string());
|
||||
let kernel_version = System::kernel_version().unwrap_or("Unknown".to_string());
|
||||
|
||||
|
||||
let architecture = if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
@ -422,7 +489,8 @@ impl HardwareFingerprintService {
|
||||
"arm"
|
||||
} else {
|
||||
"unknown"
|
||||
}.to_string();
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let total_memory = self.system.total_memory();
|
||||
let available_memory = self.system.available_memory();
|
||||
@ -456,7 +524,13 @@ impl HardwareFingerprintService {
|
||||
}
|
||||
|
||||
/// Computes fingerprint hash from core identifiers
|
||||
fn compute_fingerprint_hash(&self, cpu_id: &str, board_serial: &str, mac_addresses: &[String], disk_uuid: &str) -> String {
|
||||
fn compute_fingerprint_hash(
|
||||
&self,
|
||||
cpu_id: &str,
|
||||
board_serial: &str,
|
||||
mac_addresses: &[String],
|
||||
disk_uuid: &str,
|
||||
) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(cpu_id);
|
||||
hasher.update(board_serial);
|
||||
@ -494,7 +568,7 @@ mod tests {
|
||||
async fn test_fingerprint_generation() {
|
||||
let mut service = HardwareFingerprintService::new();
|
||||
let fingerprint = service.generate_fingerprint().await.unwrap();
|
||||
|
||||
|
||||
assert!(!fingerprint.cpu_id.is_empty());
|
||||
assert!(!fingerprint.board_serial.is_empty());
|
||||
assert!(!fingerprint.mac_addresses.is_empty());
|
||||
@ -508,7 +582,7 @@ mod tests {
|
||||
let mut service = HardwareFingerprintService::new();
|
||||
let fingerprint1 = service.generate_fingerprint().await.unwrap();
|
||||
let fingerprint2 = service.generate_fingerprint().await.unwrap();
|
||||
|
||||
|
||||
// Should be identical (cached)
|
||||
assert_eq!(fingerprint1.computed_hash, fingerprint2.computed_hash);
|
||||
}
|
||||
@ -517,13 +591,13 @@ mod tests {
|
||||
async fn test_fingerprint_validation() {
|
||||
let mut service = HardwareFingerprintService::new();
|
||||
let fingerprint = service.generate_fingerprint().await.unwrap();
|
||||
|
||||
|
||||
assert!(service.validate_fingerprint(&fingerprint).unwrap());
|
||||
|
||||
|
||||
// Test with modified fingerprint
|
||||
let mut invalid_fingerprint = fingerprint.clone();
|
||||
invalid_fingerprint.cpu_id = "modified".to_string();
|
||||
|
||||
|
||||
assert!(!service.validate_fingerprint(&invalid_fingerprint).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
8
meteor-edge-client/src/device/mod.rs
Normal file
8
meteor-edge-client/src/device/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
// Device management modules
|
||||
|
||||
pub mod hardware_fingerprint;
|
||||
pub mod registration;
|
||||
// test_fingerprint*.rs moved to src/ for standalone bin
|
||||
|
||||
pub use hardware_fingerprint::*;
|
||||
pub use registration::{DeviceRegistrationClient, Location, RegistrationConfig};
|
||||
@ -1,13 +1,13 @@
|
||||
use anyhow::{Result, Context, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tokio::time::{sleep, timeout, Instant};
|
||||
use tracing::{info, warn, error, debug};
|
||||
use uuid::Uuid;
|
||||
use sha2::{Sha256, Digest};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use qrcode::QrCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tokio::time::{sleep, timeout};
|
||||
use tracing::{debug, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::hardware_fingerprint::{HardwareFingerprint, HardwareFingerprintService};
|
||||
use crate::device::hardware_fingerprint::{HardwareFingerprint, HardwareFingerprintService};
|
||||
|
||||
/// Device registration states
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -150,12 +150,16 @@ impl DeviceRegistrationClient {
|
||||
self.set_state(RegistrationState::Initializing).await;
|
||||
|
||||
// Initialize hardware fingerprinting
|
||||
let fingerprint = self.fingerprint_service
|
||||
let fingerprint = self
|
||||
.fingerprint_service
|
||||
.generate_fingerprint()
|
||||
.await
|
||||
.context("Failed to generate hardware fingerprint")?;
|
||||
|
||||
info!("Hardware fingerprint generated: {}", fingerprint.computed_hash);
|
||||
info!(
|
||||
"Hardware fingerprint generated: {}",
|
||||
fingerprint.computed_hash
|
||||
);
|
||||
|
||||
// Enter setup mode to wait for user configuration
|
||||
self.set_state(RegistrationState::SetupMode).await;
|
||||
@ -179,8 +183,7 @@ impl DeviceRegistrationClient {
|
||||
});
|
||||
|
||||
let qr_string = serde_json::to_string(&qr_data)?;
|
||||
let qr_code = QrCode::new(&qr_string)
|
||||
.context("Failed to generate QR code")?;
|
||||
let qr_code = QrCode::new(&qr_string).context("Failed to generate QR code")?;
|
||||
|
||||
// Display QR code (for demonstration, save to file)
|
||||
self.save_qr_code_image(&qr_code).await?;
|
||||
@ -226,7 +229,9 @@ impl DeviceRegistrationClient {
|
||||
|
||||
/// Claims the device using the registration token
|
||||
async fn claim_device(&mut self) -> Result<()> {
|
||||
let token = self.registration_token.as_ref()
|
||||
let token = self
|
||||
.registration_token
|
||||
.as_ref()
|
||||
.context("No registration token available")?
|
||||
.clone();
|
||||
|
||||
@ -234,9 +239,7 @@ impl DeviceRegistrationClient {
|
||||
self.set_state(RegistrationState::Claiming).await;
|
||||
|
||||
// Generate fresh fingerprint
|
||||
let fingerprint = self.fingerprint_service
|
||||
.generate_fingerprint()
|
||||
.await?;
|
||||
let fingerprint = self.fingerprint_service.generate_fingerprint().await?;
|
||||
|
||||
// Prepare claim request
|
||||
let claim_request = serde_json::json!({
|
||||
@ -275,7 +278,7 @@ impl DeviceRegistrationClient {
|
||||
|
||||
// Send claim request
|
||||
let url = format!("{}/api/v1/devices/register/claim", token.api_url);
|
||||
|
||||
|
||||
let mut attempts = 0;
|
||||
while attempts < self.config.retry_attempts {
|
||||
attempts += 1;
|
||||
@ -290,7 +293,11 @@ impl DeviceRegistrationClient {
|
||||
if attempts < self.config.retry_attempts {
|
||||
sleep(self.config.retry_delay).await;
|
||||
} else {
|
||||
self.set_state(RegistrationState::Error(format!("Failed to claim device after {} attempts", attempts))).await;
|
||||
self.set_state(RegistrationState::Error(format!(
|
||||
"Failed to claim device after {} attempts",
|
||||
attempts
|
||||
)))
|
||||
.await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@ -301,29 +308,39 @@ impl DeviceRegistrationClient {
|
||||
}
|
||||
|
||||
/// Sends claim request to server
|
||||
async fn send_claim_request(&self, url: &str, request: &serde_json::Value) -> Result<RegistrationChallenge> {
|
||||
async fn send_claim_request(
|
||||
&self,
|
||||
url: &str,
|
||||
request: &serde_json::Value,
|
||||
) -> Result<RegistrationChallenge> {
|
||||
debug!("Sending claim request to: {}", url);
|
||||
|
||||
let response = timeout(Duration::from_secs(30),
|
||||
self.http_client.post(url)
|
||||
.json(request)
|
||||
.send()
|
||||
).await
|
||||
let response = timeout(
|
||||
Duration::from_secs(30),
|
||||
self.http_client.post(url).json(request).send(),
|
||||
)
|
||||
.await
|
||||
.context("Request timeout")?
|
||||
.context("Failed to send claim request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
bail!("Claim request failed: {} - {}", status, error_text);
|
||||
}
|
||||
|
||||
let challenge_response: serde_json::Value = response.json().await
|
||||
let challenge_response: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse challenge response")?;
|
||||
|
||||
// Parse challenge (this would be a proper challenge in production)
|
||||
let challenge = RegistrationChallenge {
|
||||
challenge: challenge_response.get("challenge")
|
||||
challenge: challenge_response
|
||||
.get("challenge")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_else(|| "mock-challenge")
|
||||
.to_string(),
|
||||
@ -337,13 +354,19 @@ impl DeviceRegistrationClient {
|
||||
|
||||
/// Responds to the security challenge
|
||||
async fn respond_to_challenge(&mut self) -> Result<()> {
|
||||
let challenge_str = self.challenge.as_ref()
|
||||
let challenge_str = self
|
||||
.challenge
|
||||
.as_ref()
|
||||
.map(|c| c.challenge.clone())
|
||||
.context("No challenge available")?;
|
||||
let token_str = self.registration_token.as_ref()
|
||||
let token_str = self
|
||||
.registration_token
|
||||
.as_ref()
|
||||
.map(|t| t.claim_token.clone())
|
||||
.context("No registration token available")?;
|
||||
let api_url = self.registration_token.as_ref()
|
||||
let api_url = self
|
||||
.registration_token
|
||||
.as_ref()
|
||||
.map(|t| t.api_url.clone())
|
||||
.context("No API URL available")?;
|
||||
|
||||
@ -362,43 +385,53 @@ impl DeviceRegistrationClient {
|
||||
});
|
||||
|
||||
let url = format!("{}/api/v1/devices/register/confirm", api_url);
|
||||
|
||||
let response = timeout(Duration::from_secs(30),
|
||||
self.http_client.post(&url)
|
||||
.json(&confirm_request)
|
||||
.send()
|
||||
).await
|
||||
|
||||
let response = timeout(
|
||||
Duration::from_secs(30),
|
||||
self.http_client.post(&url).json(&confirm_request).send(),
|
||||
)
|
||||
.await
|
||||
.context("Request timeout")?
|
||||
.context("Failed to send challenge response")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
bail!("Challenge response failed: {} - {}", status, error_text);
|
||||
}
|
||||
|
||||
let credentials_response: serde_json::Value = response.json().await
|
||||
let credentials_response: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse credentials response")?;
|
||||
|
||||
// Parse credentials
|
||||
let credentials = DeviceCredentials {
|
||||
device_id: credentials_response.get("device_id")
|
||||
device_id: credentials_response
|
||||
.get("device_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string(),
|
||||
device_token: credentials_response.get("device_token")
|
||||
device_token: credentials_response
|
||||
.get("device_token")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
certificate_pem: credentials_response.get("device_certificate")
|
||||
certificate_pem: credentials_response
|
||||
.get("device_certificate")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
private_key_pem: credentials_response.get("private_key")
|
||||
private_key_pem: credentials_response
|
||||
.get("private_key")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
ca_certificate_pem: credentials_response.get("ca_certificate")
|
||||
ca_certificate_pem: credentials_response
|
||||
.get("ca_certificate")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
@ -415,14 +448,22 @@ impl DeviceRegistrationClient {
|
||||
self.set_state(RegistrationState::Operational).await;
|
||||
|
||||
info!("Device registration completed successfully!");
|
||||
info!("Device ID: {}", self.credentials.as_ref().unwrap().device_id);
|
||||
info!(
|
||||
"Device ID: {}",
|
||||
self.credentials.as_ref().unwrap().device_id
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates challenge response using device fingerprint
|
||||
fn generate_challenge_response(&self, challenge: &RegistrationChallenge, fingerprint: &HardwareFingerprint) -> Result<String> {
|
||||
let data = format!("{}|{}|{}|{}",
|
||||
fn generate_challenge_response(
|
||||
&self,
|
||||
challenge: &RegistrationChallenge,
|
||||
fingerprint: &HardwareFingerprint,
|
||||
) -> Result<String> {
|
||||
let data = format!(
|
||||
"{}|{}|{}|{}",
|
||||
challenge.challenge,
|
||||
fingerprint.cpu_id,
|
||||
fingerprint.board_serial,
|
||||
@ -443,11 +484,8 @@ impl DeviceRegistrationClient {
|
||||
/// Saves QR code as image file
|
||||
async fn save_qr_code_image(&self, qr_code: &QrCode) -> Result<()> {
|
||||
// Convert QR code to string representation and save as text for demo
|
||||
let qr_string = qr_code.render()
|
||||
.dark_color('#')
|
||||
.light_color(' ')
|
||||
.build();
|
||||
|
||||
let qr_string = qr_code.render().dark_color('#').light_color(' ').build();
|
||||
|
||||
std::fs::write("device_setup_qr.txt", qr_string)
|
||||
.context("Failed to save QR code as text")?;
|
||||
|
||||
@ -476,7 +514,7 @@ impl DeviceRegistrationClient {
|
||||
// Try to reconnect with exponential backoff
|
||||
let mut delay = Duration::from_secs(1);
|
||||
let max_delay = Duration::from_secs(60);
|
||||
|
||||
|
||||
for attempt in 1..=10 {
|
||||
info!("Reconnection attempt {}/10", attempt);
|
||||
|
||||
@ -493,7 +531,10 @@ impl DeviceRegistrationClient {
|
||||
sleep(delay).await;
|
||||
}
|
||||
|
||||
self.set_state(RegistrationState::Error("Failed to reconnect after 10 attempts".to_string())).await;
|
||||
self.set_state(RegistrationState::Error(
|
||||
"Failed to reconnect after 10 attempts".to_string(),
|
||||
))
|
||||
.await;
|
||||
bail!("Network reconnection failed")
|
||||
}
|
||||
|
||||
@ -501,14 +542,19 @@ impl DeviceRegistrationClient {
|
||||
async fn test_network_connectivity(&self) -> Result<bool> {
|
||||
if let Some(token) = &self.registration_token {
|
||||
let url = format!("{}/health", token.api_url);
|
||||
|
||||
|
||||
match timeout(Duration::from_secs(10), self.http_client.get(&url).send()).await {
|
||||
Ok(Ok(response)) => Ok(response.status().is_success()),
|
||||
_ => Ok(false),
|
||||
}
|
||||
} else {
|
||||
// Test with public DNS
|
||||
match timeout(Duration::from_secs(5), self.http_client.get("https://8.8.8.8").send()).await {
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
self.http_client.get("https://8.8.8.8").send(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
@ -583,9 +629,9 @@ mod tests {
|
||||
async fn test_registration_state_machine() {
|
||||
let config = RegistrationConfig::default();
|
||||
let mut client = DeviceRegistrationClient::new(config);
|
||||
|
||||
|
||||
assert_eq!(client.state(), &RegistrationState::Uninitialized);
|
||||
|
||||
|
||||
client.set_state(RegistrationState::Initializing).await;
|
||||
assert_eq!(client.state(), &RegistrationState::Initializing);
|
||||
}
|
||||
@ -594,7 +640,7 @@ mod tests {
|
||||
fn test_pin_generation() {
|
||||
let config = RegistrationConfig::default();
|
||||
let client = DeviceRegistrationClient::new(config);
|
||||
|
||||
|
||||
let pin = client.generate_pin();
|
||||
assert_eq!(pin.len(), 6);
|
||||
assert!(pin.chars().all(|c| c.is_ascii_digit()));
|
||||
@ -604,7 +650,7 @@ mod tests {
|
||||
fn test_challenge_response() {
|
||||
let config = RegistrationConfig::default();
|
||||
let client = DeviceRegistrationClient::new(config);
|
||||
|
||||
|
||||
let challenge = RegistrationChallenge {
|
||||
challenge: "test-challenge".to_string(),
|
||||
algorithm: "SHA256".to_string(),
|
||||
@ -616,7 +662,7 @@ mod tests {
|
||||
board_serial: "test-board".to_string(),
|
||||
mac_addresses: vec!["00:11:22:33:44:55".to_string()],
|
||||
disk_uuid: "test-disk".to_string(),
|
||||
tmp_attestation: None,
|
||||
tpm_attestation: None,
|
||||
system_info: crate::hardware_fingerprint::SystemInfo {
|
||||
hostname: "test".to_string(),
|
||||
os_name: "Linux".to_string(),
|
||||
@ -632,8 +678,10 @@ mod tests {
|
||||
computed_hash: "test-hash".to_string(),
|
||||
};
|
||||
|
||||
let response = client.generate_challenge_response(&challenge, &fingerprint).unwrap();
|
||||
let response = client
|
||||
.generate_challenge_response(&challenge, &fingerprint)
|
||||
.unwrap();
|
||||
assert!(!response.is_empty());
|
||||
assert_eq!(response.len(), 64); // SHA256 hex length
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,443 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tracing::{info, warn, error, debug};
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry, Layer};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Standardized log entry structure that matches backend services
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub level: String,
|
||||
pub service_name: String,
|
||||
pub correlation_id: Option<String>,
|
||||
pub message: String,
|
||||
#[serde(flatten)]
|
||||
pub fields: serde_json::Map<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Configuration for the logging system
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoggingConfig {
|
||||
pub log_directory: PathBuf,
|
||||
pub service_name: String,
|
||||
pub device_id: String,
|
||||
pub max_file_size: u64,
|
||||
pub rotation: Rotation,
|
||||
pub log_level: String,
|
||||
}
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
fn default() -> Self {
|
||||
let log_dir = dirs::data_local_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("meteor-edge-client")
|
||||
.join("logs");
|
||||
|
||||
Self {
|
||||
log_directory: log_dir,
|
||||
service_name: "meteor-edge-client".to_string(),
|
||||
device_id: "unknown".to_string(),
|
||||
max_file_size: 50 * 1024 * 1024, // 50MB
|
||||
rotation: Rotation::HOURLY,
|
||||
log_level: "info".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom JSON formatter for structured logging
|
||||
struct JsonFormatter {
|
||||
service_name: String,
|
||||
device_id: String,
|
||||
}
|
||||
|
||||
impl JsonFormatter {
|
||||
fn new(service_name: String, device_id: String) -> Self {
|
||||
Self {
|
||||
service_name,
|
||||
device_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the structured logging system
|
||||
pub async fn init_logging(config: LoggingConfig) -> Result<()> {
|
||||
// Ensure log directory exists
|
||||
fs::create_dir_all(&config.log_directory).await?;
|
||||
|
||||
// Create rolling file appender
|
||||
let file_appender = RollingFileAppender::new(
|
||||
config.rotation,
|
||||
&config.log_directory,
|
||||
"meteor-edge-client.log",
|
||||
);
|
||||
|
||||
// Create JSON layer for file output
|
||||
let file_layer = fmt::layer()
|
||||
.json()
|
||||
.with_current_span(false)
|
||||
.with_span_list(false)
|
||||
.with_writer(file_appender)
|
||||
.with_filter(EnvFilter::try_new(&config.log_level).unwrap_or_else(|_| EnvFilter::new("info")));
|
||||
|
||||
// Create console layer for development
|
||||
let console_layer = fmt::layer()
|
||||
.pretty()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_filter(EnvFilter::try_new("debug").unwrap_or_else(|_| EnvFilter::new("info")));
|
||||
|
||||
// Initialize the subscriber
|
||||
Registry::default()
|
||||
.with(file_layer)
|
||||
.with(console_layer)
|
||||
.init();
|
||||
|
||||
info!(
|
||||
service_name = %config.service_name,
|
||||
device_id = %config.device_id,
|
||||
log_directory = %config.log_directory.display(),
|
||||
"Structured logging initialized"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Structured logger for the edge client
|
||||
#[derive(Clone)]
|
||||
pub struct StructuredLogger {
|
||||
service_name: String,
|
||||
device_id: String,
|
||||
}
|
||||
|
||||
impl StructuredLogger {
|
||||
pub fn new(service_name: String, device_id: String) -> Self {
|
||||
Self {
|
||||
service_name,
|
||||
device_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an info message with structured fields
|
||||
pub fn info(&self, message: &str, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
/// Log a warning message with structured fields
|
||||
pub fn warn(&self, message: &str, correlation_id: Option<&str>) {
|
||||
warn!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
/// Log an error message with structured fields
|
||||
pub fn error(&self, message: &str, error: Option<&dyn std::error::Error>, correlation_id: Option<&str>) {
|
||||
error!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
error = error.map(|e| e.to_string()).as_deref(),
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
/// Log a debug message with structured fields
|
||||
pub fn debug(&self, message: &str, correlation_id: Option<&str>) {
|
||||
debug!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
/// Log camera-related events
|
||||
pub fn camera_event(&self, event: &str, camera_id: &str, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
camera_id = camera_id,
|
||||
camera_event = event,
|
||||
"Camera event: {}",
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
/// Log detection-related events
|
||||
pub fn detection_event(&self, detection_type: &str, confidence: f64, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
detection_type = detection_type,
|
||||
confidence = confidence,
|
||||
"Detection event: {} (confidence: {:.2})",
|
||||
detection_type,
|
||||
confidence
|
||||
);
|
||||
}
|
||||
|
||||
/// Log storage-related events
|
||||
pub fn storage_event(&self, operation: &str, file_path: &str, file_size: Option<u64>, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
storage_operation = operation,
|
||||
file_path = file_path,
|
||||
file_size = file_size,
|
||||
"Storage event: {}",
|
||||
operation
|
||||
);
|
||||
}
|
||||
|
||||
/// Log communication-related events
|
||||
pub fn communication_event(&self, operation: &str, endpoint: &str, status_code: Option<u16>, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
communication_operation = operation,
|
||||
endpoint = endpoint,
|
||||
status_code = status_code,
|
||||
"Communication event: {}",
|
||||
operation
|
||||
);
|
||||
}
|
||||
|
||||
/// Log hardware-related events
|
||||
pub fn hardware_event(&self, component: &str, event: &str, temperature: Option<f64>, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
hardware_component = component,
|
||||
hardware_event = event,
|
||||
temperature = temperature,
|
||||
"Hardware event: {} - {}",
|
||||
component,
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
/// Log configuration-related events
|
||||
pub fn config_event(&self, operation: &str, config_key: &str, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
config_operation = operation,
|
||||
config_key = config_key,
|
||||
"Configuration event: {}",
|
||||
operation
|
||||
);
|
||||
}
|
||||
|
||||
/// Log startup events
|
||||
pub fn startup_event(&self, component: &str, version: &str, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
startup_component = component,
|
||||
version = version,
|
||||
"Component started: {} v{}",
|
||||
component,
|
||||
version
|
||||
);
|
||||
}
|
||||
|
||||
/// Log shutdown events
|
||||
pub fn shutdown_event(&self, component: &str, reason: &str, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
shutdown_component = component,
|
||||
shutdown_reason = reason,
|
||||
"Component shutdown: {} - {}",
|
||||
component,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
/// Log performance metrics
|
||||
pub fn performance_event(&self, operation: &str, duration_ms: u64, correlation_id: Option<&str>) {
|
||||
info!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
performance_operation = operation,
|
||||
duration_ms = duration_ms,
|
||||
"Performance: {} completed in {}ms",
|
||||
operation,
|
||||
duration_ms
|
||||
);
|
||||
}
|
||||
|
||||
/// Log security-related events
|
||||
pub fn security_event(&self, event: &str, severity: &str, correlation_id: Option<&str>) {
|
||||
warn!(
|
||||
service_name = %self.service_name,
|
||||
device_id = %self.device_id,
|
||||
correlation_id = correlation_id,
|
||||
security_event = event,
|
||||
severity = severity,
|
||||
"Security event: {} (severity: {})",
|
||||
event,
|
||||
severity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility functions for log file management
|
||||
pub struct LogFileManager {
|
||||
log_directory: PathBuf,
|
||||
}
|
||||
|
||||
impl LogFileManager {
|
||||
pub fn new(log_directory: PathBuf) -> Self {
|
||||
Self { log_directory }
|
||||
}
|
||||
|
||||
/// Get all log files in the directory
|
||||
pub async fn get_log_files(&self) -> Result<Vec<PathBuf>> {
|
||||
let mut log_files = Vec::new();
|
||||
let mut entries = fs::read_dir(&self.log_directory).await?;
|
||||
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if let Some(extension) = path.extension() {
|
||||
if extension == "log" {
|
||||
log_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by modification time (oldest first)
|
||||
log_files.sort_by_key(|path| {
|
||||
std::fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
});
|
||||
|
||||
Ok(log_files)
|
||||
}
|
||||
|
||||
/// Get log files that are ready for upload (older than current hour)
|
||||
pub async fn get_uploadable_log_files(&self) -> Result<Vec<PathBuf>> {
|
||||
let all_files = self.get_log_files().await?;
|
||||
let mut uploadable_files = Vec::new();
|
||||
|
||||
let current_time = std::time::SystemTime::now();
|
||||
let one_hour_ago = current_time - std::time::Duration::from_secs(3600);
|
||||
|
||||
for file_path in all_files {
|
||||
// Skip the current active log file (usually the most recently modified)
|
||||
if let Ok(metadata) = std::fs::metadata(&file_path) {
|
||||
if let Ok(modified_time) = metadata.modified() {
|
||||
// Only upload files that are older than 1 hour
|
||||
if modified_time < one_hour_ago {
|
||||
uploadable_files.push(file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(uploadable_files)
|
||||
}
|
||||
|
||||
/// Compress a log file using gzip
|
||||
pub async fn compress_log_file(&self, file_path: &PathBuf) -> Result<PathBuf> {
|
||||
use flate2::{write::GzEncoder, Compression};
|
||||
use std::io::Write;
|
||||
|
||||
let file_content = fs::read(file_path).await?;
|
||||
let compressed_path = file_path.with_extension("log.gz");
|
||||
|
||||
let compressed_data = tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
||||
encoder.write_all(&file_content)?;
|
||||
Ok(encoder.finish()?)
|
||||
}).await??;
|
||||
|
||||
fs::write(&compressed_path, compressed_data).await?;
|
||||
Ok(compressed_path)
|
||||
}
|
||||
|
||||
/// Remove a log file
|
||||
pub async fn remove_log_file(&self, file_path: &PathBuf) -> Result<()> {
|
||||
fs::remove_file(file_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get total size of all log files
|
||||
pub async fn get_total_log_size(&self) -> Result<u64> {
|
||||
let log_files = self.get_log_files().await?;
|
||||
let mut total_size = 0;
|
||||
|
||||
for file_path in log_files {
|
||||
if let Ok(metadata) = std::fs::metadata(&file_path) {
|
||||
total_size += metadata.len();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_size)
|
||||
}
|
||||
|
||||
/// Clean up old log files if total size exceeds limit
|
||||
pub async fn cleanup_old_logs(&self, max_total_size: u64) -> Result<()> {
|
||||
let total_size = self.get_total_log_size().await?;
|
||||
|
||||
if total_size <= max_total_size {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let log_files = self.get_log_files().await?;
|
||||
let mut current_size = total_size;
|
||||
|
||||
// Remove oldest files until we're under the limit
|
||||
for file_path in log_files {
|
||||
if current_size <= max_total_size {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(metadata) = std::fs::metadata(&file_path) {
|
||||
let file_size = metadata.len();
|
||||
self.remove_log_file(&file_path).await?;
|
||||
current_size -= file_size;
|
||||
|
||||
debug!(
|
||||
"Removed old log file: {} (size: {} bytes)",
|
||||
file_path.display(),
|
||||
file_size
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a correlation ID for request tracing
|
||||
pub fn generate_correlation_id() -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
@ -1,25 +1,43 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use anyhow::Result;
|
||||
|
||||
mod config;
|
||||
mod api;
|
||||
mod frame_data;
|
||||
mod frame_pool;
|
||||
mod memory_monitor;
|
||||
mod events;
|
||||
mod app;
|
||||
// Module declarations
|
||||
mod camera;
|
||||
mod core;
|
||||
mod detection;
|
||||
mod device;
|
||||
mod memory;
|
||||
mod monitoring;
|
||||
mod network;
|
||||
mod storage;
|
||||
mod communication;
|
||||
mod hardware_fingerprint;
|
||||
mod device_registration;
|
||||
mod websocket_client;
|
||||
mod tests;
|
||||
|
||||
use config::{Config, ConfigManager};
|
||||
use api::ApiClient;
|
||||
use app::Application;
|
||||
use hardware_fingerprint::HardwareFingerprintService;
|
||||
// Re-export logging for macros
|
||||
use core::logging;
|
||||
|
||||
macro_rules! println {
|
||||
() => {
|
||||
$crate::core::logging::println_with_timestamp(format_args!(""))
|
||||
};
|
||||
($($arg:tt)*) => {
|
||||
$crate::core::logging::println_with_timestamp(format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! eprintln {
|
||||
() => {
|
||||
$crate::core::logging::eprintln_with_timestamp(format_args!(""))
|
||||
};
|
||||
($($arg:tt)*) => {
|
||||
$crate::core::logging::eprintln_with_timestamp(format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
// Import commonly used types
|
||||
use core::{Application, Config, ConfigManager};
|
||||
use device::HardwareFingerprintService;
|
||||
use network::ApiClient;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "meteor-edge-client")]
|
||||
@ -65,7 +83,23 @@ enum Commands {
|
||||
api_url: String,
|
||||
},
|
||||
/// Run the edge client application
|
||||
Run,
|
||||
Run {
|
||||
/// Camera specification (e.g., "device:0", "hw:1")
|
||||
#[arg(long, default_value = "device:0")]
|
||||
camera: String,
|
||||
|
||||
/// Configuration file path
|
||||
#[arg(long)]
|
||||
config: Option<String>,
|
||||
|
||||
/// Enable debug mode
|
||||
#[arg(long)]
|
||||
debug: bool,
|
||||
|
||||
/// Allow running without registration (auto-generates offline config)
|
||||
#[arg(long)]
|
||||
offline: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@ -82,8 +116,15 @@ async fn main() -> Result<()> {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Commands::RegisterDevice { api_url, device_name, location } => {
|
||||
if let Err(e) = register_device_interactive(api_url.clone(), device_name.clone(), location.clone()).await {
|
||||
Commands::RegisterDevice {
|
||||
api_url,
|
||||
device_name,
|
||||
location,
|
||||
} => {
|
||||
if let Err(e) =
|
||||
register_device_interactive(api_url.clone(), device_name.clone(), location.clone())
|
||||
.await
|
||||
{
|
||||
eprintln!("❌ Device registration failed: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@ -103,23 +144,30 @@ async fn main() -> Result<()> {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Commands::Run => {
|
||||
if let Err(e) = run_application().await {
|
||||
Commands::Run {
|
||||
camera,
|
||||
config,
|
||||
debug,
|
||||
offline,
|
||||
} => {
|
||||
if let Err(e) =
|
||||
run_application_with_camera(camera.clone(), config.clone(), *debug, *offline).await
|
||||
{
|
||||
eprintln!("❌ Application failed: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Registers the device with the backend using the provided JWT token
|
||||
async fn register_device(jwt_token: String, api_url: String) -> Result<()> {
|
||||
println!("🚀 Starting device registration process...");
|
||||
|
||||
|
||||
let config_manager = ConfigManager::new();
|
||||
|
||||
|
||||
// Check if device is already registered
|
||||
if config_manager.config_exists() {
|
||||
match config_manager.load_config() {
|
||||
@ -137,31 +185,33 @@ async fn register_device(jwt_token: String, api_url: String) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
println!("📝 Found existing config file, but device not fully registered. Continuing...");
|
||||
println!(
|
||||
"📝 Found existing config file, but device not fully registered. Continuing..."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get hardware ID
|
||||
println!("🔍 Reading hardware identifier...");
|
||||
let mut fingerprint_service = HardwareFingerprintService::new();
|
||||
let fingerprint = fingerprint_service.generate_fingerprint().await?;
|
||||
let hardware_id = fingerprint.computed_hash[..16].to_string();
|
||||
println!(" Hardware ID: {}", hardware_id);
|
||||
|
||||
|
||||
// Create API client
|
||||
let api_client = ApiClient::new(api_url);
|
||||
|
||||
|
||||
// Verify backend connectivity first
|
||||
println!("🏥 Checking backend connectivity...");
|
||||
api_client.health_check().await?;
|
||||
|
||||
|
||||
// Attempt registration
|
||||
println!("📡 Registering device with backend...");
|
||||
let registration_response = api_client
|
||||
.register_device(hardware_id.clone(), jwt_token.clone())
|
||||
.await?;
|
||||
|
||||
|
||||
// Save configuration
|
||||
println!("💾 Saving registration configuration...");
|
||||
let mut config = Config::new(hardware_id);
|
||||
@ -170,13 +220,13 @@ async fn register_device(jwt_token: String, api_url: String) -> Result<()> {
|
||||
registration_response.device.id,
|
||||
jwt_token,
|
||||
);
|
||||
|
||||
|
||||
config_manager.save_config(&config)?;
|
||||
|
||||
|
||||
println!("🎉 Device registration completed successfully!");
|
||||
println!(" Device ID: {}", config.device_id);
|
||||
println!(" Config saved to: {:?}", config_manager.get_config_path());
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -184,12 +234,15 @@ async fn register_device(jwt_token: String, api_url: String) -> Result<()> {
|
||||
async fn show_status() -> Result<()> {
|
||||
println!("📊 Meteor Edge Client Status");
|
||||
println!("============================");
|
||||
|
||||
|
||||
// Show hardware information using the new fingerprint service
|
||||
let mut fingerprint_service = HardwareFingerprintService::new();
|
||||
match fingerprint_service.generate_fingerprint().await {
|
||||
Ok(fingerprint) => {
|
||||
println!("🔧 Hardware ID: {}", fingerprint.computed_hash[..16].to_string());
|
||||
println!(
|
||||
"🔧 Hardware ID: {}",
|
||||
fingerprint.computed_hash[..16].to_string()
|
||||
);
|
||||
println!(" CPU ID: {}", fingerprint.cpu_id);
|
||||
println!(" Board Serial: {}", fingerprint.board_serial);
|
||||
println!(" MAC Addresses: {}", fingerprint.mac_addresses.join(", "));
|
||||
@ -198,11 +251,11 @@ async fn show_status() -> Result<()> {
|
||||
println!("❌ Could not read hardware fingerprint: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show configuration status
|
||||
let config_manager = ConfigManager::new();
|
||||
println!("📁 Config file: {:?}", config_manager.get_config_path());
|
||||
|
||||
|
||||
if config_manager.config_exists() {
|
||||
match config_manager.load_config() {
|
||||
Ok(config) => {
|
||||
@ -229,17 +282,17 @@ async fn show_status() -> Result<()> {
|
||||
println!(" No configuration file found");
|
||||
println!(" Use 'register <token>' command to register this device");
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks backend health and connectivity
|
||||
async fn check_health(api_url: String) -> Result<()> {
|
||||
println!("🏥 Checking backend health at: {}", api_url);
|
||||
|
||||
|
||||
let api_client = ApiClient::new(api_url);
|
||||
api_client.health_check().await?;
|
||||
|
||||
|
||||
println!("✅ Backend is healthy and reachable!");
|
||||
Ok(())
|
||||
}
|
||||
@ -261,34 +314,41 @@ async fn run_application() -> Result<()> {
|
||||
}
|
||||
|
||||
println!("🎯 Initializing Meteor Edge Client...");
|
||||
|
||||
|
||||
// Create the application
|
||||
let mut app = Application::new(1000);
|
||||
|
||||
|
||||
println!("📊 Application Statistics:");
|
||||
println!(" Event Bus Capacity: 1000");
|
||||
println!(" Initial Subscribers: {}", app.subscriber_count());
|
||||
|
||||
|
||||
// Run the application
|
||||
app.run().await
|
||||
}
|
||||
|
||||
/// Interactive device registration process
|
||||
async fn register_device_interactive(api_url: String, device_name: Option<String>, location: Option<String>) -> Result<()> {
|
||||
use device_registration::{DeviceRegistrationClient, RegistrationConfig, Location};
|
||||
async fn register_device_interactive(
|
||||
api_url: String,
|
||||
device_name: Option<String>,
|
||||
location: Option<String>,
|
||||
) -> Result<()> {
|
||||
use device::{DeviceRegistrationClient, Location, RegistrationConfig};
|
||||
|
||||
println!("🚀 Starting interactive device registration...");
|
||||
|
||||
|
||||
// Parse location if provided
|
||||
let parsed_location = if let Some(loc_str) = location {
|
||||
let coords: Vec<&str> = loc_str.split(',').collect();
|
||||
if coords.len() >= 2 {
|
||||
if let (Ok(lat), Ok(lon)) = (coords[0].trim().parse::<f64>(), coords[1].trim().parse::<f64>()) {
|
||||
Some(Location {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
altitude: None,
|
||||
accuracy: None
|
||||
if let (Ok(lat), Ok(lon)) = (
|
||||
coords[0].trim().parse::<f64>(),
|
||||
coords[1].trim().parse::<f64>(),
|
||||
) {
|
||||
Some(Location {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
altitude: None,
|
||||
accuracy: None,
|
||||
})
|
||||
} else {
|
||||
eprintln!("⚠️ Invalid location format. Use: latitude,longitude");
|
||||
@ -320,22 +380,22 @@ async fn register_device_interactive(api_url: String, device_name: Option<String
|
||||
match client.start_registration().await {
|
||||
Ok(()) => {
|
||||
println!("🎉 Device registration completed successfully!");
|
||||
|
||||
|
||||
if let Some(credentials) = client.credentials() {
|
||||
println!(" Device ID: {}", credentials.device_id);
|
||||
println!(" Token: {}...", &credentials.device_token[..20]);
|
||||
println!(" Certificate generated and stored");
|
||||
|
||||
|
||||
// Save credentials to config file
|
||||
let config_data = client.export_config()?;
|
||||
let config_path = dirs::config_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join("meteor-edge-client")
|
||||
.join("registration.json");
|
||||
|
||||
|
||||
std::fs::create_dir_all(config_path.parent().unwrap())?;
|
||||
std::fs::write(&config_path, serde_json::to_string_pretty(&config_data)?)?;
|
||||
|
||||
|
||||
println!(" Configuration saved to: {:?}", config_path);
|
||||
}
|
||||
}
|
||||
@ -350,13 +410,13 @@ async fn register_device_interactive(api_url: String, device_name: Option<String
|
||||
|
||||
/// Tests hardware fingerprinting functionality
|
||||
async fn test_hardware_fingerprint() -> Result<()> {
|
||||
use hardware_fingerprint::HardwareFingerprintService;
|
||||
use device::HardwareFingerprintService;
|
||||
|
||||
println!("🔍 Testing hardware fingerprinting...");
|
||||
|
||||
|
||||
let mut service = HardwareFingerprintService::new();
|
||||
let fingerprint = service.generate_fingerprint().await?;
|
||||
|
||||
|
||||
println!("✅ Hardware fingerprint generated successfully!");
|
||||
println!("");
|
||||
println!("Hardware Information:");
|
||||
@ -370,21 +430,30 @@ async fn test_hardware_fingerprint() -> Result<()> {
|
||||
println!(" TPM Attestation: Not available");
|
||||
}
|
||||
println!(" Computed Hash: {}", fingerprint.computed_hash);
|
||||
|
||||
|
||||
println!("");
|
||||
println!("System Information:");
|
||||
println!(" Hostname: {}", fingerprint.system_info.hostname);
|
||||
println!(" OS: {} {}", fingerprint.system_info.os_name, fingerprint.system_info.os_version);
|
||||
println!(
|
||||
" OS: {} {}",
|
||||
fingerprint.system_info.os_name, fingerprint.system_info.os_version
|
||||
);
|
||||
println!(" Kernel: {}", fingerprint.system_info.kernel_version);
|
||||
println!(" Architecture: {}", fingerprint.system_info.architecture);
|
||||
println!(" Memory: {} MB total, {} MB available",
|
||||
println!(
|
||||
" Memory: {} MB total, {} MB available",
|
||||
fingerprint.system_info.total_memory / 1024 / 1024,
|
||||
fingerprint.system_info.available_memory / 1024 / 1024);
|
||||
println!(" CPU: {} cores, {}",
|
||||
fingerprint.system_info.cpu_count,
|
||||
fingerprint.system_info.cpu_brand);
|
||||
println!(" Disks: {} mounted", fingerprint.system_info.disk_info.len());
|
||||
|
||||
fingerprint.system_info.available_memory / 1024 / 1024
|
||||
);
|
||||
println!(
|
||||
" CPU: {} cores, {}",
|
||||
fingerprint.system_info.cpu_count, fingerprint.system_info.cpu_brand
|
||||
);
|
||||
println!(
|
||||
" Disks: {} mounted",
|
||||
fingerprint.system_info.disk_info.len()
|
||||
);
|
||||
|
||||
// Test fingerprint validation
|
||||
println!("");
|
||||
println!("🔐 Testing fingerprint validation...");
|
||||
@ -394,7 +463,7 @@ async fn test_hardware_fingerprint() -> Result<()> {
|
||||
} else {
|
||||
println!("❌ Fingerprint validation: FAILED");
|
||||
}
|
||||
|
||||
|
||||
// Test consistency
|
||||
println!("");
|
||||
println!("🔄 Testing fingerprint consistency...");
|
||||
@ -406,6 +475,157 @@ async fn test_hardware_fingerprint() -> Result<()> {
|
||||
println!(" First: {}", fingerprint.computed_hash);
|
||||
println!(" Second: {}", fingerprint2.computed_hash);
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the application with specified camera
|
||||
async fn run_application_with_camera(
|
||||
camera_spec_input: String,
|
||||
_config_path: Option<String>,
|
||||
debug: bool,
|
||||
offline: bool,
|
||||
) -> Result<()> {
|
||||
println!("🚀 Starting Meteor Edge Client...");
|
||||
|
||||
if debug {
|
||||
println!("🐛 Debug mode enabled");
|
||||
}
|
||||
|
||||
let camera_spec = normalize_camera_spec(&camera_spec_input);
|
||||
|
||||
// Load configuration
|
||||
let config_manager = ConfigManager::new();
|
||||
let video_requested = is_video_file_spec(&camera_spec);
|
||||
let offline_mode = offline || video_requested;
|
||||
|
||||
if offline_mode {
|
||||
if !config_manager.config_exists() {
|
||||
println!(
|
||||
"⚙️ No device configuration found. Generating offline profile for testing..."
|
||||
);
|
||||
}
|
||||
config_manager.ensure_offline_config(Some(&camera_spec))?;
|
||||
} else if !config_manager.config_exists() {
|
||||
eprintln!("❌ Device not registered. Use 'register <token>' command first.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut existing = config_manager.load_config()?;
|
||||
if !existing.registered {
|
||||
if offline_mode {
|
||||
println!("⚙️ Found unregistered config. Promoting to offline development profile...");
|
||||
config_manager.ensure_offline_config(Some(&camera_spec))?;
|
||||
existing = config_manager.load_config()?;
|
||||
} else {
|
||||
eprintln!("❌ Device not registered. Use 'register <token>' command first.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if offline_mode {
|
||||
println!(
|
||||
"ℹ️ Running in offline mode with device id: {}",
|
||||
existing.device_id
|
||||
);
|
||||
}
|
||||
|
||||
// Display camera specification
|
||||
println!("📷 Camera specification: {}", camera_spec);
|
||||
|
||||
// Validate camera specification before starting
|
||||
use crate::camera::{print_available_cameras, CameraFactory};
|
||||
let frame_pool = std::sync::Arc::new(crate::memory::frame_pool::HierarchicalFramePool::new(20));
|
||||
let factory = CameraFactory::new(frame_pool);
|
||||
|
||||
let camera_config = match factory.config_from_spec(&camera_spec) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("❌ Invalid camera specification: {}", e);
|
||||
eprintln!("\nAvailable camera specifications:");
|
||||
print_available_cameras();
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Create and run application
|
||||
let mut app = Application::new(1000);
|
||||
app.set_camera_override(camera_config);
|
||||
app.run().await
|
||||
}
|
||||
|
||||
fn is_video_file_spec(spec: &str) -> bool {
|
||||
let trimmed = spec.trim();
|
||||
trimmed.starts_with("file:")
|
||||
|| trimmed.starts_with("video:")
|
||||
|| trimmed.starts_with("sim:file:")
|
||||
|| trimmed.contains('/')
|
||||
|| trimmed.contains('\\')
|
||||
|| trimmed.ends_with(".mp4")
|
||||
|| trimmed.ends_with(".mov")
|
||||
|| trimmed.ends_with(".mkv")
|
||||
|| trimmed.ends_with(".avi")
|
||||
}
|
||||
|
||||
fn normalize_camera_spec(spec: &str) -> String {
|
||||
let trimmed = spec.trim();
|
||||
|
||||
if let Some(path) = trimmed.strip_prefix("sim:file:") {
|
||||
let normalized = normalize_path(path);
|
||||
return format!("file:{}", normalized);
|
||||
}
|
||||
|
||||
if let Some(path) = trimmed.strip_prefix("file:") {
|
||||
let normalized = normalize_path(path);
|
||||
return format!("file:{}", normalized);
|
||||
}
|
||||
|
||||
if let Some(path) = trimmed.strip_prefix("video:") {
|
||||
let normalized = normalize_path(path);
|
||||
return format!("file:{}", normalized);
|
||||
}
|
||||
|
||||
if is_video_file_spec(trimmed) {
|
||||
return format!("file:{}", normalize_path(trimmed));
|
||||
}
|
||||
|
||||
trimmed.to_string()
|
||||
}
|
||||
|
||||
fn normalize_path(path: &str) -> String {
|
||||
let candidate = PathBuf::from(path);
|
||||
if candidate.is_absolute() {
|
||||
return candidate.to_string_lossy().to_string();
|
||||
}
|
||||
|
||||
let mut search_roots: Vec<PathBuf> = Vec::new();
|
||||
|
||||
if let Ok(dir) = env::current_dir() {
|
||||
search_roots.push(dir);
|
||||
}
|
||||
|
||||
if let Ok(pwd) = env::var("PWD") {
|
||||
search_roots.push(PathBuf::from(pwd));
|
||||
}
|
||||
|
||||
search_roots.push(PathBuf::from(env!("CARGO_MANIFEST_DIR")));
|
||||
if let Some(parent) = PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent() {
|
||||
search_roots.push(parent.to_path_buf());
|
||||
}
|
||||
|
||||
for root in &search_roots {
|
||||
let joined = root.join(&candidate);
|
||||
if joined.exists() {
|
||||
return joined.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to current dir join even if it doesn't exist (better than mutating path further)
|
||||
search_roots
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(candidate)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ use std::time::{Duration, Instant};
|
||||
use std::collections::VecDeque;
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::frame_pool::{HierarchicalFramePool, FramePool, FramePoolStats};
|
||||
use crate::memory_monitor::{SystemMemoryInfo, MemoryStats, GLOBAL_MEMORY_MONITOR};
|
||||
use crate::memory::frame_pool::{HierarchicalFramePool, FramePool, FramePoolStats};
|
||||
use crate::memory::memory_monitor::{SystemMemoryInfo, MemoryStats, GLOBAL_MEMORY_MONITOR};
|
||||
|
||||
/// Adaptive pool management configuration
|
||||
#[derive(Debug, Clone)]
|
||||
@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
use bytes::Bytes;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Zero-copy frame data with reference counting
|
||||
/// This structure eliminates memory copying by using Arc for shared ownership
|
||||
@ -44,7 +44,7 @@ impl FrameData {
|
||||
timestamp: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create frame data from existing Bytes (zero-copy)
|
||||
pub fn from_bytes(data: Bytes, width: u32, height: u32, format: FrameFormat) -> Self {
|
||||
Self {
|
||||
@ -55,28 +55,28 @@ impl FrameData {
|
||||
timestamp: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get reference to frame data as slice
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
|
||||
/// Create zero-copy slice of frame data
|
||||
/// This operation doesn't allocate new memory
|
||||
pub fn slice(&self, start: usize, end: usize) -> Bytes {
|
||||
self.data.slice(start..end)
|
||||
}
|
||||
|
||||
|
||||
/// Get frame data size in bytes
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
|
||||
/// Check if frame data is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
|
||||
|
||||
/// Calculate expected frame size for given dimensions and format
|
||||
pub fn calculate_expected_size(width: u32, height: u32, format: &FrameFormat) -> usize {
|
||||
match format {
|
||||
@ -86,7 +86,7 @@ impl FrameData {
|
||||
FrameFormat::H264Frame => (width * height / 2) as usize, // Estimate for H.264
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get frame metadata
|
||||
pub fn metadata(&self) -> FrameMetadata {
|
||||
FrameMetadata {
|
||||
@ -126,10 +126,10 @@ pub type SharedFrameData = Arc<FrameData>;
|
||||
|
||||
/// Helper function to create shared frame data
|
||||
pub fn create_shared_frame(
|
||||
data: Vec<u8>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: FrameFormat
|
||||
data: Vec<u8>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: FrameFormat,
|
||||
) -> SharedFrameData {
|
||||
Arc::new(FrameData::new(data, width, height, format))
|
||||
}
|
||||
@ -137,54 +137,57 @@ pub fn create_shared_frame(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_frame_data_creation() {
|
||||
let data = vec![128u8; 640 * 480 * 3];
|
||||
let frame = FrameData::new(data.clone(), 640, 480, FrameFormat::RGB888);
|
||||
|
||||
|
||||
assert_eq!(frame.width, 640);
|
||||
assert_eq!(frame.height, 480);
|
||||
assert_eq!(frame.format, FrameFormat::RGB888);
|
||||
assert_eq!(frame.len(), data.len());
|
||||
assert_eq!(frame.as_slice().len(), data.len());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_zero_copy_slice() {
|
||||
let data = vec![128u8; 1000];
|
||||
let frame = FrameData::new(data, 100, 100, FrameFormat::RGB888);
|
||||
|
||||
|
||||
// Create zero-copy slice
|
||||
let slice = frame.slice(100, 200);
|
||||
assert_eq!(slice.len(), 100);
|
||||
|
||||
|
||||
// Verify it's the same underlying data
|
||||
assert_eq!(&slice[..], &frame.as_slice()[100..200]);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_shared_frame_data() {
|
||||
let data = vec![255u8; 100];
|
||||
let shared_frame = create_shared_frame(data, 10, 10, FrameFormat::RGB888);
|
||||
|
||||
|
||||
// Clone the Arc (cheap operation)
|
||||
let cloned_frame = Arc::clone(&shared_frame);
|
||||
|
||||
|
||||
// Both should point to same data
|
||||
assert_eq!(shared_frame.as_slice().as_ptr(), cloned_frame.as_slice().as_ptr());
|
||||
assert_eq!(
|
||||
shared_frame.as_slice().as_ptr(),
|
||||
cloned_frame.as_slice().as_ptr()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_calculate_expected_size() {
|
||||
assert_eq!(
|
||||
FrameData::calculate_expected_size(640, 480, &FrameFormat::RGB888),
|
||||
640 * 480 * 3
|
||||
);
|
||||
|
||||
|
||||
assert_eq!(
|
||||
FrameData::calculate_expected_size(640, 480, &FrameFormat::YUV420),
|
||||
640 * 480 * 3 / 2
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::VecDeque;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
/// Frame pool statistics for monitoring
|
||||
@ -31,35 +31,50 @@ impl PooledFrameBuffer {
|
||||
buffer_size,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get mutable access to the buffer
|
||||
pub fn as_mut(&mut self) -> &mut BytesMut {
|
||||
self.buffer.as_mut().expect("Buffer should be available")
|
||||
}
|
||||
|
||||
|
||||
/// Get immutable access to the buffer
|
||||
pub fn as_ref(&self) -> &BytesMut {
|
||||
self.buffer.as_ref().expect("Buffer should be available")
|
||||
}
|
||||
|
||||
|
||||
/// Convert to frozen Bytes for zero-copy sharing
|
||||
pub fn freeze(mut self) -> Bytes {
|
||||
let buffer = self.buffer.take().expect("Buffer should be available");
|
||||
// Note: We don't return to pool since buffer is now frozen
|
||||
buffer.freeze()
|
||||
}
|
||||
|
||||
|
||||
/// Get the capacity of this buffer
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.buffer_size
|
||||
}
|
||||
|
||||
|
||||
/// Clear the buffer contents (keeping capacity)
|
||||
pub fn clear(&mut self) {
|
||||
if let Some(ref mut buffer) = self.buffer {
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get pointer to the buffer data
|
||||
pub fn as_ptr(&self) -> *const u8 {
|
||||
self.as_ref().as_ptr()
|
||||
}
|
||||
|
||||
/// Get buffer length
|
||||
pub fn len(&self) -> usize {
|
||||
self.as_ref().len()
|
||||
}
|
||||
|
||||
/// Check if buffer is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.as_ref().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PooledFrameBuffer {
|
||||
@ -74,7 +89,7 @@ impl Drop for PooledFrameBuffer {
|
||||
// Clear the buffer but keep capacity
|
||||
restored_buffer.clear();
|
||||
}
|
||||
|
||||
|
||||
self.pool.return_buffer(restored_buffer);
|
||||
}
|
||||
}
|
||||
@ -104,7 +119,7 @@ struct FramePoolStatsInner {
|
||||
|
||||
impl FramePool {
|
||||
/// Create a new frame pool
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pool_capacity` - Maximum number of buffers to keep in pool
|
||||
/// * `buffer_size` - Size of each buffer in bytes (typically frame size)
|
||||
@ -115,33 +130,36 @@ impl FramePool {
|
||||
buffer_size,
|
||||
stats: FramePoolStatsInner::default(),
|
||||
};
|
||||
|
||||
|
||||
Arc::new(Self {
|
||||
inner: Arc::new(Mutex::new(inner)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Pre-populate the pool with buffers
|
||||
pub fn warm_up(&self) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
|
||||
// Pre-allocate half the pool capacity
|
||||
let warm_up_count = inner.pool_capacity / 2;
|
||||
|
||||
|
||||
for _ in 0..warm_up_count {
|
||||
let buffer = BytesMut::with_capacity(inner.buffer_size);
|
||||
inner.available_buffers.push_back(buffer);
|
||||
}
|
||||
|
||||
println!("🔥 Frame pool warmed up with {} buffers ({} KB each)",
|
||||
warm_up_count, inner.buffer_size / 1024);
|
||||
|
||||
println!(
|
||||
"🔥 Frame pool warmed up with {} buffers ({} KB each)",
|
||||
warm_up_count,
|
||||
inner.buffer_size / 1024
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// Acquire a buffer from the pool (or allocate new if pool empty)
|
||||
pub fn acquire(self: &Arc<Self>) -> PooledFrameBuffer {
|
||||
let start_time = Instant::now();
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
|
||||
let buffer = if let Some(buffer) = inner.available_buffers.pop_front() {
|
||||
// Cache hit - reuse existing buffer
|
||||
buffer
|
||||
@ -149,53 +167,57 @@ impl FramePool {
|
||||
// Cache miss - allocate new buffer
|
||||
BytesMut::with_capacity(inner.buffer_size)
|
||||
};
|
||||
|
||||
|
||||
// Update statistics
|
||||
inner.stats.total_allocations += 1;
|
||||
let allocation_time = start_time.elapsed().as_nanos() as u64;
|
||||
inner.stats.allocation_time_nanos += allocation_time;
|
||||
inner.stats.allocation_count += 1;
|
||||
|
||||
|
||||
let buffer_size = inner.buffer_size;
|
||||
drop(inner); // Release lock early
|
||||
|
||||
|
||||
PooledFrameBuffer::new(buffer, self.clone(), buffer_size)
|
||||
}
|
||||
|
||||
|
||||
/// Return a buffer to the pool (called automatically by PooledFrameBuffer::drop)
|
||||
fn return_buffer(&self, buffer: BytesMut) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
|
||||
inner.stats.total_returns += 1;
|
||||
|
||||
|
||||
// Only keep buffer if pool not full and buffer is correct size
|
||||
if inner.available_buffers.len() < inner.pool_capacity
|
||||
&& buffer.capacity() >= inner.buffer_size {
|
||||
if inner.available_buffers.len() < inner.pool_capacity
|
||||
&& buffer.capacity() >= inner.buffer_size
|
||||
{
|
||||
inner.available_buffers.push_back(buffer);
|
||||
}
|
||||
// If pool full or buffer wrong size, buffer will be dropped and deallocated
|
||||
}
|
||||
|
||||
|
||||
/// Get current pool statistics
|
||||
pub fn stats(&self) -> FramePoolStats {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
|
||||
let cache_hit_rate = if inner.stats.total_allocations > 0 {
|
||||
// Cache hit rate = times we reused a buffer / total allocations
|
||||
// For now, use a simple approximation based on available buffers vs capacity
|
||||
let pool_utilization = (inner.pool_capacity.saturating_sub(inner.available_buffers.len()) as f64)
|
||||
let pool_utilization = (inner
|
||||
.pool_capacity
|
||||
.saturating_sub(inner.available_buffers.len())
|
||||
as f64)
|
||||
/ inner.pool_capacity as f64;
|
||||
pool_utilization.min(1.0).max(0.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
|
||||
let average_allocation_time_nanos = if inner.stats.allocation_count > 0 {
|
||||
inner.stats.allocation_time_nanos / inner.stats.allocation_count
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
|
||||
FramePoolStats {
|
||||
pool_capacity: inner.pool_capacity,
|
||||
available_buffers: inner.available_buffers.len(),
|
||||
@ -206,29 +228,29 @@ impl FramePool {
|
||||
average_allocation_time_nanos,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adjust pool capacity dynamically
|
||||
pub fn resize(&self, new_capacity: usize) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
|
||||
if new_capacity < inner.pool_capacity {
|
||||
// Shrink pool - remove excess buffers
|
||||
while inner.available_buffers.len() > new_capacity {
|
||||
inner.available_buffers.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner.pool_capacity = new_capacity;
|
||||
println!("🔄 Frame pool resized to capacity: {}", new_capacity);
|
||||
}
|
||||
|
||||
|
||||
/// Clear all buffers from pool (useful for memory pressure situations)
|
||||
pub fn clear(&self) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.available_buffers.clear();
|
||||
println!("🧹 Frame pool cleared - all buffers released");
|
||||
}
|
||||
|
||||
|
||||
/// Get memory usage in bytes
|
||||
pub fn memory_usage(&self) -> usize {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
@ -246,28 +268,28 @@ impl HierarchicalFramePool {
|
||||
/// Create hierarchical pool with common frame sizes
|
||||
pub fn new(default_capacity: usize) -> Self {
|
||||
let common_sizes = vec![
|
||||
64 * 1024, // 64KB - small frames
|
||||
256 * 1024, // 256KB - medium frames
|
||||
900 * 1024, // 900KB - large HD frames
|
||||
64 * 1024, // 64KB - small frames
|
||||
256 * 1024, // 256KB - medium frames
|
||||
900 * 1024, // 900KB - large HD frames
|
||||
2 * 1024 * 1024, // 2MB - 4K frames
|
||||
];
|
||||
|
||||
|
||||
let pools = common_sizes
|
||||
.into_iter()
|
||||
.map(|size| (size, FramePool::new(default_capacity, size)))
|
||||
.collect();
|
||||
|
||||
|
||||
let pool_manager = Self {
|
||||
pools,
|
||||
default_capacity,
|
||||
};
|
||||
|
||||
|
||||
// Warm up all pools
|
||||
pool_manager.warm_up_all();
|
||||
|
||||
|
||||
pool_manager
|
||||
}
|
||||
|
||||
|
||||
/// Warm up all pools
|
||||
pub fn warm_up_all(&self) {
|
||||
for (_size, pool) in &self.pools {
|
||||
@ -275,7 +297,7 @@ impl HierarchicalFramePool {
|
||||
}
|
||||
println!("🔥 Hierarchical frame pool system warmed up");
|
||||
}
|
||||
|
||||
|
||||
/// Acquire buffer from most appropriate pool
|
||||
pub fn acquire(&self, required_size: usize) -> PooledFrameBuffer {
|
||||
// Find the smallest pool that can accommodate the required size
|
||||
@ -284,7 +306,7 @@ impl HierarchicalFramePool {
|
||||
return pool.acquire();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no pool is large enough, use the largest one
|
||||
if let Some((_, largest_pool)) = self.pools.last() {
|
||||
largest_pool.acquire()
|
||||
@ -294,7 +316,7 @@ impl HierarchicalFramePool {
|
||||
fallback_pool.acquire()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get statistics for all pools
|
||||
pub fn all_stats(&self) -> Vec<(usize, FramePoolStats)> {
|
||||
self.pools
|
||||
@ -302,29 +324,26 @@ impl HierarchicalFramePool {
|
||||
.map(|(size, pool)| (*size, pool.stats()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
/// Calculate total memory usage across all pools
|
||||
pub fn total_memory_usage(&self) -> usize {
|
||||
self.pools
|
||||
.iter()
|
||||
.map(|(_, pool)| pool.memory_usage())
|
||||
.sum()
|
||||
self.pools.iter().map(|(_, pool)| pool.memory_usage()).sum()
|
||||
}
|
||||
|
||||
|
||||
/// Resize all pools (for adaptive management)
|
||||
pub fn resize_all(&self, new_capacity: usize) {
|
||||
for (_, pool) in &self.pools {
|
||||
pool.resize(new_capacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Clear all pools (memory pressure response)
|
||||
pub fn clear_all(&self) {
|
||||
for (_, pool) in &self.pools {
|
||||
pool.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get individual pool reference for advanced operations
|
||||
pub fn get_pool_for_size(&self, size: usize) -> Option<Arc<FramePool>> {
|
||||
// Find the smallest pool that can accommodate the size
|
||||
@ -335,21 +354,26 @@ impl HierarchicalFramePool {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// Resize a specific pool size
|
||||
pub fn resize_pool(&self, target_size: usize, new_capacity: usize) {
|
||||
for (pool_size, pool) in &self.pools {
|
||||
if *pool_size == target_size {
|
||||
pool.resize(new_capacity);
|
||||
println!("🔧 Resized {}KB pool to {} buffers", target_size / 1024, new_capacity);
|
||||
println!(
|
||||
"🔧 Resized {}KB pool to {} buffers",
|
||||
target_size / 1024,
|
||||
new_capacity
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get pool sizes and their capacities
|
||||
pub fn get_pool_capacities(&self) -> Vec<(usize, usize)> {
|
||||
self.pools.iter()
|
||||
self.pools
|
||||
.iter()
|
||||
.map(|(size, pool)| (*size, pool.stats().pool_capacity))
|
||||
.collect()
|
||||
}
|
||||
@ -360,86 +384,86 @@ mod tests {
|
||||
use super::*;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_frame_pool_creation() {
|
||||
let pool = FramePool::new(10, 1024);
|
||||
let stats = pool.stats();
|
||||
|
||||
|
||||
assert_eq!(stats.pool_capacity, 10);
|
||||
assert_eq!(stats.available_buffers, 0);
|
||||
assert_eq!(stats.total_allocations, 0);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_buffer_acquisition_and_return() {
|
||||
let pool = FramePool::new(5, 1024);
|
||||
|
||||
|
||||
{
|
||||
let _buffer = pool.acquire();
|
||||
let stats = pool.stats();
|
||||
assert_eq!(stats.total_allocations, 1);
|
||||
assert_eq!(stats.allocated_buffers, 1);
|
||||
} // buffer dropped here, should return to pool
|
||||
|
||||
|
||||
thread::sleep(Duration::from_millis(1)); // Allow drop to complete
|
||||
|
||||
|
||||
let stats = pool.stats();
|
||||
assert_eq!(stats.total_returns, 1);
|
||||
assert_eq!(stats.available_buffers, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
#[test]
|
||||
fn test_pool_reuse() {
|
||||
let pool = FramePool::new(5, 1024);
|
||||
pool.warm_up();
|
||||
|
||||
|
||||
// First acquisition should reuse pre-warmed buffer
|
||||
let buffer1 = pool.acquire();
|
||||
let stats1 = pool.stats();
|
||||
assert!(stats1.available_buffers < 2); // Should have taken from pool
|
||||
|
||||
|
||||
drop(buffer1);
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
|
||||
|
||||
// Second acquisition should reuse returned buffer
|
||||
let _buffer2 = pool.acquire();
|
||||
let stats2 = pool.stats();
|
||||
assert_eq!(stats2.total_allocations, 2);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_hierarchical_pool() {
|
||||
let hierarchical = HierarchicalFramePool::new(5);
|
||||
|
||||
|
||||
// Test different size acquisitions
|
||||
let small_buffer = hierarchical.acquire(32 * 1024); // Should use 64KB pool
|
||||
let small_buffer = hierarchical.acquire(32 * 1024); // Should use 64KB pool
|
||||
let medium_buffer = hierarchical.acquire(200 * 1024); // Should use 256KB pool
|
||||
let large_buffer = hierarchical.acquire(800 * 1024); // Should use 900KB pool
|
||||
|
||||
let large_buffer = hierarchical.acquire(800 * 1024); // Should use 900KB pool
|
||||
|
||||
assert!(small_buffer.capacity() >= 32 * 1024);
|
||||
assert!(medium_buffer.capacity() >= 200 * 1024);
|
||||
assert!(large_buffer.capacity() >= 800 * 1024);
|
||||
|
||||
|
||||
let total_memory = hierarchical.total_memory_usage();
|
||||
assert!(total_memory > 0);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_pooled_buffer_operations() {
|
||||
let pool = FramePool::new(5, 1024);
|
||||
let mut buffer = pool.acquire();
|
||||
|
||||
|
||||
// Test buffer operations
|
||||
buffer.clear();
|
||||
assert_eq!(buffer.as_ref().len(), 0);
|
||||
|
||||
|
||||
buffer.as_mut().extend_from_slice(b"test data");
|
||||
assert_eq!(buffer.as_ref().len(), 9);
|
||||
|
||||
|
||||
// Test freeze operation
|
||||
let frozen = buffer.freeze();
|
||||
assert_eq!(frozen.len(), 9);
|
||||
assert_eq!(&frozen[..], b"test data");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,8 +7,8 @@ use std::sync::atomic::{AtomicUsize, AtomicU64, AtomicBool, Ordering};
|
||||
use anyhow::Result;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::ring_buffer::AstronomicalFrame;
|
||||
use crate::memory_mapping::MemoryMappedFile;
|
||||
use crate::memory::ring_buffer::AstronomicalFrame;
|
||||
use crate::memory::memory_mapping::MemoryMappedFile;
|
||||
|
||||
/// Multi-level hierarchical cache system optimized for astronomical data processing
|
||||
pub struct HierarchicalCache<K, V>
|
||||
@ -20,27 +20,29 @@ impl MemoryMonitor {
|
||||
start_time: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Record a frame being processed with avoided memory copies
|
||||
pub fn record_frame_processed(&self, frame_size: usize, subscribers: usize) {
|
||||
self.frames_processed.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
|
||||
// Calculate bytes saved: (subscribers - 1) * frame_size
|
||||
// We subtract 1 because the first copy is unavoidable
|
||||
let bytes_saved = (subscribers.saturating_sub(1)) * frame_size;
|
||||
self.bytes_saved.fetch_add(bytes_saved as u64, Ordering::Relaxed);
|
||||
|
||||
self.bytes_saved
|
||||
.fetch_add(bytes_saved as u64, Ordering::Relaxed);
|
||||
|
||||
// Track Arc references created (one per subscriber)
|
||||
self.arc_references_created.fetch_add(subscribers as u64, Ordering::Relaxed);
|
||||
self.arc_references_created
|
||||
.fetch_add(subscribers as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
|
||||
/// Get current memory optimization statistics
|
||||
pub fn stats(&self) -> MemoryStats {
|
||||
let frames = self.frames_processed.load(Ordering::Relaxed);
|
||||
let bytes_saved = self.bytes_saved.load(Ordering::Relaxed);
|
||||
let arc_refs = self.arc_references_created.load(Ordering::Relaxed);
|
||||
let elapsed = self.start_time.elapsed();
|
||||
|
||||
|
||||
MemoryStats {
|
||||
frames_processed: frames,
|
||||
bytes_saved_total: bytes_saved,
|
||||
@ -58,35 +60,39 @@ impl MemoryMonitor {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Start background reporting loop
|
||||
pub async fn start_reporting(&self, interval_seconds: u64) {
|
||||
let mut reporting_interval = interval(Duration::from_secs(interval_seconds));
|
||||
|
||||
|
||||
loop {
|
||||
reporting_interval.tick().await;
|
||||
let stats = self.stats();
|
||||
Self::log_stats(&stats).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn log_stats(stats: &MemoryStats) {
|
||||
if stats.frames_processed > 0 {
|
||||
println!("📊 Memory Optimization Stats:");
|
||||
println!(" Frames Processed: {}", stats.frames_processed);
|
||||
println!(" Memory Saved: {:.1} MB ({:.1} MB/s)",
|
||||
println!(
|
||||
" Memory Saved: {:.1} MB ({:.1} MB/s)",
|
||||
stats.bytes_saved_total as f64 / 1_000_000.0,
|
||||
stats.bytes_saved_per_second / 1_000_000.0
|
||||
);
|
||||
println!(" Frame Rate: {:.1} FPS", stats.frames_per_second);
|
||||
println!(" Arc References: {}", stats.arc_references_created);
|
||||
println!(" Runtime: {}s", stats.elapsed_seconds);
|
||||
|
||||
|
||||
// Calculate efficiency
|
||||
if stats.frames_processed > 100 {
|
||||
let efficiency = (stats.bytes_saved_total as f64) /
|
||||
(stats.frames_processed as f64 * 900_000.0); // Assuming 900KB frames
|
||||
println!(" Memory Efficiency: {:.1}% (vs traditional copying)", efficiency * 100.0);
|
||||
let efficiency =
|
||||
(stats.bytes_saved_total as f64) / (stats.frames_processed as f64 * 900_000.0); // Assuming 900KB frames
|
||||
println!(
|
||||
" Memory Efficiency: {:.1}% (vs traditional copying)",
|
||||
efficiency * 100.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -128,7 +134,7 @@ impl SystemMemoryInfo {
|
||||
let available_mb = mem_info.avail / 1024;
|
||||
let used_mb = total_mb - available_mb;
|
||||
let used_percentage = (used_mb as f32 / total_mb as f32) * 100.0;
|
||||
|
||||
|
||||
Ok(Self {
|
||||
total_mb,
|
||||
available_mb,
|
||||
@ -147,7 +153,7 @@ impl SystemMemoryInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check if system is under memory pressure
|
||||
pub fn is_under_pressure(&self) -> bool {
|
||||
self.used_percentage > 80.0
|
||||
@ -167,19 +173,18 @@ impl MemoryPressureMonitor {
|
||||
pressure_threshold,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check current memory pressure and return recommendations
|
||||
pub async fn check_pressure(&self) -> MemoryPressureReport {
|
||||
let system_info = SystemMemoryInfo::current()
|
||||
.unwrap_or_else(|_| SystemMemoryInfo {
|
||||
total_mb: 1024, // Default for Pi
|
||||
available_mb: 512,
|
||||
used_mb: 512,
|
||||
used_percentage: 50.0,
|
||||
});
|
||||
|
||||
let system_info = SystemMemoryInfo::current().unwrap_or_else(|_| SystemMemoryInfo {
|
||||
total_mb: 1024, // Default for Pi
|
||||
available_mb: 512,
|
||||
used_mb: 512,
|
||||
used_percentage: 50.0,
|
||||
});
|
||||
|
||||
let optimization_stats = self.monitor.stats();
|
||||
|
||||
|
||||
let pressure_level = if system_info.used_percentage > 90.0 {
|
||||
PressureLevel::Critical
|
||||
} else if system_info.used_percentage > self.pressure_threshold {
|
||||
@ -189,7 +194,7 @@ impl MemoryPressureMonitor {
|
||||
} else {
|
||||
PressureLevel::Low
|
||||
};
|
||||
|
||||
|
||||
MemoryPressureReport {
|
||||
pressure_level: pressure_level.clone(),
|
||||
system_info,
|
||||
@ -197,10 +202,10 @@ impl MemoryPressureMonitor {
|
||||
recommendations: Self::generate_recommendations(&pressure_level, &optimization_stats),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn generate_recommendations(level: &PressureLevel, stats: &MemoryStats) -> Vec<String> {
|
||||
let mut recommendations = Vec::new();
|
||||
|
||||
|
||||
match level {
|
||||
PressureLevel::Critical => {
|
||||
recommendations.push("CRITICAL: Consider reducing frame buffer sizes".to_string());
|
||||
@ -217,17 +222,22 @@ impl MemoryPressureMonitor {
|
||||
recommendations.push("LOW: Memory usage optimal".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add optimization-specific recommendations
|
||||
if stats.frames_processed > 100 {
|
||||
let efficiency = stats.bytes_saved_total as f64 / (stats.frames_processed as f64 * 900_000.0);
|
||||
let efficiency =
|
||||
stats.bytes_saved_total as f64 / (stats.frames_processed as f64 * 900_000.0);
|
||||
if efficiency < 0.5 {
|
||||
recommendations.push("OPTIMIZATION: Zero-copy efficiency could be improved".to_string());
|
||||
recommendations
|
||||
.push("OPTIMIZATION: Zero-copy efficiency could be improved".to_string());
|
||||
} else {
|
||||
recommendations.push(format!("OPTIMIZATION: Zero-copy working well ({:.1}% efficient)", efficiency * 100.0));
|
||||
recommendations.push(format!(
|
||||
"OPTIMIZATION: Zero-copy working well ({:.1}% efficient)",
|
||||
efficiency * 100.0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
recommendations
|
||||
}
|
||||
}
|
||||
@ -262,42 +272,42 @@ pub fn record_frame_processed(frame_size: usize, subscribers: usize) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio::time::sleep;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_memory_monitor_creation() {
|
||||
let monitor = MemoryMonitor::new();
|
||||
let stats = monitor.stats();
|
||||
|
||||
|
||||
assert_eq!(stats.frames_processed, 0);
|
||||
assert_eq!(stats.bytes_saved_total, 0);
|
||||
assert_eq!(stats.arc_references_created, 0);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_frame_processing_recording() {
|
||||
let monitor = MemoryMonitor::new();
|
||||
|
||||
|
||||
// Simulate processing a frame with 3 subscribers
|
||||
monitor.record_frame_processed(900_000, 3); // 900KB frame, 3 subscribers
|
||||
|
||||
|
||||
let stats = monitor.stats();
|
||||
assert_eq!(stats.frames_processed, 1);
|
||||
assert_eq!(stats.bytes_saved_total, 1_800_000); // 2 * 900KB saved
|
||||
assert_eq!(stats.arc_references_created, 3);
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_memory_pressure_monitor() {
|
||||
let pressure_monitor = MemoryPressureMonitor::new(75.0);
|
||||
let report = pressure_monitor.check_pressure().await;
|
||||
|
||||
|
||||
// Should not panic and should provide some recommendations
|
||||
assert!(!report.recommendations.is_empty());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_system_memory_info() {
|
||||
// Should not panic even if system info is unavailable
|
||||
let _info = SystemMemoryInfo::current();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,8 +57,8 @@ impl MemoryPressureDetector {
|
||||
|
||||
/// Take a memory sample and evaluate pressure level
|
||||
async fn sample_and_evaluate(&self) -> anyhow::Result<()> {
|
||||
let memory_info = crate::memory_monitor::SystemMemoryInfo::current()
|
||||
.unwrap_or_else(|_| crate::memory_monitor::SystemMemoryInfo {
|
||||
let memory_info = crate::memory::memory_monitor::SystemMemoryInfo::current()
|
||||
.unwrap_or_else(|_| crate::memory::memory_monitor::SystemMemoryInfo {
|
||||
total_mb: 1024,
|
||||
available_mb: 512,
|
||||
used_mb: 512,
|
||||
23
meteor-edge-client/src/memory/mod.rs
Normal file
23
meteor-edge-client/src/memory/mod.rs
Normal file
@ -0,0 +1,23 @@
|
||||
// Memory management modules
|
||||
|
||||
pub mod adaptive_pool_manager;
|
||||
pub mod frame_data;
|
||||
pub mod frame_pool;
|
||||
pub mod hierarchical_cache;
|
||||
pub mod memory_mapping;
|
||||
pub mod memory_monitor;
|
||||
pub mod memory_pressure;
|
||||
pub mod ring_buffer;
|
||||
|
||||
// Test modules
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
pub use adaptive_pool_manager::*;
|
||||
pub use frame_data::*;
|
||||
pub use frame_pool::*;
|
||||
pub use hierarchical_cache::*;
|
||||
pub use memory_mapping::*;
|
||||
pub use memory_monitor::*;
|
||||
pub use memory_pressure::*;
|
||||
pub use ring_buffer::*;
|
||||
@ -2,9 +2,9 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
use crate::adaptive_pool_manager::{AdaptivePoolConfig, AdaptivePoolManager};
|
||||
use crate::frame_pool::HierarchicalFramePool;
|
||||
use crate::memory_monitor::MemoryMonitor;
|
||||
use crate::memory::adaptive_pool_manager::{AdaptivePoolConfig, AdaptivePoolManager};
|
||||
use crate::memory::frame_pool::HierarchicalFramePool;
|
||||
use crate::memory::memory_monitor::MemoryMonitor;
|
||||
|
||||
/// Main adaptive pool system test (entry point for CLI)
|
||||
pub async fn test_adaptive_pool_system() -> anyhow::Result<()> {
|
||||
@ -2,8 +2,8 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::frame_pool::{FramePool, HierarchicalFramePool};
|
||||
use crate::memory_monitor::GLOBAL_MEMORY_MONITOR;
|
||||
use crate::memory::frame_pool::{FramePool, HierarchicalFramePool};
|
||||
use crate::memory::memory_monitor::GLOBAL_MEMORY_MONITOR;
|
||||
|
||||
/// Integration test for frame pool performance and zero-allocation behavior
|
||||
pub async fn test_frame_pool_integration() -> anyhow::Result<()> {
|
||||
@ -3,13 +3,13 @@ use std::time::{Duration, Instant};
|
||||
use tokio::time::{sleep, timeout};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::hierarchical_cache::{
|
||||
use crate::memory::hierarchical_cache::{
|
||||
HierarchicalCache, CacheConfig, EvictionPolicy, EntryMetadata,
|
||||
create_astronomical_cache, create_memory_region_cache,
|
||||
CacheMonitor, CacheMonitorable
|
||||
};
|
||||
use crate::ring_buffer::AstronomicalFrame;
|
||||
use crate::memory_mapping::{MemoryMappedFile, MappingConfig, AccessPattern};
|
||||
use crate::memory::ring_buffer::AstronomicalFrame;
|
||||
use crate::memory::memory_mapping::{MemoryMappedFile, MappingConfig, AccessPattern};
|
||||
|
||||
/// Comprehensive test suite for Hierarchical Cache System
|
||||
pub async fn test_hierarchical_cache_system() -> Result<()> {
|
||||
14
meteor-edge-client/src/memory/tests/mod.rs
Normal file
14
meteor-edge-client/src/memory/tests/mod.rs
Normal file
@ -0,0 +1,14 @@
|
||||
// Memory management tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod adaptive_pool_tests;
|
||||
#[cfg(test)]
|
||||
mod frame_pool_tests;
|
||||
#[cfg(test)]
|
||||
mod hierarchical_cache_tests;
|
||||
#[cfg(test)]
|
||||
mod pool_integration_tests;
|
||||
#[cfg(test)]
|
||||
mod ring_buffer_tests;
|
||||
#[cfg(test)]
|
||||
mod zero_copy_tests;
|
||||
@ -3,9 +3,9 @@ use std::time::{Duration, Instant};
|
||||
use tokio::time::{sleep, timeout};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::frame_pool::HierarchicalFramePool;
|
||||
use crate::adaptive_pool_manager::{AdaptivePoolConfig, AdaptivePoolManager};
|
||||
use crate::memory_monitor::{MemoryMonitor, GLOBAL_MEMORY_MONITOR};
|
||||
use crate::memory::frame_pool::HierarchicalFramePool;
|
||||
use crate::memory::adaptive_pool_manager::{AdaptivePoolConfig, AdaptivePoolManager};
|
||||
use crate::memory::memory_monitor::{MemoryMonitor, GLOBAL_MEMORY_MONITOR};
|
||||
|
||||
/// Comprehensive integration test for the complete pool system
|
||||
pub async fn test_complete_pool_integration() -> Result<()> {
|
||||
@ -3,12 +3,12 @@ use std::time::{Duration, Instant};
|
||||
use tokio::time::{sleep, timeout};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::ring_buffer::{
|
||||
use crate::memory::ring_buffer::{
|
||||
RingBuffer, RingBufferConfig, AstronomicalFrame,
|
||||
create_meteor_frame_buffer, RingBufferMonitor
|
||||
};
|
||||
use crate::memory_mapping::{MappingConfig, MappingPool, AccessPattern};
|
||||
use crate::frame_pool::HierarchicalFramePool;
|
||||
use crate::memory::memory_mapping::{MappingConfig, MappingPool, AccessPattern};
|
||||
use crate::memory::frame_pool::HierarchicalFramePool;
|
||||
|
||||
/// Comprehensive test suite for Ring Buffer & Memory Mapping integration
|
||||
pub async fn test_ring_buffer_system() -> Result<()> {
|
||||
@ -5,9 +5,9 @@ mod zero_copy_tests {
|
||||
use std::time::Instant;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::frame_data::{create_shared_frame, FrameFormat};
|
||||
use crate::events::{EventBus, FrameCapturedEvent, SystemEvent};
|
||||
use crate::memory_monitor::{MemoryMonitor, GLOBAL_MEMORY_MONITOR};
|
||||
use crate::memory::frame_data::{create_shared_frame, FrameFormat};
|
||||
use crate::core::events::{EventBus, FrameCapturedEvent, SystemEvent};
|
||||
use crate::memory::memory_monitor::{MemoryMonitor, GLOBAL_MEMORY_MONITOR};
|
||||
|
||||
#[test]
|
||||
fn test_zero_copy_frame_sharing() {
|
||||
@ -4,12 +4,12 @@ use anyhow::Result;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio::time::{interval, timeout};
|
||||
|
||||
use crate::frame_pool::{HierarchicalFramePool, PooledFrameBuffer};
|
||||
use crate::adaptive_pool_manager::{AdaptivePoolManager, AdaptivePoolConfig};
|
||||
use crate::ring_buffer::{RingBuffer, AstronomicalFrame};
|
||||
use crate::hierarchical_cache::{HierarchicalCache, CacheConfig, create_astronomical_cache};
|
||||
use crate::production_monitor::{ProductionMonitor, MonitoringConfig};
|
||||
use crate::memory_monitor::{MemoryMonitor, SystemMemoryInfo};
|
||||
use crate::memory::frame_pool::{HierarchicalFramePool, PooledFrameBuffer};
|
||||
use crate::memory::adaptive_pool_manager::{AdaptivePoolManager, AdaptivePoolConfig};
|
||||
use crate::memory::ring_buffer::{RingBuffer, AstronomicalFrame};
|
||||
use crate::memory::hierarchical_cache::{HierarchicalCache, CacheConfig, create_astronomical_cache};
|
||||
use crate::monitoring::production_monitor::{ProductionMonitor, MonitoringConfig};
|
||||
use crate::memory::memory_monitor::{MemoryMonitor, SystemMemoryInfo};
|
||||
|
||||
/// Integrated memory management system for meteor detection
|
||||
/// Combines all memory optimization components into a cohesive system
|
||||
@ -128,11 +128,16 @@ impl IntegratedMemorySystem {
|
||||
let pool_manager = Arc::new(AdaptivePoolManager::new(
|
||||
config.adaptive_config.clone(),
|
||||
frame_pool.clone(),
|
||||
)?);
|
||||
));
|
||||
println!(" ✓ Adaptive Pool Manager initialized");
|
||||
|
||||
// Initialize ring buffer for astronomical frames
|
||||
let ring_buffer = Arc::new(RingBuffer::new(config.ring_buffer_capacity)?);
|
||||
use crate::memory::ring_buffer::RingBufferConfig;
|
||||
let ring_buffer_config = RingBufferConfig {
|
||||
capacity: config.ring_buffer_capacity,
|
||||
..Default::default()
|
||||
};
|
||||
let ring_buffer = Arc::new(RingBuffer::new(ring_buffer_config)?);
|
||||
println!(" ✓ Ring Buffer initialized (capacity: {})", config.ring_buffer_capacity);
|
||||
|
||||
// Initialize hierarchical cache
|
||||
@ -164,9 +169,7 @@ impl IntegratedMemorySystem {
|
||||
// Start adaptive pool management
|
||||
let pool_manager = self.pool_manager.clone();
|
||||
let pool_handle = tokio::spawn(async move {
|
||||
if let Err(e) = pool_manager.start_monitoring().await {
|
||||
eprintln!("Pool manager error: {}", e);
|
||||
}
|
||||
pool_manager.start_adaptive_management().await;
|
||||
});
|
||||
|
||||
// Start production monitoring
|
||||
@ -235,7 +238,7 @@ impl IntegratedMemorySystem {
|
||||
}
|
||||
|
||||
// Store in ring buffer for streaming
|
||||
if !self.ring_buffer.try_produce(frame.clone()) {
|
||||
if self.ring_buffer.try_write(frame.clone()).is_err() {
|
||||
println!("⚠️ Ring buffer full, frame {} dropped", frame.frame_id);
|
||||
}
|
||||
|
||||
@ -264,7 +267,9 @@ impl IntegratedMemorySystem {
|
||||
let metrics = self.get_metrics().await;
|
||||
let monitor_health = self.monitor.get_health_status();
|
||||
let cache_stats = self.frame_cache.stats();
|
||||
|
||||
|
||||
let recommendations = self.generate_recommendations(&metrics);
|
||||
|
||||
SystemHealthReport {
|
||||
overall_status: if metrics.performance_score > 0.8 {
|
||||
SystemStatus::Healthy
|
||||
@ -276,7 +281,7 @@ impl IntegratedMemorySystem {
|
||||
metrics,
|
||||
monitor_health,
|
||||
cache_stats,
|
||||
recommendations: self.generate_recommendations(&metrics),
|
||||
recommendations,
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,8 +347,8 @@ impl IntegratedMemorySystem {
|
||||
}
|
||||
|
||||
async fn start_processing_pipeline(&self) -> Result<tokio::task::JoinHandle<()>> {
|
||||
let (tx, rx) = mpsc::channel(1000);
|
||||
let (_processed_tx, _processed_rx) = mpsc::channel(1000);
|
||||
let (_tx, _rx) = mpsc::channel::<AstronomicalFrame>(1000);
|
||||
let (_processed_tx, _processed_rx) = mpsc::channel::<ProcessedFrame>(1000);
|
||||
|
||||
let system = Arc::new(self.clone());
|
||||
|
||||
@ -439,8 +444,8 @@ impl Clone for IntegratedMemorySystem {
|
||||
pub struct SystemHealthReport {
|
||||
pub overall_status: SystemStatus,
|
||||
pub metrics: SystemMetrics,
|
||||
pub monitor_health: crate::production_monitor::SystemHealth,
|
||||
pub cache_stats: crate::hierarchical_cache::CacheStatsSnapshot,
|
||||
pub monitor_health: crate::monitoring::production_monitor::SystemHealth,
|
||||
pub cache_stats: crate::memory::hierarchical_cache::CacheStatsSnapshot,
|
||||
pub recommendations: Vec<String>,
|
||||
}
|
||||
|
||||
7
meteor-edge-client/src/monitoring/mod.rs
Normal file
7
meteor-edge-client/src/monitoring/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
// Monitoring modules
|
||||
|
||||
pub mod integrated_system;
|
||||
pub mod production_monitor;
|
||||
|
||||
pub use integrated_system::*;
|
||||
pub use production_monitor::*;
|
||||
@ -7,11 +7,11 @@ use tokio::time::{sleep, interval};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::memory_monitor::{MemoryMonitor, SystemMemoryInfo};
|
||||
use crate::frame_pool::HierarchicalFramePool;
|
||||
use crate::adaptive_pool_manager::AdaptivePoolManager;
|
||||
use crate::ring_buffer::RingBuffer;
|
||||
use crate::hierarchical_cache::HierarchicalCache;
|
||||
use crate::memory::memory_monitor::{MemoryMonitor, SystemMemoryInfo};
|
||||
use crate::memory::frame_pool::HierarchicalFramePool;
|
||||
use crate::memory::adaptive_pool_manager::AdaptivePoolManager;
|
||||
use crate::memory::ring_buffer::RingBuffer;
|
||||
use crate::memory::hierarchical_cache::HierarchicalCache;
|
||||
|
||||
/// Comprehensive production monitoring system for the meteor edge client
|
||||
pub struct ProductionMonitor {
|
||||
@ -59,15 +59,15 @@ impl ApiClient {
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
|
||||
Self { client, base_url }
|
||||
}
|
||||
|
||||
|
||||
/// Creates a new API client with default localhost URL
|
||||
pub fn default() -> Self {
|
||||
Self::new("http://localhost:3000".to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Registers a device with the backend API
|
||||
pub async fn register_device(
|
||||
&self,
|
||||
@ -75,12 +75,12 @@ impl ApiClient {
|
||||
jwt_token: String,
|
||||
) -> Result<RegisterDeviceResponse> {
|
||||
let request_payload = RegisterDeviceRequest { hardware_id };
|
||||
|
||||
|
||||
let url = format!("{}/api/v1/devices/register", self.base_url);
|
||||
|
||||
|
||||
println!("🌐 Registering device with backend at: {}", url);
|
||||
println!("📱 Hardware ID: {}", request_payload.hardware_id);
|
||||
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
@ -90,25 +90,28 @@ impl ApiClient {
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send registration request")?;
|
||||
|
||||
|
||||
let status = response.status();
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.context("Failed to read response body")?;
|
||||
|
||||
|
||||
println!("📡 Response status: {}", status);
|
||||
println!("📡 Response body: {}", response_text);
|
||||
|
||||
|
||||
if status.is_success() {
|
||||
let registration_response: RegisterDeviceResponse =
|
||||
serde_json::from_str(&response_text)
|
||||
.context("Failed to parse successful registration response")?;
|
||||
|
||||
|
||||
println!("✅ Device registered successfully!");
|
||||
println!(" Device ID: {}", registration_response.device.id);
|
||||
println!(" User Profile ID: {}", registration_response.device.user_profile_id);
|
||||
|
||||
println!(
|
||||
" User Profile ID: {}",
|
||||
registration_response.device.user_profile_id
|
||||
);
|
||||
|
||||
Ok(registration_response)
|
||||
} else {
|
||||
// Try to parse error response
|
||||
@ -127,20 +130,20 @@ impl ApiClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Health check to verify backend connectivity
|
||||
pub async fn health_check(&self) -> Result<()> {
|
||||
let url = format!("{}/health", self.base_url);
|
||||
|
||||
|
||||
println!("🏥 Checking backend health at: {}", url);
|
||||
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to reach backend health endpoint")?;
|
||||
|
||||
|
||||
if response.status().is_success() {
|
||||
println!("✅ Backend is healthy");
|
||||
Ok(())
|
||||
@ -148,20 +151,16 @@ impl ApiClient {
|
||||
anyhow::bail!("Backend health check failed: HTTP {}", response.status());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Sends a heartbeat to the backend API
|
||||
pub async fn send_heartbeat(
|
||||
&self,
|
||||
hardware_id: String,
|
||||
jwt_token: String,
|
||||
) -> Result<()> {
|
||||
pub async fn send_heartbeat(&self, hardware_id: String, jwt_token: String) -> Result<()> {
|
||||
let request_payload = HeartbeatRequest { hardware_id };
|
||||
|
||||
|
||||
let url = format!("{}/api/v1/devices/heartbeat", self.base_url);
|
||||
|
||||
|
||||
println!("💓 Sending heartbeat to backend at: {}", url);
|
||||
println!("📱 Hardware ID: {}", request_payload.hardware_id);
|
||||
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
@ -171,9 +170,9 @@ impl ApiClient {
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send heartbeat request")?;
|
||||
|
||||
|
||||
let status = response.status();
|
||||
|
||||
|
||||
if status.is_success() {
|
||||
println!("✅ Heartbeat sent successfully!");
|
||||
Ok(())
|
||||
@ -182,7 +181,7 @@ impl ApiClient {
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unable to read response body".to_string());
|
||||
|
||||
|
||||
// Try to parse error response for better error messages
|
||||
if let Ok(error_response) = serde_json::from_str::<ApiErrorResponse>(&response_text) {
|
||||
anyhow::bail!(
|
||||
@ -204,29 +203,29 @@ impl ApiClient {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_register_device_request_serialization() {
|
||||
let request = RegisterDeviceRequest {
|
||||
hardware_id: "TEST_DEVICE_123".to_string(),
|
||||
};
|
||||
|
||||
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
assert!(json.contains("hardwareId"));
|
||||
assert!(json.contains("TEST_DEVICE_123"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_heartbeat_request_serialization() {
|
||||
let request = HeartbeatRequest {
|
||||
hardware_id: "TEST_DEVICE_456".to_string(),
|
||||
};
|
||||
|
||||
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
assert!(json.contains("hardwareId"));
|
||||
assert!(json.contains("TEST_DEVICE_456"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_register_device_response_deserialization() {
|
||||
let json_response = r#"
|
||||
@ -241,14 +240,14 @@ mod tests {
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
|
||||
let response: RegisterDeviceResponse = serde_json::from_str(json_response).unwrap();
|
||||
assert_eq!(response.message, "Device registered successfully");
|
||||
assert_eq!(response.device.id, "device-123");
|
||||
assert_eq!(response.device.user_profile_id, "user-456");
|
||||
assert_eq!(response.device.hardware_id, "TEST_DEVICE_123");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_api_error_response_deserialization() {
|
||||
let json_error = r#"
|
||||
@ -257,9 +256,9 @@ mod tests {
|
||||
"error": "Conflict"
|
||||
}
|
||||
"#;
|
||||
|
||||
|
||||
let error: ApiErrorResponse = serde_json::from_str(json_error).unwrap();
|
||||
assert!(error.message.contains("already been claimed"));
|
||||
assert_eq!(error.error, Some("Conflict".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,15 @@
|
||||
use anyhow::{Result, Context};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use std::fs;
|
||||
use tokio::fs as async_fs;
|
||||
use std::process::Command;
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::multipart;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tokio::fs as async_fs;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::events::{EventBus, SystemEvent, EventPackageArchivedEvent};
|
||||
use crate::api::ApiClient;
|
||||
use crate::network::api::ApiClient;
|
||||
use crate::core::config::ConfigManager;
|
||||
use crate::core::events::{EventBus, EventPackageArchivedEvent, SystemEvent};
|
||||
use serde_json::json;
|
||||
|
||||
/// Configuration for the communication controller
|
||||
#[derive(Debug, Clone)]
|
||||
@ -44,7 +46,7 @@ impl CommunicationController {
|
||||
/// Create a new CommunicationController
|
||||
pub fn new(config: CommunicationConfig, event_bus: EventBus) -> Result<Self> {
|
||||
let api_client = ApiClient::new(config.api_base_url.clone());
|
||||
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
event_bus,
|
||||
@ -57,7 +59,10 @@ impl CommunicationController {
|
||||
println!("📡 Starting communication controller...");
|
||||
println!(" API Base URL: {}", self.config.api_base_url);
|
||||
println!(" Retry attempts: {}", self.config.retry_attempts);
|
||||
println!(" Request timeout: {}s", self.config.request_timeout_seconds);
|
||||
println!(
|
||||
" Request timeout: {}s",
|
||||
self.config.request_timeout_seconds
|
||||
);
|
||||
|
||||
let mut event_receiver = self.event_bus.subscribe();
|
||||
|
||||
@ -65,8 +70,11 @@ impl CommunicationController {
|
||||
match event_receiver.recv().await {
|
||||
Ok(event) => {
|
||||
if let SystemEvent::EventPackageArchived(archive_event) = event.as_ref() {
|
||||
println!("📦 Received EventPackageArchivedEvent: {}", archive_event.event_id);
|
||||
|
||||
println!(
|
||||
"📦 Received EventPackageArchivedEvent: {}",
|
||||
archive_event.event_id
|
||||
);
|
||||
|
||||
if let Err(e) = self.process_archived_event(archive_event.clone()).await {
|
||||
eprintln!("❌ Failed to process archived event: {}", e);
|
||||
}
|
||||
@ -90,41 +98,63 @@ impl CommunicationController {
|
||||
|
||||
// Step 1: Verify the event directory exists
|
||||
if !event.event_directory_path.exists() {
|
||||
anyhow::bail!("Event directory does not exist: {:?}", event.event_directory_path);
|
||||
anyhow::bail!(
|
||||
"Event directory does not exist: {:?}",
|
||||
event.event_directory_path
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Create compressed archive
|
||||
let archive_path = self.create_compressed_archive(&event.event_directory_path, &event.event_id).await?;
|
||||
let archive_path = self
|
||||
.create_compressed_archive(&event.event_directory_path, &event.event_id)
|
||||
.await?;
|
||||
println!("✅ Created compressed archive: {:?}", archive_path);
|
||||
|
||||
// Step 3: Upload with retry logic
|
||||
let upload_result = self.upload_with_retry(&archive_path, &event).await;
|
||||
// Step 3: Build upload context (auth + payload)
|
||||
let (jwt_token, event_data_json) = self.prepare_upload_context(&event).await?;
|
||||
|
||||
// Step 4: Cleanup regardless of upload result
|
||||
// Step 4: Upload with retry logic
|
||||
let upload_result = self
|
||||
.upload_with_retry(&archive_path, &event, &jwt_token, &event_data_json)
|
||||
.await;
|
||||
|
||||
// Step 5: Cleanup regardless of upload result
|
||||
match upload_result {
|
||||
Ok(_) => {
|
||||
println!("✅ Upload successful, cleaning up local files...");
|
||||
|
||||
|
||||
// Delete the compressed archive
|
||||
if let Err(e) = async_fs::remove_file(&archive_path).await {
|
||||
eprintln!("⚠️ Failed to delete archive file {:?}: {}", archive_path, e);
|
||||
eprintln!(
|
||||
"⚠️ Failed to delete archive file {:?}: {}",
|
||||
archive_path, e
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the original event directory
|
||||
if let Err(e) = async_fs::remove_dir_all(&event.event_directory_path).await {
|
||||
eprintln!("⚠️ Failed to delete event directory {:?}: {}", event.event_directory_path, e);
|
||||
eprintln!(
|
||||
"⚠️ Failed to delete event directory {:?}: {}",
|
||||
event.event_directory_path, e
|
||||
);
|
||||
} else {
|
||||
println!("🗑️ Cleaned up event directory: {:?}", event.event_directory_path);
|
||||
println!(
|
||||
"🗑️ Cleaned up event directory: {:?}",
|
||||
event.event_directory_path
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Upload failed after retries: {}", e);
|
||||
|
||||
|
||||
// Still clean up the temporary archive file
|
||||
if let Err(cleanup_err) = async_fs::remove_file(&archive_path).await {
|
||||
eprintln!("⚠️ Failed to delete archive file {:?}: {}", archive_path, cleanup_err);
|
||||
eprintln!(
|
||||
"⚠️ Failed to delete archive file {:?}: {}",
|
||||
archive_path, cleanup_err
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@ -134,10 +164,12 @@ impl CommunicationController {
|
||||
|
||||
/// Create a compressed tar.gz archive of the event directory
|
||||
async fn create_compressed_archive(&self, event_dir: &Path, event_id: &str) -> Result<PathBuf> {
|
||||
let parent_dir = event_dir.parent()
|
||||
let parent_dir = event_dir
|
||||
.parent()
|
||||
.context("Event directory must have a parent directory")?;
|
||||
|
||||
let dir_name = event_dir.file_name()
|
||||
|
||||
let dir_name = event_dir
|
||||
.file_name()
|
||||
.context("Event directory must have a name")?
|
||||
.to_string_lossy();
|
||||
|
||||
@ -167,23 +199,35 @@ impl CommunicationController {
|
||||
anyhow::bail!("Archive file was not created: {:?}", archive_path);
|
||||
}
|
||||
|
||||
let metadata = fs::metadata(&archive_path)
|
||||
.context("Failed to get archive file metadata")?;
|
||||
|
||||
let metadata =
|
||||
fs::metadata(&archive_path).context("Failed to get archive file metadata")?;
|
||||
|
||||
println!("✅ Archive created successfully: {} bytes", metadata.len());
|
||||
|
||||
Ok(archive_path)
|
||||
}
|
||||
|
||||
/// Upload file with exponential backoff retry logic
|
||||
async fn upload_with_retry(&self, archive_path: &Path, event: &EventPackageArchivedEvent) -> Result<()> {
|
||||
async fn upload_with_retry(
|
||||
&self,
|
||||
archive_path: &Path,
|
||||
event: &EventPackageArchivedEvent,
|
||||
jwt_token: &str,
|
||||
event_data_json: &str,
|
||||
) -> Result<()> {
|
||||
let mut delay = Duration::from_secs(self.config.retry_delay_seconds);
|
||||
let max_delay = Duration::from_secs(self.config.max_retry_delay_seconds);
|
||||
|
||||
for attempt in 1..=self.config.retry_attempts {
|
||||
println!("📤 Upload attempt {}/{}", attempt, self.config.retry_attempts);
|
||||
println!(
|
||||
"📤 Upload attempt {}/{}",
|
||||
attempt, self.config.retry_attempts
|
||||
);
|
||||
|
||||
match self.upload_archive(archive_path, event).await {
|
||||
match self
|
||||
.upload_archive(archive_path, event, jwt_token, event_data_json)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("✅ Upload successful on attempt {}", attempt);
|
||||
return Ok(());
|
||||
@ -194,11 +238,15 @@ impl CommunicationController {
|
||||
if attempt < self.config.retry_attempts {
|
||||
println!("⏳ Waiting {}s before retry...", delay.as_secs());
|
||||
sleep(delay).await;
|
||||
|
||||
|
||||
// Exponential backoff: double the delay, but cap at max_delay
|
||||
delay = std::cmp::min(delay * 2, max_delay);
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Upload failed after {} attempts: {}", self.config.retry_attempts, e));
|
||||
return Err(anyhow::anyhow!(
|
||||
"Upload failed after {} attempts: {}",
|
||||
self.config.retry_attempts,
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -208,31 +256,43 @@ impl CommunicationController {
|
||||
}
|
||||
|
||||
/// Upload archive file to the backend API
|
||||
async fn upload_archive(&self, archive_path: &Path, event: &EventPackageArchivedEvent) -> Result<()> {
|
||||
async fn upload_archive(
|
||||
&self,
|
||||
archive_path: &Path,
|
||||
event: &EventPackageArchivedEvent,
|
||||
jwt_token: &str,
|
||||
event_data_json: &str,
|
||||
) -> Result<()> {
|
||||
let url = format!("{}/api/v1/events/upload", self.config.api_base_url);
|
||||
|
||||
|
||||
println!("🌐 Uploading to: {}", url);
|
||||
println!(" File: {:?}", archive_path);
|
||||
|
||||
// Read the archive file
|
||||
let file_content = async_fs::read(archive_path).await
|
||||
let file_content = async_fs::read(archive_path)
|
||||
.await
|
||||
.context("Failed to read archive file")?;
|
||||
|
||||
let file_name = archive_path.file_name()
|
||||
let file_name = archive_path
|
||||
.file_name()
|
||||
.context("Archive file must have a name")?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// Create multipart form data
|
||||
let form = multipart::Form::new()
|
||||
.part("file", multipart::Part::bytes(file_content)
|
||||
.file_name(file_name)
|
||||
.mime_str("application/gzip")?)
|
||||
.part(
|
||||
"file",
|
||||
multipart::Part::bytes(file_content)
|
||||
.file_name(file_name)
|
||||
.mime_str("application/gzip")?,
|
||||
)
|
||||
.text("event_id", event.event_id.clone())
|
||||
.text("trigger_frame_id", event.trigger_frame_id.to_string())
|
||||
.text("total_frames", event.total_frames.to_string())
|
||||
.text("archive_size_bytes", event.archive_size_bytes.to_string())
|
||||
.text("archived_timestamp", event.archived_timestamp.to_rfc3339());
|
||||
.text("archived_timestamp", event.archived_timestamp.to_rfc3339())
|
||||
.text("eventData", event_data_json.to_string());
|
||||
|
||||
// Create HTTP client with timeout
|
||||
let client = reqwest::Client::builder()
|
||||
@ -243,6 +303,7 @@ impl CommunicationController {
|
||||
// Send the request
|
||||
let response = client
|
||||
.post(&url)
|
||||
.bearer_auth(jwt_token)
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await
|
||||
@ -255,19 +316,80 @@ impl CommunicationController {
|
||||
println!("✅ File uploaded successfully!");
|
||||
Ok(())
|
||||
} else {
|
||||
let response_text = response.text().await
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unable to read response body".to_string());
|
||||
|
||||
|
||||
anyhow::bail!("Upload failed with status {}: {}", status, response_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommunicationController {
|
||||
async fn prepare_upload_context(
|
||||
&self,
|
||||
event: &EventPackageArchivedEvent,
|
||||
) -> Result<(String, String)> {
|
||||
let config_manager = ConfigManager::new();
|
||||
let device_config = config_manager
|
||||
.load_config()
|
||||
.context("Failed to load device configuration for upload")?;
|
||||
|
||||
if !device_config.registered {
|
||||
anyhow::bail!("Device is not registered; cannot upload events");
|
||||
}
|
||||
|
||||
let jwt_token = device_config
|
||||
.jwt_token
|
||||
.clone()
|
||||
.context("Device configuration missing JWT token")?;
|
||||
|
||||
let metadata_path = event.event_directory_path.join("metadata.json");
|
||||
let metadata_json = match async_fs::read_to_string(&metadata_path).await {
|
||||
Ok(content) => serde_json::from_str::<serde_json::Value>(&content).ok(),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let event_timestamp = metadata_json
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("detection_timestamp"))
|
||||
.and_then(|value| value.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| event.archived_timestamp.to_rfc3339());
|
||||
|
||||
let mut metadata_payload = json!({
|
||||
"deviceId": device_config.device_id,
|
||||
"eventId": event.event_id,
|
||||
"triggerFrameId": event.trigger_frame_id,
|
||||
"framesCaptured": event.total_frames,
|
||||
"archiveSizeBytes": event.archive_size_bytes,
|
||||
});
|
||||
|
||||
if let Some(value) = metadata_json {
|
||||
if let Some(obj) = metadata_payload.as_object_mut() {
|
||||
obj.insert("eventMetadata".to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
let event_data = json!({
|
||||
"eventType": "meteor",
|
||||
"eventTimestamp": event_timestamp,
|
||||
"metadata": metadata_payload,
|
||||
});
|
||||
|
||||
let event_data_json =
|
||||
serde_json::to_string(&event_data).context("Failed to serialize event data payload")?;
|
||||
|
||||
Ok((jwt_token, event_data_json))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_compressed_archive() {
|
||||
@ -275,11 +397,11 @@ mod tests {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let event_dir = temp_dir.path().join("test_event_123");
|
||||
fs::create_dir_all(&event_dir).unwrap();
|
||||
|
||||
|
||||
// Create some test files
|
||||
fs::write(event_dir.join("video.mp4"), b"fake video data").unwrap();
|
||||
fs::write(event_dir.join("metadata.json"), b"{\"test\": \"data\"}").unwrap();
|
||||
|
||||
|
||||
let detection_dir = event_dir.join("detection_data");
|
||||
fs::create_dir_all(&detection_dir).unwrap();
|
||||
fs::write(detection_dir.join("frame_001.jpg"), b"fake image data").unwrap();
|
||||
@ -290,12 +412,15 @@ mod tests {
|
||||
let comm_controller = CommunicationController::new(config, event_bus).unwrap();
|
||||
|
||||
// Test archive creation
|
||||
let archive_path = comm_controller.create_compressed_archive(&event_dir, "test_event_123").await.unwrap();
|
||||
|
||||
let archive_path = comm_controller
|
||||
.create_compressed_archive(&event_dir, "test_event_123")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify archive exists and has content
|
||||
assert!(archive_path.exists());
|
||||
assert!(archive_path.to_string_lossy().ends_with(".tar.gz"));
|
||||
|
||||
|
||||
let metadata = fs::metadata(&archive_path).unwrap();
|
||||
assert!(metadata.len() > 0);
|
||||
|
||||
@ -313,4 +438,4 @@ mod tests {
|
||||
assert_eq!(config.request_timeout_seconds, 300);
|
||||
assert_eq!(config.heartbeat_interval_seconds, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,9 @@ use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::{fs, time};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::logging::{LogFileManager, StructuredLogger, generate_correlation_id};
|
||||
use crate::core::config::Config;
|
||||
// TODO: Re-enable once logging module is properly implemented
|
||||
// use crate::core::logging::{LogFileManager, StructuredLogger, generate_correlation_id};
|
||||
|
||||
/// Configuration for log upload functionality
|
||||
#[derive(Debug, Clone)]
|
||||
12
meteor-edge-client/src/network/mod.rs
Normal file
12
meteor-edge-client/src/network/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
// Network communication modules
|
||||
|
||||
pub mod api;
|
||||
pub mod communication;
|
||||
// TODO: Re-enable once logging infrastructure is complete
|
||||
// pub mod log_uploader;
|
||||
pub mod websocket_client;
|
||||
|
||||
pub use api::*;
|
||||
pub use communication::*;
|
||||
// pub use log_uploader::*;
|
||||
pub use websocket_client::*;
|
||||
@ -1,15 +1,15 @@
|
||||
use anyhow::{Result, Context, bail};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::{mpsc, RwLock, Mutex};
|
||||
use tokio::time::{sleep, timeout, interval, Instant};
|
||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||
use tokio::time::{interval, sleep, timeout, Instant};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use tracing::{info, warn, error, debug};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::device_registration::DeviceCredentials;
|
||||
use crate::device::registration::DeviceCredentials;
|
||||
|
||||
/// WebSocket connection state
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@ -131,25 +131,25 @@ pub enum CommandStatus {
|
||||
pub enum WebSocketMessage {
|
||||
#[serde(rename = "device-heartbeat")]
|
||||
DeviceHeartbeat { data: DeviceHeartbeat },
|
||||
|
||||
|
||||
#[serde(rename = "device-status-update")]
|
||||
DeviceStatusUpdate { data: serde_json::Value },
|
||||
|
||||
|
||||
#[serde(rename = "device-command")]
|
||||
DeviceCommand(DeviceCommand),
|
||||
|
||||
|
||||
#[serde(rename = "command-response")]
|
||||
CommandResponse { data: CommandResponse },
|
||||
|
||||
|
||||
#[serde(rename = "connected")]
|
||||
Connected {
|
||||
Connected {
|
||||
#[serde(rename = "clientId")]
|
||||
client_id: String,
|
||||
client_id: String,
|
||||
#[serde(rename = "userType")]
|
||||
user_type: String,
|
||||
timestamp: String
|
||||
user_type: String,
|
||||
timestamp: String,
|
||||
},
|
||||
|
||||
|
||||
#[serde(rename = "auth-error")]
|
||||
AuthError { message: String },
|
||||
}
|
||||
@ -158,7 +158,8 @@ pub enum WebSocketMessage {
|
||||
pub struct WebSocketClient {
|
||||
credentials: Arc<RwLock<Option<DeviceCredentials>>>,
|
||||
connection_state: Arc<RwLock<ConnectionState>>,
|
||||
command_handlers: Arc<RwLock<Vec<Box<dyn Fn(DeviceCommand) -> Result<serde_json::Value> + Send + Sync>>>>,
|
||||
command_handlers:
|
||||
Arc<RwLock<Vec<Box<dyn Fn(DeviceCommand) -> Result<serde_json::Value> + Send + Sync>>>>,
|
||||
reconnect_attempts: Arc<Mutex<u32>>,
|
||||
max_reconnect_attempts: u32,
|
||||
reconnect_delay: Duration,
|
||||
@ -206,12 +207,19 @@ impl WebSocketClient {
|
||||
/// Connects to the WebSocket server
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
let credentials = self.credentials.read().await;
|
||||
let creds = credentials.as_ref()
|
||||
let creds = credentials
|
||||
.as_ref()
|
||||
.context("No credentials available for WebSocket connection")?;
|
||||
|
||||
let ws_url = format!("{}/device-realtime",
|
||||
creds.api_endpoints.heartbeat.replace("/heartbeat", "").replace("http", "ws"));
|
||||
|
||||
let ws_url = format!(
|
||||
"{}/device-realtime",
|
||||
creds
|
||||
.api_endpoints
|
||||
.heartbeat
|
||||
.replace("/heartbeat", "")
|
||||
.replace("http", "ws")
|
||||
);
|
||||
|
||||
info!("Connecting to WebSocket: {}", ws_url);
|
||||
self.set_connection_state(ConnectionState::Connecting).await;
|
||||
|
||||
@ -281,12 +289,14 @@ impl WebSocketClient {
|
||||
let connection_state = self.connection_state.clone();
|
||||
let command_handlers = self.command_handlers.clone();
|
||||
let message_sender = message_tx.clone();
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(msg_result) = ws_receiver.next().await {
|
||||
match msg_result {
|
||||
Ok(msg) => {
|
||||
if let Err(e) = Self::handle_message(msg, &command_handlers, &message_sender).await {
|
||||
if let Err(e) =
|
||||
Self::handle_message(msg, &command_handlers, &message_sender).await
|
||||
{
|
||||
error!("Error handling WebSocket message: {}", e);
|
||||
}
|
||||
}
|
||||
@ -317,31 +327,44 @@ impl WebSocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
self.set_connection_state(ConnectionState::Disconnected).await;
|
||||
self.set_connection_state(ConnectionState::Disconnected)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles incoming WebSocket messages
|
||||
async fn handle_message(
|
||||
msg: Message,
|
||||
command_handlers: &Arc<RwLock<Vec<Box<dyn Fn(DeviceCommand) -> Result<serde_json::Value> + Send + Sync>>>>,
|
||||
command_handlers: &Arc<
|
||||
RwLock<Vec<Box<dyn Fn(DeviceCommand) -> Result<serde_json::Value> + Send + Sync>>>,
|
||||
>,
|
||||
message_sender: &mpsc::UnboundedSender<Message>,
|
||||
) -> Result<()> {
|
||||
match msg {
|
||||
Message::Text(text) => {
|
||||
debug!("Received WebSocket message: {}", text);
|
||||
|
||||
let ws_message: WebSocketMessage = serde_json::from_str(&text)
|
||||
.context("Failed to parse WebSocket message")?;
|
||||
|
||||
let ws_message: WebSocketMessage =
|
||||
serde_json::from_str(&text).context("Failed to parse WebSocket message")?;
|
||||
|
||||
match ws_message {
|
||||
WebSocketMessage::Connected { client_id, user_type, timestamp } => {
|
||||
info!("WebSocket connection confirmed: {} as {}", client_id, user_type);
|
||||
WebSocketMessage::Connected {
|
||||
client_id,
|
||||
user_type,
|
||||
timestamp,
|
||||
} => {
|
||||
info!(
|
||||
"WebSocket connection confirmed: {} as {}",
|
||||
client_id, user_type
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
WebSocketMessage::DeviceCommand(command) => {
|
||||
info!("Received device command: {} ({})", command.command, command.command_id);
|
||||
|
||||
info!(
|
||||
"Received device command: {} ({})",
|
||||
command.command, command.command_id
|
||||
);
|
||||
|
||||
// Execute command with registered handlers
|
||||
let handlers = command_handlers.read().await;
|
||||
let mut result = None;
|
||||
@ -362,49 +385,55 @@ impl WebSocketClient {
|
||||
// Send command response
|
||||
let response = CommandResponse {
|
||||
command_id: command.command_id,
|
||||
status: if result.is_some() { CommandStatus::Success } else { CommandStatus::Error },
|
||||
status: if result.is_some() {
|
||||
CommandStatus::Success
|
||||
} else {
|
||||
CommandStatus::Error
|
||||
},
|
||||
result,
|
||||
error,
|
||||
};
|
||||
|
||||
let response_message = WebSocketMessage::CommandResponse { data: response };
|
||||
let response_json = serde_json::to_string(&response_message)?;
|
||||
|
||||
message_sender.send(Message::Text(response_json))
|
||||
|
||||
message_sender
|
||||
.send(Message::Text(response_json))
|
||||
.map_err(|_| anyhow::anyhow!("Failed to send command response"))?;
|
||||
}
|
||||
|
||||
|
||||
WebSocketMessage::AuthError { message } => {
|
||||
error!("WebSocket authentication error: {}", message);
|
||||
return Err(anyhow::anyhow!("Authentication error: {}", message));
|
||||
}
|
||||
|
||||
|
||||
_ => {
|
||||
debug!("Received unhandled message type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Message::Binary(data) => {
|
||||
debug!("Received binary message: {} bytes", data.len());
|
||||
// Handle binary messages if needed
|
||||
}
|
||||
|
||||
|
||||
Message::Ping(data) => {
|
||||
debug!("Received ping, sending pong");
|
||||
message_sender.send(Message::Pong(data))
|
||||
message_sender
|
||||
.send(Message::Pong(data))
|
||||
.map_err(|_| anyhow::anyhow!("Failed to send pong"))?;
|
||||
}
|
||||
|
||||
|
||||
Message::Pong(_) => {
|
||||
debug!("Received pong");
|
||||
}
|
||||
|
||||
|
||||
Message::Close(close_frame) => {
|
||||
info!("WebSocket closed: {:?}", close_frame);
|
||||
return Err(anyhow::anyhow!("WebSocket closed"));
|
||||
}
|
||||
|
||||
|
||||
_ => {
|
||||
debug!("Received unhandled message type");
|
||||
}
|
||||
@ -414,18 +443,21 @@ impl WebSocketClient {
|
||||
}
|
||||
|
||||
/// Starts the heartbeat task
|
||||
async fn start_heartbeat_task(&self, message_sender: mpsc::UnboundedSender<Message>) -> tokio::task::JoinHandle<()> {
|
||||
async fn start_heartbeat_task(
|
||||
&self,
|
||||
message_sender: mpsc::UnboundedSender<Message>,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
let heartbeat_interval = self.heartbeat_interval;
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval = interval(heartbeat_interval);
|
||||
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
|
||||
let heartbeat = Self::collect_heartbeat_data().await;
|
||||
let heartbeat_message = WebSocketMessage::DeviceHeartbeat { data: heartbeat };
|
||||
|
||||
|
||||
match serde_json::to_string(&heartbeat_message) {
|
||||
Ok(json) => {
|
||||
if message_sender.send(Message::Text(json)).is_err() {
|
||||
@ -443,8 +475,8 @@ impl WebSocketClient {
|
||||
|
||||
/// Collects device heartbeat data
|
||||
async fn collect_heartbeat_data() -> DeviceHeartbeat {
|
||||
use sysinfo::{System, Disks};
|
||||
|
||||
use sysinfo::{Disks, System};
|
||||
|
||||
let system = System::new_all();
|
||||
|
||||
let uptime = SystemTime::now()
|
||||
@ -526,9 +558,10 @@ impl WebSocketClient {
|
||||
pub async fn send_status_update(&self, data: serde_json::Value) -> Result<()> {
|
||||
let message = WebSocketMessage::DeviceStatusUpdate { data };
|
||||
let json = serde_json::to_string(&message)?;
|
||||
|
||||
|
||||
if let Some(sender) = &self.message_sender {
|
||||
sender.send(Message::Text(json))
|
||||
sender
|
||||
.send(Message::Text(json))
|
||||
.map_err(|_| anyhow::anyhow!("Failed to send status update"))?;
|
||||
} else {
|
||||
bail!("WebSocket not connected");
|
||||
@ -545,7 +578,8 @@ impl WebSocketClient {
|
||||
let _ = shutdown_sender.send(());
|
||||
}
|
||||
|
||||
self.set_connection_state(ConnectionState::Disconnected).await;
|
||||
self.set_connection_state(ConnectionState::Disconnected)
|
||||
.await;
|
||||
self.message_sender = None;
|
||||
self.shutdown_sender = None;
|
||||
|
||||
@ -565,16 +599,25 @@ impl WebSocketClient {
|
||||
*attempts += 1;
|
||||
|
||||
if *attempts >= self.max_reconnect_attempts {
|
||||
error!("Max reconnection attempts reached ({})", self.max_reconnect_attempts);
|
||||
self.set_connection_state(ConnectionState::Error("Max reconnection attempts exceeded".to_string())).await;
|
||||
error!(
|
||||
"Max reconnection attempts reached ({})",
|
||||
self.max_reconnect_attempts
|
||||
);
|
||||
self.set_connection_state(ConnectionState::Error(
|
||||
"Max reconnection attempts exceeded".to_string(),
|
||||
))
|
||||
.await;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
warn!("WebSocket connection failed (attempt {}/{}): {}",
|
||||
*attempts, self.max_reconnect_attempts, e);
|
||||
|
||||
self.set_connection_state(ConnectionState::Reconnecting).await;
|
||||
|
||||
warn!(
|
||||
"WebSocket connection failed (attempt {}/{}): {}",
|
||||
*attempts, self.max_reconnect_attempts, e
|
||||
);
|
||||
|
||||
self.set_connection_state(ConnectionState::Reconnecting)
|
||||
.await;
|
||||
|
||||
let delay = self.reconnect_delay * (*attempts as u32);
|
||||
sleep(delay).await;
|
||||
}
|
||||
@ -665,4 +708,4 @@ mod tests {
|
||||
let deserialized: DeviceHeartbeat = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.uptime, heartbeat.uptime);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
meteor-edge-client/src/storage/mod.rs
Normal file
5
meteor-edge-client/src/storage/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
// Storage modules
|
||||
|
||||
pub mod storage;
|
||||
|
||||
pub use storage::*;
|
||||
@ -1,12 +1,14 @@
|
||||
use anyhow::{Result, Context};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs as async_fs;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::events::{EventBus, SystemEvent, FrameCapturedEvent, MeteorDetectedEvent, EventPackageArchivedEvent};
|
||||
use crate::core::events::{
|
||||
EventBus, EventPackageArchivedEvent, FrameCapturedEvent, MeteorDetectedEvent, SystemEvent,
|
||||
};
|
||||
|
||||
/// Configuration for the storage controller
|
||||
#[derive(Debug, Clone)]
|
||||
@ -164,7 +166,7 @@ impl StorageController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Periodic cleanup check
|
||||
_ = sleep(cleanup_interval) => {
|
||||
if let Err(e) = self.run_cleanup().await {
|
||||
@ -197,56 +199,63 @@ impl StorageController {
|
||||
/// Handle frame captured events by adding to buffer
|
||||
async fn handle_frame_captured(&mut self, frame_event: FrameCapturedEvent) -> Result<()> {
|
||||
let stored_frame = StoredFrame::from(frame_event);
|
||||
|
||||
|
||||
// Add to circular buffer
|
||||
self.frame_buffer.push_back(stored_frame);
|
||||
|
||||
|
||||
// Maintain buffer size
|
||||
while self.frame_buffer.len() > self.config.frame_buffer_size {
|
||||
self.frame_buffer.pop_front();
|
||||
}
|
||||
|
||||
|
||||
self.total_frames_captured += 1;
|
||||
|
||||
|
||||
if self.total_frames_captured % 100 == 0 {
|
||||
println!("💾 Stored {} frames, buffer size: {}",
|
||||
self.total_frames_captured,
|
||||
println!(
|
||||
"💾 Stored {} frames, buffer size: {}",
|
||||
self.total_frames_captured,
|
||||
self.frame_buffer.len()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle meteor detected events by creating archive
|
||||
async fn handle_meteor_detected(&mut self, meteor_event: MeteorDetectedEvent) -> Result<()> {
|
||||
println!("🌟 Creating meteor event archive for frame #{}", meteor_event.trigger_frame_id);
|
||||
|
||||
println!(
|
||||
"🌟 Creating meteor event archive for frame #{}",
|
||||
meteor_event.trigger_frame_id
|
||||
);
|
||||
|
||||
// Create unique event directory
|
||||
let event_dir = self.create_event_directory(&meteor_event).await?;
|
||||
println!(" Event directory: {:?}", event_dir);
|
||||
|
||||
|
||||
// Extract frames around the detection
|
||||
let (video_frames, detection_frames) = self.extract_event_frames(&meteor_event)?;
|
||||
|
||||
|
||||
// Save video file
|
||||
self.save_video_file(&event_dir, &video_frames).await?;
|
||||
|
||||
|
||||
// Save detection frame images
|
||||
self.save_detection_frames(&event_dir, &detection_frames).await?;
|
||||
|
||||
self.save_detection_frames(&event_dir, &detection_frames)
|
||||
.await?;
|
||||
|
||||
// Create and save metadata
|
||||
self.save_metadata(&event_dir, &meteor_event, &video_frames, &detection_frames).await?;
|
||||
|
||||
self.save_metadata(&event_dir, &meteor_event, &video_frames, &detection_frames)
|
||||
.await?;
|
||||
|
||||
// Calculate archive size
|
||||
let archive_size = self.calculate_directory_size(&event_dir).await?;
|
||||
|
||||
|
||||
// Get event ID from directory name
|
||||
let event_id = event_dir.file_name()
|
||||
let event_id = event_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
|
||||
// Publish EventPackageArchivedEvent
|
||||
let archived_event = EventPackageArchivedEvent::new(
|
||||
event_id,
|
||||
@ -255,71 +264,85 @@ impl StorageController {
|
||||
video_frames.len(),
|
||||
archive_size,
|
||||
);
|
||||
|
||||
if let Err(e) = self.event_bus.publish_event_package_archived(archived_event) {
|
||||
|
||||
if let Err(e) = self
|
||||
.event_bus
|
||||
.publish_event_package_archived(archived_event)
|
||||
{
|
||||
eprintln!("⚠️ Failed to publish EventPackageArchivedEvent: {}", e);
|
||||
} else {
|
||||
println!("📦 Published EventPackageArchivedEvent for archive: {:?}", event_dir);
|
||||
println!(
|
||||
"📦 Published EventPackageArchivedEvent for archive: {:?}",
|
||||
event_dir
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
println!("✅ Meteor event archive created successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create unique directory for meteor event
|
||||
async fn create_event_directory(&self, meteor_event: &MeteorDetectedEvent) -> Result<PathBuf> {
|
||||
let timestamp_str = meteor_event.detection_timestamp
|
||||
let timestamp_str = meteor_event
|
||||
.detection_timestamp
|
||||
.format("%Y%m%d_%H%M%S")
|
||||
.to_string();
|
||||
|
||||
|
||||
let event_id = format!("meteor_{}_{}", timestamp_str, meteor_event.trigger_frame_id);
|
||||
let event_dir = self.config.base_storage_path.join(&event_id);
|
||||
|
||||
|
||||
// Create main event directory
|
||||
async_fs::create_dir_all(&event_dir).await
|
||||
async_fs::create_dir_all(&event_dir)
|
||||
.await
|
||||
.context("Failed to create event directory")?;
|
||||
|
||||
|
||||
// Create detection_data subdirectory
|
||||
let detection_dir = event_dir.join("detection_data");
|
||||
async_fs::create_dir_all(&detection_dir).await
|
||||
async_fs::create_dir_all(&detection_dir)
|
||||
.await
|
||||
.context("Failed to create detection_data directory")?;
|
||||
|
||||
|
||||
Ok(event_dir)
|
||||
}
|
||||
|
||||
/// Extract frames for video and detection analysis
|
||||
fn extract_event_frames(&self, meteor_event: &MeteorDetectedEvent) -> Result<(Vec<StoredFrame>, Vec<StoredFrame>)> {
|
||||
fn extract_event_frames(
|
||||
&self,
|
||||
meteor_event: &MeteorDetectedEvent,
|
||||
) -> Result<(Vec<StoredFrame>, Vec<StoredFrame>)> {
|
||||
if self.frame_buffer.is_empty() {
|
||||
return Ok((Vec::new(), Vec::new()));
|
||||
}
|
||||
|
||||
|
||||
let trigger_frame_id = meteor_event.trigger_frame_id;
|
||||
|
||||
|
||||
// Find the trigger frame in buffer
|
||||
let trigger_pos = self.frame_buffer.iter()
|
||||
let trigger_pos = self
|
||||
.frame_buffer
|
||||
.iter()
|
||||
.position(|frame| frame.frame_id == trigger_frame_id);
|
||||
|
||||
|
||||
let mut video_frames = Vec::new();
|
||||
let mut detection_frames = Vec::new();
|
||||
|
||||
|
||||
match trigger_pos {
|
||||
Some(pos) => {
|
||||
// Extract frames for video (e.g., 2 seconds before and after at 30 FPS = 60 frames each side)
|
||||
let video_range = 60;
|
||||
let start_video = pos.saturating_sub(video_range);
|
||||
let end_video = (pos + video_range + 1).min(self.frame_buffer.len());
|
||||
|
||||
|
||||
for i in start_video..end_video {
|
||||
if let Some(frame) = self.frame_buffer.get(i) {
|
||||
video_frames.push(frame.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Extract detection frames (trigger frame and neighbors)
|
||||
let detection_range = 2; // 2 frames before and after
|
||||
let start_detection = pos.saturating_sub(detection_range);
|
||||
let end_detection = (pos + detection_range + 1).min(self.frame_buffer.len());
|
||||
|
||||
|
||||
for i in start_detection..end_detection {
|
||||
if let Some(frame) = self.frame_buffer.get(i) {
|
||||
detection_frames.push(frame.clone());
|
||||
@ -330,11 +353,11 @@ impl StorageController {
|
||||
// If trigger frame not found, use recent frames
|
||||
let recent_count = 120.min(self.frame_buffer.len()); // Last 4 seconds at 30 FPS
|
||||
let start_idx = self.frame_buffer.len().saturating_sub(recent_count);
|
||||
|
||||
|
||||
for i in start_idx..self.frame_buffer.len() {
|
||||
if let Some(frame) = self.frame_buffer.get(i) {
|
||||
video_frames.push(frame.clone());
|
||||
|
||||
|
||||
// Use last few frames as detection frames
|
||||
if i >= self.frame_buffer.len().saturating_sub(5) {
|
||||
detection_frames.push(frame.clone());
|
||||
@ -343,10 +366,13 @@ impl StorageController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(" Extracted {} video frames, {} detection frames",
|
||||
video_frames.len(), detection_frames.len());
|
||||
|
||||
|
||||
println!(
|
||||
" Extracted {} video frames, {} detection frames",
|
||||
video_frames.len(),
|
||||
detection_frames.len()
|
||||
);
|
||||
|
||||
Ok((video_frames, detection_frames))
|
||||
}
|
||||
|
||||
@ -355,29 +381,34 @@ impl StorageController {
|
||||
if frames.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
let video_path = event_dir.join("video.mp4");
|
||||
|
||||
|
||||
// For now, create a simple text file with frame information
|
||||
// In a real implementation, this would use ffmpeg or similar for actual video encoding
|
||||
let mut video_info = String::new();
|
||||
video_info.push_str("# Meteor Event Video File\n");
|
||||
video_info.push_str(&format!("# Generated at: {}\n", chrono::Utc::now()));
|
||||
video_info.push_str(&format!("# Frame count: {}\n", frames.len()));
|
||||
video_info.push_str(&format!("# Resolution: {}x{}\n", frames[0].width, frames[0].height));
|
||||
video_info.push_str(&format!(
|
||||
"# Resolution: {}x{}\n",
|
||||
frames[0].width, frames[0].height
|
||||
));
|
||||
video_info.push_str("# Frame Information:\n");
|
||||
|
||||
|
||||
for frame in frames {
|
||||
video_info.push_str(&format!("Frame {}: {} bytes at {}\n",
|
||||
frame.frame_id,
|
||||
frame.frame_data.len(),
|
||||
video_info.push_str(&format!(
|
||||
"Frame {}: {} bytes at {}\n",
|
||||
frame.frame_id,
|
||||
frame.frame_data.len(),
|
||||
frame.timestamp
|
||||
));
|
||||
}
|
||||
|
||||
async_fs::write(&video_path, video_info).await
|
||||
|
||||
async_fs::write(&video_path, video_info)
|
||||
.await
|
||||
.context("Failed to save video file")?;
|
||||
|
||||
|
||||
println!(" Saved video placeholder: {:?}", video_path);
|
||||
Ok(())
|
||||
}
|
||||
@ -385,28 +416,38 @@ impl StorageController {
|
||||
/// Save detection frame images
|
||||
async fn save_detection_frames(&self, event_dir: &Path, frames: &[StoredFrame]) -> Result<()> {
|
||||
let detection_dir = event_dir.join("detection_data");
|
||||
|
||||
|
||||
for frame in frames {
|
||||
let frame_filename = format!("frame_{}.jpg", frame.frame_id);
|
||||
let frame_path = detection_dir.join(&frame_filename);
|
||||
|
||||
|
||||
// Save the raw frame data (in a real implementation, this would be proper image encoding)
|
||||
async_fs::write(&frame_path, &frame.frame_data).await
|
||||
async_fs::write(&frame_path, &frame.frame_data)
|
||||
.await
|
||||
.with_context(|| format!("Failed to save detection frame {}", frame.frame_id))?;
|
||||
}
|
||||
|
||||
println!(" Saved {} detection frames to detection_data/", frames.len());
|
||||
|
||||
println!(
|
||||
" Saved {} detection frames to detection_data/",
|
||||
frames.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create and save event metadata
|
||||
async fn save_metadata(&self, event_dir: &Path, meteor_event: &MeteorDetectedEvent,
|
||||
video_frames: &[StoredFrame], detection_frames: &[StoredFrame]) -> Result<()> {
|
||||
let event_id = event_dir.file_name()
|
||||
async fn save_metadata(
|
||||
&self,
|
||||
event_dir: &Path,
|
||||
meteor_event: &MeteorDetectedEvent,
|
||||
video_frames: &[StoredFrame],
|
||||
detection_frames: &[StoredFrame],
|
||||
) -> Result<()> {
|
||||
let event_id = event_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
|
||||
let video_info = VideoInfo {
|
||||
filename: "video.mp4".to_string(),
|
||||
format: "MP4".to_string(),
|
||||
@ -417,29 +458,32 @@ impl StorageController {
|
||||
end_frame_id: video_frames.last().map(|f| f.frame_id).unwrap_or(0),
|
||||
duration_seconds: video_frames.len() as f64 / 30.0, // Assuming 30 FPS
|
||||
};
|
||||
|
||||
|
||||
let detection_info = DetectionInfo {
|
||||
algorithm_version: meteor_event.algorithm_name.clone(),
|
||||
detection_frames: detection_frames.iter().map(|frame| DetectionFrame {
|
||||
frame_id: frame.frame_id,
|
||||
filename: format!("detection_data/frame_{}.jpg", frame.frame_id),
|
||||
confidence: if frame.frame_id == meteor_event.trigger_frame_id {
|
||||
meteor_event.confidence_score
|
||||
} else {
|
||||
0.0 // Unknown confidence for neighboring frames
|
||||
},
|
||||
timestamp: frame.timestamp,
|
||||
}).collect(),
|
||||
detection_frames: detection_frames
|
||||
.iter()
|
||||
.map(|frame| DetectionFrame {
|
||||
frame_id: frame.frame_id,
|
||||
filename: format!("detection_data/frame_{}.jpg", frame.frame_id),
|
||||
confidence: if frame.frame_id == meteor_event.trigger_frame_id {
|
||||
meteor_event.confidence_score
|
||||
} else {
|
||||
0.0 // Unknown confidence for neighboring frames
|
||||
},
|
||||
timestamp: frame.timestamp,
|
||||
})
|
||||
.collect(),
|
||||
confidence_threshold: 0.05, // From detection config
|
||||
};
|
||||
|
||||
|
||||
let system_info = SystemInfo {
|
||||
created_at: chrono::Utc::now(),
|
||||
client_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
frame_buffer_size: self.config.frame_buffer_size,
|
||||
total_frames_captured: self.total_frames_captured,
|
||||
};
|
||||
|
||||
|
||||
let metadata = EventMetadata {
|
||||
event_id,
|
||||
trigger_frame_id: meteor_event.trigger_frame_id,
|
||||
@ -450,14 +494,15 @@ impl StorageController {
|
||||
detection_info,
|
||||
system_info,
|
||||
};
|
||||
|
||||
let metadata_json = serde_json::to_string_pretty(&metadata)
|
||||
.context("Failed to serialize metadata")?;
|
||||
|
||||
|
||||
let metadata_json =
|
||||
serde_json::to_string_pretty(&metadata).context("Failed to serialize metadata")?;
|
||||
|
||||
let metadata_path = event_dir.join("metadata.json");
|
||||
async_fs::write(&metadata_path, metadata_json).await
|
||||
async_fs::write(&metadata_path, metadata_json)
|
||||
.await
|
||||
.context("Failed to save metadata file")?;
|
||||
|
||||
|
||||
println!(" Saved metadata: {:?}", metadata_path);
|
||||
Ok(())
|
||||
}
|
||||
@ -465,30 +510,31 @@ impl StorageController {
|
||||
/// Run periodic cleanup of old event directories
|
||||
async fn run_cleanup(&mut self) -> Result<()> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
|
||||
// Only run cleanup once per day
|
||||
if now.signed_duration_since(self.last_cleanup).num_hours() < 24 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
println!("🧹 Running storage cleanup...");
|
||||
|
||||
|
||||
let retention_duration = chrono::Duration::days(self.config.retention_days as i64);
|
||||
let cutoff_date = now - retention_duration;
|
||||
|
||||
let mut entries = async_fs::read_dir(&self.config.base_storage_path).await
|
||||
|
||||
let mut entries = async_fs::read_dir(&self.config.base_storage_path)
|
||||
.await
|
||||
.context("Failed to read storage directory")?;
|
||||
|
||||
|
||||
let mut deleted_count = 0;
|
||||
|
||||
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
|
||||
|
||||
if path.is_dir() {
|
||||
if let Ok(metadata) = entry.metadata().await {
|
||||
if let Ok(created) = metadata.created() {
|
||||
let created_dt = chrono::DateTime::<chrono::Utc>::from(created);
|
||||
|
||||
|
||||
if created_dt < cutoff_date {
|
||||
println!(" Deleting expired event directory: {:?}", path);
|
||||
if let Err(e) = async_fs::remove_dir_all(&path).await {
|
||||
@ -501,15 +547,18 @@ impl StorageController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.last_cleanup = now;
|
||||
|
||||
|
||||
if deleted_count > 0 {
|
||||
println!("✅ Cleanup completed, deleted {} expired event directories", deleted_count);
|
||||
println!(
|
||||
"✅ Cleanup completed, deleted {} expired event directories",
|
||||
deleted_count
|
||||
);
|
||||
} else {
|
||||
println!("✅ Cleanup completed, no expired directories found");
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -519,18 +568,24 @@ impl StorageController {
|
||||
}
|
||||
|
||||
/// Recursive helper for calculating directory size
|
||||
fn calculate_directory_size_recursive<'a>(&'a self, dir_path: &'a Path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<u64>> + Send + '_>> {
|
||||
fn calculate_directory_size_recursive<'a>(
|
||||
&'a self,
|
||||
dir_path: &'a Path,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<u64>> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let mut total_size = 0u64;
|
||||
|
||||
let mut entries = async_fs::read_dir(dir_path).await
|
||||
|
||||
let mut entries = async_fs::read_dir(dir_path)
|
||||
.await
|
||||
.context("Failed to read directory for size calculation")?;
|
||||
|
||||
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
let metadata = entry.metadata().await
|
||||
let metadata = entry
|
||||
.metadata()
|
||||
.await
|
||||
.context("Failed to get file metadata")?;
|
||||
|
||||
|
||||
if metadata.is_file() {
|
||||
total_size += metadata.len();
|
||||
} else if metadata.is_dir() {
|
||||
@ -538,7 +593,7 @@ impl StorageController {
|
||||
total_size += self.calculate_directory_size_recursive(&path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(total_size)
|
||||
})
|
||||
}
|
||||
@ -568,7 +623,7 @@ pub struct StorageStats {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::events::EventBus;
|
||||
use crate::core::events::EventBus;
|
||||
|
||||
#[test]
|
||||
fn test_storage_config_default() {
|
||||
@ -582,7 +637,7 @@ mod tests {
|
||||
fn test_stored_frame_conversion() {
|
||||
let frame_event = FrameCapturedEvent::new(1, 640, 480, vec![1, 2, 3, 4]);
|
||||
let stored_frame = StoredFrame::from(frame_event.clone());
|
||||
|
||||
|
||||
assert_eq!(stored_frame.frame_id, frame_event.frame_id);
|
||||
assert_eq!(stored_frame.width, frame_event.width);
|
||||
assert_eq!(stored_frame.height, frame_event.height);
|
||||
@ -597,13 +652,13 @@ mod tests {
|
||||
..StorageConfig::default()
|
||||
};
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
|
||||
let controller = StorageController::new(config, event_bus);
|
||||
assert!(controller.is_ok());
|
||||
|
||||
|
||||
// Cleanup
|
||||
if temp_dir.exists() {
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use tracing_subscriber;
|
||||
|
||||
mod hardware_fingerprint;
|
||||
// Define the module inline to make this a standalone binary
|
||||
mod device;
|
||||
use device::HardwareFingerprintService;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@ -9,10 +11,10 @@ async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
println!("🔍 Testing hardware fingerprinting...");
|
||||
|
||||
let mut service = hardware_fingerprint::HardwareFingerprintService::new();
|
||||
|
||||
let mut service = HardwareFingerprintService::new();
|
||||
let fingerprint = service.generate_fingerprint().await?;
|
||||
|
||||
|
||||
println!("✅ Hardware fingerprint generated successfully!");
|
||||
println!("");
|
||||
println!("Hardware Information:");
|
||||
@ -20,27 +22,36 @@ async fn main() -> Result<()> {
|
||||
println!(" Board Serial: {}", fingerprint.board_serial);
|
||||
println!(" MAC Addresses: {}", fingerprint.mac_addresses.join(", "));
|
||||
println!(" Disk UUID: {}", fingerprint.disk_uuid);
|
||||
if let Some(tmp) = &fingerprint.tmp_attestation {
|
||||
if let Some(tmp) = &fingerprint.tpm_attestation {
|
||||
println!(" TPM Attestation: {}...", &tmp[..20]);
|
||||
} else {
|
||||
println!(" TPM Attestation: Not available");
|
||||
}
|
||||
println!(" Computed Hash: {}", fingerprint.computed_hash);
|
||||
|
||||
|
||||
println!("");
|
||||
println!("System Information:");
|
||||
println!(" Hostname: {}", fingerprint.system_info.hostname);
|
||||
println!(" OS: {} {}", fingerprint.system_info.os_name, fingerprint.system_info.os_version);
|
||||
println!(
|
||||
" OS: {} {}",
|
||||
fingerprint.system_info.os_name, fingerprint.system_info.os_version
|
||||
);
|
||||
println!(" Kernel: {}", fingerprint.system_info.kernel_version);
|
||||
println!(" Architecture: {}", fingerprint.system_info.architecture);
|
||||
println!(" Memory: {} MB total, {} MB available",
|
||||
println!(
|
||||
" Memory: {} MB total, {} MB available",
|
||||
fingerprint.system_info.total_memory / 1024 / 1024,
|
||||
fingerprint.system_info.available_memory / 1024 / 1024);
|
||||
println!(" CPU: {} cores, {}",
|
||||
fingerprint.system_info.cpu_count,
|
||||
fingerprint.system_info.cpu_brand);
|
||||
println!(" Disks: {} mounted", fingerprint.system_info.disk_info.len());
|
||||
|
||||
fingerprint.system_info.available_memory / 1024 / 1024
|
||||
);
|
||||
println!(
|
||||
" CPU: {} cores, {}",
|
||||
fingerprint.system_info.cpu_count, fingerprint.system_info.cpu_brand
|
||||
);
|
||||
println!(
|
||||
" Disks: {} mounted",
|
||||
fingerprint.system_info.disk_info.len()
|
||||
);
|
||||
|
||||
// Test fingerprint validation
|
||||
println!("");
|
||||
println!("🔐 Testing fingerprint validation...");
|
||||
@ -50,7 +61,7 @@ async fn main() -> Result<()> {
|
||||
} else {
|
||||
println!("❌ Fingerprint validation: FAILED");
|
||||
}
|
||||
|
||||
|
||||
// Test consistency
|
||||
println!("");
|
||||
println!("🔄 Testing fingerprint consistency...");
|
||||
@ -62,6 +73,6 @@ async fn main() -> Result<()> {
|
||||
println!(" First: {}", fingerprint.computed_hash);
|
||||
println!(" Second: {}", fingerprint2.computed_hash);
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use tracing_subscriber;
|
||||
|
||||
mod hardware_fingerprint;
|
||||
// Import from parent module
|
||||
use super::hardware_fingerprint::HardwareFingerprintService;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@ -9,8 +10,8 @@ async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
println!("🔍 Testing hardware fingerprinting...");
|
||||
|
||||
let mut service = hardware_fingerprint::HardwareFingerprintService::new();
|
||||
|
||||
let mut service = HardwareFingerprintService::new();
|
||||
let fingerprint = service.generate_fingerprint().await?;
|
||||
|
||||
println!("✅ Hardware fingerprint generated successfully!");
|
||||
@ -21,7 +22,7 @@ async fn main() -> Result<()> {
|
||||
println!(" MAC Addresses: {}", fingerprint.mac_addresses.join(", "));
|
||||
println!(" Disk UUID: {}", fingerprint.disk_uuid);
|
||||
if let Some(tpm) = &fingerprint.tpm_attestation {
|
||||
println!(" TPM Attestation: {}...", &tmp[..20]);
|
||||
println!(" TPM Attestation: {}...", &tpm[..20]);
|
||||
} else {
|
||||
println!(" TPM Attestation: Not available");
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod integration_tests {
|
||||
use crate::events::{EventBus, SystemEvent, FrameCapturedEvent, MeteorDetectedEvent};
|
||||
use crate::detection::{DetectionController, DetectionConfig};
|
||||
use crate::core::events::{EventBus, SystemEvent, FrameCapturedEvent, MeteorDetectedEvent};
|
||||
use crate::detection::detector::{DetectionController, DetectionConfig};
|
||||
use tokio::time::{sleep, timeout, Duration};
|
||||
|
||||
#[tokio::test]
|
||||
@ -96,7 +96,7 @@ mod integration_tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_storage_integration() {
|
||||
use crate::storage::{StorageController, StorageConfig};
|
||||
use crate::storage::storage::{StorageController, StorageConfig};
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
@ -191,8 +191,8 @@ mod integration_tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_communication_integration() {
|
||||
use crate::communication::{CommunicationController, CommunicationConfig};
|
||||
use crate::events::EventPackageArchivedEvent;
|
||||
use crate::network::communication::{CommunicationController, CommunicationConfig};
|
||||
use crate::core::events::EventPackageArchivedEvent;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
4
meteor-edge-client/src/tests/mod.rs
Normal file
4
meteor-edge-client/src/tests/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
// Integration tests
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod integration_test;
|
||||
@ -0,0 +1,43 @@
|
||||
[device]
|
||||
registered = true
|
||||
hardware_id = "SIM_DEVICE"
|
||||
device_id = "sim-device-001"
|
||||
user_profile_id = "user-sim"
|
||||
registered_at = "2025-01-01T00:00:00Z"
|
||||
jwt_token = "dummy-jwt-token"
|
||||
|
||||
[api]
|
||||
base_url = "http://localhost:3000"
|
||||
upload_endpoint = "/api/v1/events"
|
||||
timeout_seconds = 30
|
||||
|
||||
[camera]
|
||||
source = "device"
|
||||
device_index = 0
|
||||
fps = 30.0
|
||||
width = 640
|
||||
height = 480
|
||||
|
||||
[detection]
|
||||
algorithm = "brightness_diff"
|
||||
threshold = 0.3
|
||||
buffer_frames = 150
|
||||
|
||||
[storage]
|
||||
base_path = "./meteor_events"
|
||||
max_storage_gb = 10.0
|
||||
retention_days = 7
|
||||
pre_event_seconds = 2
|
||||
post_event_seconds = 2
|
||||
|
||||
[communication]
|
||||
heartbeat_interval_seconds = 300
|
||||
upload_batch_size = 1
|
||||
retry_attempts = 3
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
directory = "./meteor_logs"
|
||||
max_file_size_mb = 10
|
||||
max_files = 5
|
||||
upload_enabled = false
|
||||
3
meteor-edge-client/tmp-home/.rustup/settings.toml
Normal file
3
meteor-edge-client/tmp-home/.rustup/settings.toml
Normal file
@ -0,0 +1,3 @@
|
||||
version = "12"
|
||||
|
||||
[overrides]
|
||||
@ -0,0 +1,43 @@
|
||||
[device]
|
||||
registered = true
|
||||
hardware_id = "SIM_DEVICE"
|
||||
device_id = "sim-device-001"
|
||||
user_profile_id = "user-sim"
|
||||
registered_at = "2025-01-01T00:00:00Z"
|
||||
jwt_token = "dummy-jwt-token"
|
||||
|
||||
[api]
|
||||
base_url = "http://localhost:3000"
|
||||
upload_endpoint = "/api/v1/events"
|
||||
timeout_seconds = 30
|
||||
|
||||
[camera]
|
||||
source = "device"
|
||||
device_index = 0
|
||||
fps = 30.0
|
||||
width = 640
|
||||
height = 480
|
||||
|
||||
[detection]
|
||||
algorithm = "brightness_diff"
|
||||
threshold = 0.3
|
||||
buffer_frames = 150
|
||||
|
||||
[storage]
|
||||
base_path = "./meteor_events"
|
||||
max_storage_gb = 10.0
|
||||
retention_days = 7
|
||||
pre_event_seconds = 2
|
||||
post_event_seconds = 2
|
||||
|
||||
[communication]
|
||||
heartbeat_interval_seconds = 300
|
||||
upload_batch_size = 1
|
||||
retry_attempts = 3
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
directory = "./meteor_logs"
|
||||
max_file_size_mb = 10
|
||||
max_files = 5
|
||||
upload_enabled = false
|
||||
BIN
meteor-edge-client/video.mp4
Normal file
BIN
meteor-edge-client/video.mp4
Normal file
Binary file not shown.
43
tmp-home/.config/meteor-client/config.toml
Normal file
43
tmp-home/.config/meteor-client/config.toml
Normal file
@ -0,0 +1,43 @@
|
||||
[device]
|
||||
registered = true
|
||||
hardware_id = "SIM_DEVICE"
|
||||
device_id = "sim-device-001"
|
||||
user_profile_id = "user-sim"
|
||||
registered_at = "2025-01-01T00:00:00Z"
|
||||
jwt_token = "dummy-jwt-token"
|
||||
|
||||
[api]
|
||||
base_url = "http://localhost:3000"
|
||||
upload_endpoint = "/api/v1/events"
|
||||
timeout_seconds = 30
|
||||
|
||||
[camera]
|
||||
source = "device"
|
||||
device_index = 0
|
||||
fps = 30.0
|
||||
width = 640
|
||||
height = 480
|
||||
|
||||
[detection]
|
||||
algorithm = "brightness_diff"
|
||||
threshold = 0.3
|
||||
buffer_frames = 150
|
||||
|
||||
[storage]
|
||||
base_path = "./meteor_events"
|
||||
max_storage_gb = 10.0
|
||||
retention_days = 7
|
||||
pre_event_seconds = 2
|
||||
post_event_seconds = 2
|
||||
|
||||
[communication]
|
||||
heartbeat_interval_seconds = 300
|
||||
upload_batch_size = 1
|
||||
retry_attempts = 3
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
directory = "./meteor_logs"
|
||||
max_file_size_mb = 10
|
||||
max_files = 5
|
||||
upload_enabled = false
|
||||
@ -0,0 +1,43 @@
|
||||
[device]
|
||||
registered = true
|
||||
hardware_id = "SIM_DEVICE"
|
||||
device_id = "sim-device-001"
|
||||
user_profile_id = "user-sim"
|
||||
registered_at = "2025-01-01T00:00:00Z"
|
||||
jwt_token = "dummy-jwt-token"
|
||||
|
||||
[api]
|
||||
base_url = "http://localhost:3000"
|
||||
upload_endpoint = "/api/v1/events"
|
||||
timeout_seconds = 30
|
||||
|
||||
[camera]
|
||||
source = "device"
|
||||
device_index = 0
|
||||
fps = 30.0
|
||||
width = 640
|
||||
height = 480
|
||||
|
||||
[detection]
|
||||
algorithm = "brightness_diff"
|
||||
threshold = 0.3
|
||||
buffer_frames = 150
|
||||
|
||||
[storage]
|
||||
base_path = "./meteor_events"
|
||||
max_storage_gb = 10.0
|
||||
retention_days = 7
|
||||
pre_event_seconds = 2
|
||||
post_event_seconds = 2
|
||||
|
||||
[communication]
|
||||
heartbeat_interval_seconds = 300
|
||||
upload_batch_size = 1
|
||||
retry_attempts = 3
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
directory = "./meteor_logs"
|
||||
max_file_size_mb = 10
|
||||
max_files = 5
|
||||
upload_enabled = false
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user