You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

781 lines
25 KiB

#!/usr/bin/env python3
"""
Database Schema Generator - vzug-e-hinge
=========================================
Creates and initializes the SQLite database schema.
Includes:
- Schema creation
- Initial data population
- Database connection management
- Size monitoring
Author: Kynsight
Version: 1.0.0
"""
import sqlite3
import os
import sys
from pathlib import Path
from datetime import datetime
# =============================================================================
# Configuration
# =============================================================================
DEFAULT_DB_PATH = "./database/ehinge.db"
MAX_DB_SIZE_BYTES = 2 * 1024 * 1024 * 1024 # 2 GB limit
# =============================================================================
# Database Schema
# =============================================================================
SCHEMA_SQL = """
-- =============================================================================
-- 1. EXISTING COMMAND TABLES
-- =============================================================================
CREATE TABLE IF NOT EXISTS "uart_commands" (
"command_id" INTEGER,
"command_name" TEXT NOT NULL UNIQUE,
"description" TEXT,
"category" TEXT,
"hex_string" TEXT NOT NULL,
"expected_response" TEXT,
"timeout_ms" INTEGER,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"is_active" BOOLEAN DEFAULT 1,
PRIMARY KEY("command_id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "i2c_commands" (
"command_id" INTEGER,
"command_name" TEXT NOT NULL UNIQUE,
"description" TEXT,
"category" TEXT,
"operation" TEXT NOT NULL,
"register" TEXT NOT NULL,
"hex_string" TEXT NOT NULL,
"device_address" TEXT NOT NULL,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"is_active" BOOLEAN DEFAULT 1,
PRIMARY KEY("command_id" AUTOINCREMENT)
);
-- =============================================================================
-- 2. INTERFACE PROFILES
-- =============================================================================
CREATE TABLE IF NOT EXISTS "interface_profiles" (
"profile_id" INTEGER PRIMARY KEY AUTOINCREMENT,
"profile_name" TEXT UNIQUE NOT NULL,
"description" TEXT,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"last_modified" TIMESTAMP,
-- UART Command Interface
"uart_command_mode" TEXT,
"uart_command_port" TEXT,
"uart_command_baud" INTEGER,
"uart_command_data_bits" INTEGER,
"uart_command_stop_bits" INTEGER,
"uart_command_parity" TEXT,
"uart_command_timeout_ms" INTEGER,
-- UART Logger Interface
"uart_logger_enable" BOOLEAN DEFAULT 1,
"uart_logger_mode" TEXT,
"uart_logger_port" TEXT,
"uart_logger_baud" INTEGER,
"uart_logger_data_bits" INTEGER,
"uart_logger_stop_bits" INTEGER,
"uart_logger_parity" TEXT,
"uart_logger_timeout_ms" INTEGER,
"uart_logger_grace_ms" INTEGER,
-- UART Logger Packet Detection
"uart_logger_packet_detect_enable" BOOLEAN DEFAULT 0,
"uart_logger_packet_detect_start" TEXT,
"uart_logger_packet_detect_length" INTEGER,
"uart_logger_packet_detect_end" TEXT,
-- I2C Interface
"i2c_port" TEXT,
"i2c_slave_address" TEXT,
"i2c_slave_read_register" TEXT,
"i2c_slave_read_length" INTEGER
);
-- =============================================================================
-- 3. SESSION PROFILES
-- =============================================================================
CREATE TABLE IF NOT EXISTS "session_profiles" (
"profile_id" INTEGER PRIMARY KEY AUTOINCREMENT,
"profile_name" TEXT UNIQUE NOT NULL,
"description" TEXT,
"command_sequence" TEXT,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"last_modified" TIMESTAMP,
"print_command_rx" INTEGER DEFAULT 0
);
-- =============================================================================
-- 4. SESSIONS
-- =============================================================================
CREATE TABLE IF NOT EXISTS "sessions" (
"session_id" TEXT PRIMARY KEY,
"session_name" TEXT NOT NULL,
"session_date" TEXT NOT NULL,
"description" TEXT,
"notes" TEXT DEFAULT '',
"interface_profile_id" INTEGER,
"session_profile_id" INTEGER,
"status" TEXT DEFAULT 'active',
"total_runs" INTEGER DEFAULT 0,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"ended_at" TIMESTAMP,
FOREIGN KEY ("interface_profile_id") REFERENCES "interface_profiles"("profile_id"),
FOREIGN KEY ("session_profile_id") REFERENCES "session_profiles"("profile_id")
);
-- =============================================================================
-- 5. TELEMETRY_RAW
-- =============================================================================
CREATE TABLE IF NOT EXISTS "telemetry_raw" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"session_id" TEXT NOT NULL,
"session_name" TEXT NOT NULL,
"run_no" INTEGER NOT NULL,
"run_command_id" INTEGER NOT NULL,
"t_ns" INTEGER NOT NULL,
"time_ms" REAL,
"uart_raw_packet" BLOB,
"i2c_raw_bytes" BLOB,
"i2c_zero_ref" INTEGER DEFAULT 0,
FOREIGN KEY ("session_id") REFERENCES "sessions"("session_id"),
FOREIGN KEY ("run_command_id") REFERENCES "uart_commands"("command_id")
);
-- =============================================================================
-- 6. TELEMETRY_DECODED
-- =============================================================================
CREATE TABLE IF NOT EXISTS "telemetry_decoded" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"session_id" TEXT NOT NULL,
"session_name" TEXT NOT NULL,
"run_no" INTEGER NOT NULL,
"run_command_id" INTEGER NOT NULL,
"t_ns" INTEGER NOT NULL,
"time_ms" REAL,
-- UART decoded
"motor_current" INTEGER,
"encoder_value" INTEGER,
"relative_encoder_value" INTEGER,
"v24_pec_diff" INTEGER,
"pwm" INTEGER,
-- I2C decoded
"i2c_raw14" INTEGER,
"i2c_zero_raw14" INTEGER,
"i2c_delta_raw14" INTEGER,
"i2c_angle_deg" REAL,
"i2c_zero_angle_deg" REAL,
"i2c_zero_ref" INTEGER DEFAULT 0,
-- Derived
"angular_velocity" REAL,
"angular_acceleration" REAL,
FOREIGN KEY ("session_id") REFERENCES "sessions"("session_id"),
FOREIGN KEY ("run_command_id") REFERENCES "uart_commands"("command_id")
);
-- =============================================================================
-- 7. INDEXES
-- =============================================================================
CREATE INDEX IF NOT EXISTS "idx_telemetry_decoded_session"
ON "telemetry_decoded" ("session_id", "run_no");
CREATE INDEX IF NOT EXISTS "idx_telemetry_raw_session"
ON "telemetry_raw" ("session_id", "run_no");
CREATE INDEX IF NOT EXISTS "idx_telemetry_decoded_session_name"
ON "telemetry_decoded" ("session_name", "run_no");
CREATE INDEX IF NOT EXISTS "idx_telemetry_decoded_time"
ON "telemetry_decoded" ("t_ns");
-- ============================================
-- 8. GUI PROFILES
-- ============================================
CREATE TABLE IF NOT EXISTS gui_profiles (
profile_id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_name TEXT UNIQUE NOT NULL,
is_active BOOLEAN DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Theme
theme_name TEXT DEFAULT 'dark',
primary_color TEXT DEFAULT '#1f77b4',
background_color TEXT DEFAULT '#2e2e2e',
text_color TEXT DEFAULT '#ffffff',
-- Font
font_family TEXT DEFAULT 'Arial',
font_size INTEGER DEFAULT 10,
-- Window
window_width INTEGER DEFAULT 1280,
window_height INTEGER DEFAULT 720
);
-- ============================================
-- 9. DATABASE METADATA
-- ============================================
CREATE TABLE IF NOT EXISTS database_metadata (
key TEXT PRIMARY KEY,
value TEXT
);
"""
# =============================================================================
# Initial Data
# =============================================================================
INITIAL_DATA_SQL = """
-- ============================================
-- initial data population
-- ============================================
insert or ignore into database_metadata (key, value) values
('max_size_bytes', '2147483648'),
('created_at', datetime('now')),
('version', '1.0'),
('schema_version', '1.0.0');
-- default gui profile (dark theme)
insert or ignore into gui_profiles (
profile_name, is_active, theme_name,
primary_color, background_color, text_color,
font_family, font_size,
window_width, window_height
) values (
'dark theme', 1, 'dark',
'#1f77b4', '#2e2e2e', '#ffffff',
'arial', 10,
1280, 720
);
-- uart commands (from aurt_commands.csv)
insert or ignore into uart_commands (command_name, description, category, hex_string) values
('query', 'query current action and feedback status', 'action', 'dd 22 50 48 02 41 52 09'),
('reset', 'reset current action state', 'action', 'dd 22 50 48 02 41 43 18'),
('open', 'open door fully', 'door', 'dd 22 50 48 02 43 4f 16'),
('close', 'close door fully', 'door', 'dd 22 50 48 02 43 43 1a'),
('partopen', 'open door partially', 'door', 'dd 22 50 48 02 43 47 1e'),
('ref drive', 'perform reference drive', 'door', 'dd 22 50 48 02 43 52 0b'),
('status', 'query basic error flags', 'error', 'dd 22 50 48 01 45 5c'),
('software version', 'get firmware/software version', 'info', 'dd 22 50 48 01 44 5d'),
('read', 'read extended error state', 'error', 'dd 22 50 48 02 45 52 0d'),
('clear', 'clear all active error flags', 'error', 'dd 22 50 48 02 45 43 1c'),
('init status', 'check initialization complete', 'status', 'dd 22 50 48 01 49 50'),
('motor status: query', 'get motor state (ready, driving, etc.)', 'status', 'dd 22 50 48 01 4d 54'),
('off', 'power down system in safe state', 'power', 'dd 22 50 48 01 4f 56'),
('power reset', 'power off and reset controller', 'power', 'dd 22 50 48 02 4f 72 27'),
('parameter start', 'start motor parameter upload process', 'motor', 'dd 22 50 48 04 50 73 74 61 2a'),
('parameter version: set', 'store current parameter version id', 'motor', 'dd 22 50 48 0c 51 31 32 33 34 35 36 37 2d 41 41 20 78'),
('parameter version: query', 'get stored parameter version', 'motor', 'dd 22 50 48 01 51 48'),
('temperature', 'get device internal temperature', 'sensor', 'dd 22 50 48 01 54 4d'),
('door switch', 'check if door is open/closed via sensor', 'sensor', 'dd 22 50 48 01 53 4a'),
('hardware version', 'get hardware revision info', 'info', 'dd 22 50 48 01 68 71'),
('manufacturer', 'get manufacturer identification', 'info', 'dd 22 50 48 01 69 70'),
('send id 02', 'write model identifier 02', 'identification', 'dd 22 50 48 03 6d 30 32 74'),
('send id 10', 'write model identifier 10', 'identification', 'dd 22 50 48 03 6d 31 30 77'),
('check', 'check if in programming/normal state', 'programming', 'dd 22 50 48 02 70 67 0d'),
('exit', 'exit programming state', 'programming', 'dd 22 50 48 04 70 73 30 30 1f'),
('production', 'enter production mode', 'mode', 'dd 22 50 48 02 74 73 1d'),
('mode reset', 'reset production mode settings', 'mode', 'dd 22 50 48 02 74 72 1c'),
('mode query', 'check if in production mode', 'mode', 'dd 22 50 48 01 74 6d'),
('logging mode', 'direct command for logging', 'test', 'dd 22 50 48 03 50 53 50 48'),
('read paramter set 01', 'reads motors paramter set 1', 'motor', 'dd 22 50 48 04 50 72 30 31 3f'),
('read paramter set 25', 'reads motors paramter set 25', 'motor', 'dd 22 50 48 04 50 72 32 35 39');
-- i2c commands (from i2c_commands.csv)
insert or ignore into i2c_commands (command_name, description, category, operation, register, hex_string, device_address) values
('read angle', '14-bit angle (msb at 0xfe, lsb at 0xff), 2 bytes', 'sensor', 'read', '0xfe', '02', '0x40'),
('read magnitude', 'cordic magnitude, 2 bytes (0xfc/0xfd)', 'sensor', 'read', '0xfc', '02', '0x40'),
('read diagnostics', 'diagnostic flags: ocf, cof, comp high/low (0xfb)', 'sensor', 'read', '0xfb', '01', '0x40'),
('read agc', 'automatic gain control value (0xfa)', 'sensor', 'read', '0xfa', '01', '0x40'),
('read prog control', 'programming control register (pe/verify/burn at 0x03)', 'system', 'read', '0x03', '01', '0x40'),
('read i2c address', 'i2c slave address bits (volatile reg 0x15)', 'system', 'read', '0x15', '01', '0x40'),
('read zeropos high', 'zero position high byte (reg 0x16)', 'sensor', 'read', '0x16', '01', '0x40'),
('read zeropos low', 'zero position low 6 bits (reg 0x17)', 'sensor', 'read', '0x17', '01', '0x40'),
('read angle msb', 'angle msb only (0xfe)', 'sensor', 'read', '0xfe', '01', '0x40'),
('read angle lsb', 'angle lsb only (0xff)', 'sensor', 'read', '0xff', '01', '0x40'),
('progctrl: pe=1', 'enable programming (bit0=1) — volatile', 'system', 'write', '0x03', '01', '0x40'),
('progctrl: pe=0', 'disable programming (bit0=0)', 'system', 'write', '0x03', '00', '0x40'),
('progctrl: verify=1', 'set verify bit (bit6=1) to reload otp to internal regs', 'system', 'write', '0x03', '40', '0x40'),
('progctrl: verify=0', 'clear verify bit (bit6=0)', 'system', 'write', '0x03', '00', '0x40'),
('zeropos: clear high', 'set zero position high byte to 0x00 (no burn)', 'sensor', 'write', '0x16', '00', '0x40'),
('zeropos: clear low', 'set zero position low 6 bits to 0x00 (no burn)', 'sensor', 'write', '0x17', '00', '0x40'),
('I2C Addr: Set Bits=0', 'Set I2C address bits in 0x15 to 0x00 (A1/A2 pins still apply)', 'System', 'write', '0x15', '00', '0x40');
"""
# =============================================================================
# Helper Functions
# =============================================================================
def create_database(db_path: str, overwrite: bool = False) -> bool:
"""
Create and initialize database.
Args:
db_path: Path to database file
overwrite: If True, delete existing database first
Returns:
True on success, False on failure
"""
try:
# Create directory if needed
db_dir = os.path.dirname(db_path)
if db_dir:
os.makedirs(db_dir, exist_ok=True)
print(f"[✓] Created directory: {db_dir}")
# Overwrite if requested
if overwrite and os.path.exists(db_path):
os.remove(db_path)
print(f"[✓] Removed existing database: {db_path}")
# Connect to database
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
print(f"[✓] Connected to database: {db_path}")
# Enable foreign keys
conn.execute("PRAGMA foreign_keys = ON")
# Create schema
print("[...] Creating schema...")
conn.executescript(SCHEMA_SQL)
conn.commit()
print("[✓] Schema created successfully!")
# Initalt Data
print("[...] Intial Data Upload...")
conn.executescript(INITIAL_DATA_SQL)
print("[✓] Inital Data uploaded successfully!")
# Get database info
size_bytes = os.path.getsize(db_path)
size_mb = size_bytes / (1024 * 1024)
# Get table counts
cursor = conn.cursor()
tables = [
'uart_commands',
'i2c_commands',
'interface_profiles',
'session_profiles',
'sessions',
'telemetry_raw',
'telemetry_decoded'
]
print("\n" + "=" * 60)
print("DATABASE INFORMATION")
print("=" * 60)
print(f"Path: {db_path}")
print(f"Size: {size_mb:.2f} MB")
print(f"\nTABLES:")
for table in tables:
try:
cursor.execute(f"SELECT COUNT(*) FROM {table}")
count = cursor.fetchone()[0]
print(f" {table:25s} {count:6d} rows")
except:
print(f" {table:25s} ERROR")
print("=" * 60)
conn.close()
return True
except Exception as e:
print(f"[✗] ERROR: {e}")
return False
def check_database_health(db_path: str):
"""
Check database health and integrity.
Args:
db_path: Path to database file
"""
if not os.path.exists(db_path):
print(f"[✗] Database not found: {db_path}")
return
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check integrity
cursor.execute("PRAGMA integrity_check")
result = cursor.fetchone()[0]
if result == "ok":
print("[✓] Database integrity: OK")
else:
print(f"[✗] Database integrity: {result}")
# Check foreign keys
cursor.execute("PRAGMA foreign_key_check")
fk_errors = cursor.fetchall()
if not fk_errors:
print("[✓] Foreign keys: OK")
else:
print(f"[✗] Foreign key errors found: {len(fk_errors)}")
conn.close()
except Exception as e:
print(f"[✗] Health check failed: {e}")
# =============================================================================
# Database Manager Class
# =============================================================================
class DatabaseManager:
"""
Database manager for vzug-e-hinge project.
Provides a high-level interface for database operations:
- Connection management
- Schema initialization
- Size monitoring
- Health checking
Usage:
db = DatabaseManager("database/ehinge.db")
db.initialize()
conn = db.get_connection()
# Use connection...
db.close()
"""
def __init__(self, db_path: str = DEFAULT_DB_PATH):
"""
Initialize database manager.
Args:
db_path: Path to SQLite database file
"""
self.db_path = db_path
self.connection = None
def initialize(self, overwrite: bool = False) -> bool:
"""
Initialize database (create schema and tables).
Args:
overwrite: If True, delete and recreate database
Returns:
True if successful, False otherwise
"""
success = create_database(self.db_path, overwrite)
if success:
# Open connection after initialization
self.connect()
return success
def connect(self) -> bool:
"""
Connect to database.
Returns:
True if successful, False otherwise
"""
try:
if self.connection:
self.connection.close()
self.connection = sqlite3.connect(self.db_path)
# Enable foreign keys
self.connection.execute("PRAGMA foreign_keys = ON")
return True
except Exception as e:
print(f"Error connecting to database: {e}")
return False
def get_connection(self) -> sqlite3.Connection:
"""
Get database connection.
Opens connection if not already open.
Returns:
sqlite3.Connection object
"""
if not self.connection:
self.connect()
return self.connection
def close(self):
"""Close database connection."""
if self.connection:
self.connection.close()
self.connection = None
def check_size(self) -> tuple:
"""
Check database size.
Returns:
Tuple of (size_bytes, percentage, status)
- size_bytes: Database size in bytes
- percentage: Percentage of MAX_DB_SIZE_BYTES
- status: 'ok', 'warning', or 'critical'
"""
try:
size_bytes = os.path.getsize(self.db_path)
percentage = (size_bytes / MAX_DB_SIZE_BYTES) * 100
if percentage < 50:
status = 'ok'
elif percentage < 80:
status = 'warning'
else:
status = 'critical'
return (size_bytes, percentage, status)
except Exception as e:
print(f"Error checking database size: {e}")
return (0, 0, 'error')
def format_size(self, size_bytes: int) -> str:
"""
Format byte size to human-readable string.
Args:
size_bytes: Size in bytes
Returns:
Formatted string (e.g., "1.5 MB", "2.3 GB")
"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} TB"
def vacuum(self):
"""
Vacuum database (reclaim unused space).
This can significantly reduce database size after deletions.
"""
try:
conn = self.get_connection()
conn.execute("VACUUM")
conn.commit()
print("Database vacuumed successfully")
except Exception as e:
print(f"Error vacuuming database: {e}")
def check_health(self):
"""Run database health check."""
check_database_health(self.db_path)
def get_table_list(self) -> list:
"""
Get list of all tables in database.
Returns:
List of table names
"""
try:
conn = self.get_connection()
cursor = conn.execute("""
SELECT name FROM sqlite_master
WHERE type='table'
ORDER BY name
""")
return [row[0] for row in cursor.fetchall()]
except Exception as e:
print(f"Error getting table list: {e}")
return []
def get_table_info(self, table_name: str) -> list:
"""
Get column information for a table.
Args:
table_name: Name of table
Returns:
List of tuples with column info (cid, name, type, notnull, dflt_value, pk)
"""
try:
conn = self.get_connection()
cursor = conn.execute(f"PRAGMA table_info({table_name})")
return cursor.fetchall()
except Exception as e:
print(f"Error getting table info: {e}")
return []
def get_row_count(self, table_name: str) -> int:
"""
Get number of rows in a table.
Args:
table_name: Name of table
Returns:
Number of rows
"""
try:
conn = self.get_connection()
cursor = conn.execute(f"SELECT COUNT(*) FROM {table_name}")
return cursor.fetchone()[0]
except Exception as e:
print(f"Error getting row count: {e}")
return 0
def __enter__(self):
"""Context manager entry."""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.close()
def __del__(self):
"""Destructor - ensure connection is closed."""
self.close()
# =============================================================================
# Main
# =============================================================================
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(
description='Initialize vzug-e-hinge database'
)
parser.add_argument(
'--path',
default=DEFAULT_DB_PATH,
help=f'Database path (default: {DEFAULT_DB_PATH})'
)
parser.add_argument(
'--overwrite',
action='store_true',
help='Delete and recreate database'
)
parser.add_argument(
'--check',
action='store_true',
help='Check database health'
)
args = parser.parse_args()
print("=" * 60)
print("vzug-e-hinge Database Initialization")
print("=" * 60)
print()
if args.check:
check_database_health(args.path)
return
# Create database
success = create_database(args.path, args.overwrite)
if success:
print("\n[✓] Database initialization complete!")
print(f"\nYou can now use the database at: {args.path}")
sys.exit(0)
else:
print("\n[✗] Database initialization failed!")
sys.exit(1)
if __name__ == "__main__":
main()
# Example usage of DatabaseManager:
#
# from database.init_database import DatabaseManager
#
# # Create manager
# db = DatabaseManager("database/ehinge.db")
#
# # Initialize database
# db.initialize()
#
# # Get connection
# conn = db.get_connection()
#
# # Use connection
# cursor = conn.execute("SELECT * FROM sessions")
#
# # Check size
# size, percentage, status = db.check_size()
# print(f"Database size: {size} bytes ({percentage:.1f}%)")
#
# # Close when done
# db.close()
#
# # Or use context manager:
# with DatabaseManager("database/ehinge.db") as db:
# conn = db.get_connection()
# # Use connection...
# # Automatically closed when exiting context