signalk-parquet
Version:
SignalK plugin to save marine data directly to Parquet files with regimen-based control
691 lines (529 loc) • 22.6 kB
Markdown
# SignalK Parquet Data Store
**Version 0.5.0-beta.6**
A comprehensive TypeScript-based SignalK plugin that saves marine data directly to Parquet files with regimen-based control, web interface for querying, and S3 upload capabilities.
## Features
- **Smart Data Types**: Intelligent Parquet schema detection preserves native data types (DOUBLE, BOOLEAN) instead of forcing everything to strings
- **Command Management**: Register, execute, and manage SignalK commands with automatic path configuration
- **Regimen-Based Data Collection**: Control data collection with command-based regimens
- **Multi-Vessel Support**: Wildcard vessel contexts (`vessels.*`) with MMSI-based exclusion filtering
- **Multiple File Formats**: Support for Parquet, JSON, and CSV output formats (querying in parquet only)
- **Web Interface**: Responsive web interface for data exploration and configuration
- **DuckDB Integration**: Query Parquet files directly with SQL
- **History API Integration**: Full SignalK History API implementation for historical data queries
- **S3 Integration**: Upload files to Amazon S3 with configurable timing
- **Daily Consolidation**: Automatic daily file consolidation
- **Real-time Buffering**: Efficient data buffering with configurable thresholds
- **Source Filtering**: Filter data by SignalK source labels (bypasses server arbitration for raw data access)
- **Context Support**: Support for multiple vessel contexts with exclusion controls
## Installation
### Install from GitHub
```bash
# Navigate to folder
cd ~/.signalk/node_modules/
# Install from npm (after publishing)
npm install signalk-parquet
# Or install from GitHub
npm install motamman/signalk-parquet
# Restart SignalK
sudo systemctl restart signalk
```
## ⚠️ IMPORTANT: Consolidation Bug Fix
**THIS VERSION FIXES A RECURSIVE BUG THAT WAS CREATING NESTED PROCESSED DIRECTORIES AND REPEATEDLY PROCESSING THE SAME FILES. THIS SHOULD FIX THAT PROBLEM BUT ANY `processed` FOLDERS NESTED INSIDE A `processed` FOLDER SHOULD BE MANUALLY DELETED.**
### Cleaning Up Nested Processed Directories
If you're upgrading from a previous version, you may have nested processed directories that need cleanup:
```bash
# Check for nested processed directories
find data -name "*processed*" -type d | head -20
# See the deepest nesting levels
find data -name "*processed*" -type d | awk -F'/' '{print NF-1, $0}' | sort -nr | head -5
# Count files in nested processed directories
find data -path "*/processed/processed/*" -type f | wc -l
# Remove ALL nested processed directories (RECOMMENDED)
find data -name "processed" -type d -exec rm -rf {} +
# Verify cleanup completed
find data -path "*/processed/processed/*" -type f | wc -l # Should show 0
```
**Note**: The processed directories only contain files that were moved during consolidation - removing them does not delete your original data.
### Development Setup
```bash
# Clone or copy the signalk-parquet directory
cd signalk-parquet
# Install dependencies
npm install
# Build the TypeScript code
npm run build
# Copy to SignalK plugins directory
cp -r . ~/.signalk/node_modules/signalk-parquet/
# Restart SignalK
sudo systemctl restart signalk
```
### Production Build
```bash
# Build for production
npm run build
# The compiled JavaScript will be in the dist/ directory
```
## Development Scripts
- `npm run build` - Compile TypeScript to JavaScript
- `npm run watch` - Watch for changes and recompile
- `npm run clean` - Remove compiled files
- `npm run dev` - Build and watch for changes
##
## Configuration
### Plugin Configuration
Navigate to **SignalK Admin → Server → Plugin Config → SignalK Parquet Data Store**
Configure basic plugin settings (path configuration is managed separately in the web interface):
| Setting | Description | Default |
|---------|-------------|---------|
| **Buffer Size** | Number of records to buffer before writing | 1000 |
| **Save Interval** | How often to save buffered data (seconds) | 30 |
| **Output Directory** | Directory to save data files | SignalK data directory |
| **Filename Prefix** | Prefix for generated filenames | `signalk_data` |
| **File Format** | Output format (parquet, json, csv) | `parquet` |
| **Retention Days** | Days to keep processed files | 7 |
### S3 Upload Configuration
Configure S3 upload settings in the plugin configuration:
| Setting | Description | Default |
|---------|-------------|---------|
| **Enable S3 Upload** | Enable uploading to Amazon S3 | `false` |
| **Upload Timing** | When to upload (realtime/consolidation) | `consolidation` |
| **S3 Bucket** | Name of S3 bucket | - |
| **AWS Region** | AWS region for S3 bucket | `us-east-1` |
| **Key Prefix** | S3 object key prefix | - |
| **Access Key ID** | AWS credentials (optional) | - |
| **Secret Access Key** | AWS credentials (optional) | - |
| **Delete After Upload** | Delete local files after upload | `false` |
## Path Configuration
**Important**: Path configuration is managed exclusively through the web interface, not in the SignalK admin interface. This provides a more intuitive interface for managing data collection paths.
### Accessing Path Configuration
1. Navigate to: `http://localhost:3000/plugins/signalk-parquet`
2. Click the **⚙️ Path Configuration** tab
### Adding Data Paths
Use the web interface to configure which SignalK paths to collect:
1. Click **➕ Add New Path**
2. Configure the path settings:
- **SignalK Path**: The SignalK data path (e.g., `navigation.position`)
- **Always Enabled**: Collect data regardless of regimen state
- **Regimen Control**: Command name that controls collection
- **Source Filter**: Only collect from specific sources
- **Context**: SignalK context (`vessels.self`, `vessels.*`, or specific vessel)
- **Exclude MMSI**: For `vessels.*` context, exclude specific MMSI numbers
3. Click **✅ Add Path**
### Managing Existing Paths
- **Edit Path**: Click ✏️ Edit button to modify path settings
- **Delete Path**: Click 🗑️ Remove button to delete a path
- **Refresh**: Click 🔄 Refresh Paths to reload configuration
- **Show/Hide Commands**: Toggle button to show/hide command paths in the table
### Command Management
The plugin now streamlines command management with automatic path configuration:
1. **Register Command**: Commands are automatically registered with enabled path configurations
2. **Start Command**: Click **Start** button to activate a command regimen
3. **Stop Command**: Click **Stop** button to deactivate a command regimen
4. **Remove Command**: Click **Remove** button to delete a command and its path configuration
This eliminates the previous 3-step process of registering commands, adding paths, and enabling them separately.
### Path Configuration Storage
Path configurations are stored separately from plugin configuration in:
```
~/.signalk/signalk-parquet/webapp-config.json
```
This allows for:
- Independent management of path configurations
- Better separation of concerns
- Easier backup and migration of path settings
- More intuitive web-based configuration interface
### Regimen-Based Control
Regimens allow you to control data collection based on SignalK commands:
**Example**: Weather data collection with source filtering
```json
{
"path": "environment.wind.angleApparent",
"enabled": false,
"regimen": "captureWeather",
"source": "mqtt-weatherflow-udp",
"context": "vessels.self"
}
```
**Note**: Source filtering accesses raw data streams before SignalK server arbitration, allowing collection of data from specific sources that might otherwise be filtered out.
**Multi-Vessel Example**: Collect navigation data from all vessels except specific MMSI numbers
```json
{
"path": "navigation.position",
"enabled": true,
"context": "vessels.*",
"excludeMMSI": ["123456789", "987654321"]
}
```
**Command Path**: Command paths are automatically created when registering commands
```json
{
"path": "commands.captureWeather",
"enabled": true,
"context": "vessels.self"
}
```
This path will only collect data when the command `commands.captureWeather` is active.
## TypeScript Architecture
### Type Safety
The plugin uses comprehensive TypeScript interfaces:
```typescript
interface PluginConfig {
bufferSize: number;
saveIntervalSeconds: number;
outputDirectory: string;
filenamePrefix: string;
fileFormat: 'json' | 'csv' | 'parquet';
paths: PathConfig[];
s3Upload: S3UploadConfig;
}
interface PathConfig {
path: string;
enabled: boolean;
regimen?: string;
source?: string;
context: string;
excludeMMSI?: string[];
}
interface DataRecord {
received_timestamp: string;
signalk_timestamp: string;
context: string;
path: string;
value: any;
source_label?: string;
meta?: string;
}
```
### Plugin State Management
The plugin maintains typed state:
```typescript
interface PluginState {
unsubscribes: Array<() => void>;
dataBuffers: Map<string, DataRecord[]>;
activeRegimens: Set<string>;
subscribedPaths: Set<string>;
parquetWriter?: ParquetWriter;
s3Client?: any;
currentConfig?: PluginConfig;
}
```
### Express Router Types
API routes are fully typed:
```typescript
router.get('/api/paths',
(_: TypedRequest, res: TypedResponse<PathsApiResponse>) => {
// Typed request/response handling
}
);
```
## Data Output Structure
### File Organization
```
output_directory/
├── vessels/
│ └── self/
│ ├── navigation/
│ │ ├── position/
│ │ │ ├── signalk_data_20250716T120000.parquet
│ │ │ └── signalk_data_20250716_consolidated.parquet
│ │ └── speedOverGround/
│ └── environment/
│ └── wind/
│ └── angleApparent/
└── processed/
└── [moved files after consolidation]
```
### Data Schema
Each record contains:
| Field | Type | Description |
|-------|------|-------------|
| `received_timestamp` | string | When the plugin received the data |
| `signalk_timestamp` | string | Original SignalK timestamp |
| `context` | string | SignalK context (e.g., `vessels.self`) |
| `path` | string | SignalK path |
| `value` | DOUBLE/BOOLEAN/INT64/UTF8 | **Smart typed values** - numbers stored as DOUBLE, booleans as BOOLEAN, etc. |
| `value_json` | string | JSON representation for complex values |
| `source` | string | Complete source information |
| `source_label` | string | Source label |
| `source_type` | string | Source type |
| `source_pgn` | number | PGN number (if applicable) |
| `meta` | string | Metadata information |
#### Smart Data Types
The plugin now intelligently detects and preserves native data types:
- **Numbers**: Stored as `DOUBLE` (floating point) or `INT64` (integers)
- **Booleans**: Stored as `BOOLEAN`
- **Strings**: Stored as `UTF8`
- **Objects**: Serialized to JSON and stored as `UTF8`
- **Mixed Types**: Falls back to `UTF8` when a path contains multiple data types
This provides better compression, faster queries, and proper type safety for data analysis.
## Web Interface
### Features
- **Path Configuration**: Manage data collection paths with multi-vessel support
- **Command Management**: Streamlined command registration and control
- **Data Exploration**: Browse available data paths
- **SQL Queries**: Execute DuckDB queries against Parquet files
- **History API**: Query historical data using SignalK History API endpoints
- **S3 Status**: Test S3 connectivity and configuration
- **Responsive Design**: Works on desktop and mobile
- **MMSI Filtering**: Exclude specific vessels from wildcard contexts
### API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/paths` | GET | List available data paths |
| `/api/files/:path` | GET | List files for a path |
| `/api/sample/:path` | GET | Sample data from a path |
| `/api/query` | POST | Execute SQL query |
| `/api/config/paths` | GET/POST/PUT/DELETE | Manage path configurations |
| `/api/test-s3` | POST | Test S3 connection |
| `/api/health` | GET | Health check |
| `/signalk/v1/history/values` | GET | SignalK History API - Get historical values |
| `/signalk/v1/history/contexts` | GET | SignalK History API - Get available contexts |
| `/signalk/v1/history/paths` | GET | SignalK History API - Get available paths |
## DuckDB Integration
### Query Examples
```sql
-- Get latest 10 records from navigation position
SELECT * FROM '/path/to/navigation/position/*.parquet'
ORDER BY received_timestamp DESC LIMIT 10;
-- Count total records
SELECT COUNT(*) FROM '/path/to/navigation/position/*.parquet';
-- Filter by source
SELECT * FROM '/path/to/environment/wind/*.parquet'
WHERE source_label = 'mqtt-weatherflow-udp'
ORDER BY received_timestamp DESC LIMIT 100;
-- Aggregate by hour
SELECT
DATE_TRUNC('hour', received_timestamp::timestamp) as hour,
AVG(value::double) as avg_value,
COUNT(*) as record_count
FROM '/path/to/data/*.parquet'
GROUP BY hour
ORDER BY hour;
```
## History API Integration
The plugin provides full SignalK History API compatibility, allowing you to query historical data using standard SignalK API endpoints.
### Available Endpoints
| Endpoint | Description | Parameters |
|----------|-------------|------------|
| `/signalk/v1/history/values` | Get historical values for specified paths | `context`, `from`, `to`, `paths` |
| `/signalk/v1/history/contexts` | Get available vessel contexts | `from`, `to` (optional) |
| `/signalk/v1/history/paths` | Get available SignalK paths | `from`, `to` (optional) |
### Query Examples
**Get historical position data:**
```bash
curl "http://localhost:3000/signalk/v1/history/values?context=vessels.self&from=2025-01-01T00:00:00Z&to=2025-01-02T00:00:00Z&paths=navigation.position"
```
**Get wind data with multiple paths:**
```bash
curl "http://localhost:3000/signalk/v1/history/values?context=vessels.self&from=2025-01-01T00:00:00Z&to=2025-01-01T06:00:00Z&paths=environment.wind.angleApparent,environment.wind.speedApparent"
```
**Get available contexts:**
```bash
curl "http://localhost:3000/signalk/v1/history/contexts"
```
### Response Format
The History API returns data in standard SignalK format:
```json
{
"context": "vessels.self",
"data": [
{
"timestamp": "2025-01-01T12:00:00.000Z",
"values": [
{
"path": "navigation.position",
"value": {
"latitude": 37.7749,
"longitude": -122.4194
}
}
]
}
]
}
```
## S3 Integration
### Upload Timing
**Real-time Upload**: Files are uploaded immediately after creation
```json
{
"s3Upload": {
"enabled": true,
"timing": "realtime"
}
}
```
**Consolidation Upload**: Files are uploaded after daily consolidation
```json
{
"s3Upload": {
"enabled": true,
"timing": "consolidation"
}
}
```
### S3 Key Structure
With prefix `marine-data/`:
```
marine-data/vessels/self/navigation/position/signalk_data_20250716_consolidated.parquet
marine-data/vessels/self/environment/wind/angleApparent/signalk_data_20250716_120000.parquet
```
## File Consolidation
The plugin automatically consolidates files daily at midnight UTC:
1. **File Discovery**: Finds all files for the previous day
2. **Merging**: Combines files by SignalK path
3. **Sorting**: Sorts records by timestamp
4. **Cleanup**: Moves source files to `processed/` directory
5. **S3 Upload**: Uploads consolidated files if configured
## Performance Characteristics
- **Memory Usage**: Configurable buffer sizes (default 1000 records)
- **Disk I/O**: Efficient batch writes with configurable intervals
- **CPU Usage**: Minimal - mostly I/O bound operations
- **Network**: Optional S3 uploads with retry logic
## Development
### Project Structure
```
signalk-parquet/
├── src/
│ ├── index.ts # Main plugin logic
│ ├── types.ts # TypeScript interfaces
│ ├── parquet-writer.ts # File writing logic
│ ├── HistoryAPI.ts # SignalK History API implementation
│ └── HistoryAPI-types.ts # History API type definitions
├── dist/ # Compiled JavaScript
├── public/
│ ├── index.html # Web interface
│ └── parquet.png # Plugin icon
├── tsconfig.json # TypeScript configuration
├── package.json # Dependencies and scripts
└── README.md # This file
```
### Adding New Features
1. **Update Types**: Add interfaces to `src/types.ts`
2. **Implement Logic**: Add functionality to appropriate files
3. **Add Tests**: Create unit tests for new features
4. **Update Documentation**: Update README and inline comments
### Type Checking
The plugin uses strict TypeScript configuration:
```json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"strictNullChecks": true
}
}
```
## Troubleshooting
### Common Issues
**Build Errors**
```bash
# Clean and rebuild
npm run clean
npm run build
```
**DuckDB Not Available**
- Check that `@duckdb/node-api` is installed
- Verify Node.js version compatibility (>=16.0.0)
**S3 Upload Failures**
- Verify AWS credentials and permissions
- Check S3 bucket exists and is accessible
- Test connection using web interface
**No Data Collection**
- Verify path configurations are correct
- Check if regimens are properly activated
- Review SignalK logs for subscription errors
### Debug Mode
Enable debug logging in SignalK:
```json
{
"settings": {
"debug": "signalk-parquet*"
}
}
```
### Runtime Dependencies
- `@dsnp/parquetjs`: Parquet file format support
- `@duckdb/node-api`: SQL query engine
- `@aws-sdk/client-s3`: S3 upload functionality
- `fs-extra`: Enhanced file system operations
- `glob`: File pattern matching
- `express`: Web server framework
### Development Dependencies
- `typescript`: TypeScript compiler
- `@types/node`: Node.js type definitions
- `@types/express`: Express type definitions
- `@types/fs-extra`: fs-extra type definitions
## License
MIT License - See LICENSE file for details.
## Testing
Comprehensive testing procedures are documented in `TESTING.md`. The testing guide covers:
- Installation and build verification
- Plugin configuration testing
- Web interface functionality
- Data collection validation
- Regimen control testing
- File output verification
- S3 integration testing
- API endpoint testing
- Performance testing
- Error handling validation
### Quick Test
```bash
# Test plugin health
curl http://localhost:3000/plugins/signalk-parquet/api/health
# Test path configuration
curl http://localhost:3000/plugins/signalk-parquet/api/config/paths
# Test data collection
curl http://localhost:3000/plugins/signalk-parquet/api/paths
# Test History API
curl "http://localhost:3000/signalk/v1/history/contexts"
```
For detailed testing procedures, see [TESTING.md](TESTING.md).
## TODO
- [x] Implement startup consolidation for missed previous days (exclude current day)
- [x] Add history API integration
- [ ] Clean up data output of sourcing
- [ ] Add Grafana integration
- [ ] Create SignalK app store listing with screenshots
## Contributing
1. Fork the repository
2. Create a feature branch
3. Add TypeScript types for new features
4. Include tests and documentation
5. Follow the testing procedures in `TESTING.md`
6. Submit a pull request
## Changelog
### Version 0.5.0-beta.6
- **📊 History API Integration**: Implemented full SignalK History API compatibility with endpoints for `/signalk/v1/history/values`, `/signalk/v1/history/contexts`, and `/signalk/v1/history/paths`
- **🔍 Parquet File Queries**: Added robust querying of historical data from Parquet files using DuckDB integration
- **🧹 Code Quality**: Fixed linting and prettier formatting errors throughout the codebase
- **🏗️ Type Safety**: Enhanced type definitions with HistoryAPI-types.ts for better development experience
### Version 0.5.0-beta.5
- **🔧 Fixed BigInt Serialization**: Resolved BigInt serialization errors that prevented Parquet file consolidation from completing
- **🌩️ Fixed S3 Client Initialization**: Resolved timing issues where AWS SDK imports were not ready during plugin startup
- **📤 Smart S3 Upload**: Added timestamp-based conflict resolution - only uploads files newer than existing S3 versions
- **🚀 Automatic S3 Catch-up**: Added startup upload of existing consolidated files to S3 (catch-up mechanism)
- **🔍 Enhanced Debug Logging**: Added comprehensive debug logging for S3 operations and consolidation processes
- **🛡️ Improved Error Handling**: Enhanced error handling and logging throughout S3 upload pipeline
### Version 0.5.0-beta.4
- Previous release with consolidation improvements
### Version 0.5.0-beta.3
- **🔥 CRITICAL BUG FIX**: Fixed recursive consolidation bug that created infinite nested `/processed/processed/processed/...` directories
- **🎯 Smart Data Types**: Implemented intelligent Parquet schema detection that preserves native data types (DOUBLE, BOOLEAN, INT64) instead of forcing everything to UTF8 strings
- **⚡ Performance**: Numeric data now stored as native types for better compression and faster queries
- **🛠️ Type Safety**: Fixed schema field type access bug (`primitiveType` → `type`) in data conversion logic
### Version 0.5.0-beta.2
- **🐛 Fixed SignalK Source Naming**: Corrected delta updates to use `$source` instead of `source` object format for proper source label recognition
- **🧹 Reduced Debug Spam**: Removed redundant path subscription checks in delta processing
- **🔧 Enhanced Date Processing**: Updated date extraction logic and startup consolidation for missed previous days
### Version 0.5.0-beta.1
- Complete TypeScript rewrite with enhanced type safety
- **Source filtering with raw data access**: Bypass SignalK server arbitration for specific sources
- **Streambundle API integration**: Improved performance with app.streambundle instead of subscription manager
- **Backward compatibility**: Automatic config migration for older path configurations
- **Multi-vessel support**: `vessels.*` wildcard context with MMSI exclusion filtering
- **Enhanced web interface**: Restored source filtering UI with improved controls
- **Streamlined command management**: Automatic path configuration for commands
- **SignalK API compliance**: Proper subscription patterns for vessel contexts
- Performance optimizations and better error handling