|
| 1 | +# OpenRocket Motor Database |
| 2 | + |
| 3 | +This repository acts as the dynamic backend for OpenRocket's thrust curve data. |
| 4 | + |
| 5 | +## Deployment & API Endpoints |
| 6 | + |
| 7 | +This repository utilizes **GitHub Pages** to act as a static Content Delivery Network (CDN) for the compiled motor data. This ensures high availability and fast downloads for OpenRocket users without requiring a dedicated backend server. |
| 8 | + |
| 9 | +## Architecture |
| 10 | + |
| 11 | +1. **Source:** John Coker's [ThrustCurve.org](https://www.thrustcurve.org/) API (thanks a lot for this incredible resource!). |
| 12 | +2. **Automation:** GitHub Actions runs weekly. |
| 13 | +3. **Process:** |
| 14 | + * Checks for new motors. |
| 15 | + * Downloads raw `.eng` and `.rse` files to `data/`. |
| 16 | + * Compiles them into a SQLite database (`motors.db`). |
| 17 | + * GZips and hashes the database. |
| 18 | +4. **Distribution:** The resulting `motors.db.gz` and `metadata.json` are published to GitHub Pages. |
| 19 | + |
| 20 | +The deployment process follows a split-branch strategy: |
| 21 | + |
| 22 | +1. **`main` branch:** Contains the source code, scripts, and the raw text cache (`.eng`/`.rse` files). |
| 23 | +2. **`gh-pages` branch:** Contains **only** the build artifacts (`motors.db.gz` and `metadata.json`). |
| 24 | + |
| 25 | +Every time the [Update Workflow](.github/workflows/update-motors.yml) runs (scheduled weekly), it commits the new raw data to `main`, compiles the database, and force-pushes the binary artifacts to `gh-pages`. |
| 26 | + |
| 27 | +### Public Endpoints |
| 28 | + |
| 29 | +The OpenRocket client (and other interested 3rd parties) can access the live data at the following URLs: |
| 30 | + |
| 31 | +| File | URL | Description | |
| 32 | +| :--- | :--- | :--- | |
| 33 | +| **Manifest** | `https://openrocket.github.io/motor-database/metadata.json` | Lightweight JSON file. Contains `database_version`, `generated_at`, `last_checked`, `sha256`, and the signature fields for verification. Checked by the client on startup. | |
| 34 | +| **Database** | `https://openrocket.github.io/motor-database/motors.db.gz` | GZipped SQLite database. Downloaded by the client *only* if the manifest version differs from the local cache. | |
| 35 | + |
| 36 | +### Manifest Format |
| 37 | +The `metadata.json` structure is defined as follows: |
| 38 | +`database_version` is a sortable timestamp in `YYYYMMDDHHMMSS` format. |
| 39 | +```json |
| 40 | +{ |
| 41 | + "schema_version": 2, |
| 42 | + "database_version": 20251225140000, |
| 43 | + "generated_at": "2025-12-25T14:00:00.000000", |
| 44 | + "last_checked": "2025-12-27T14:00:00.000000", |
| 45 | + "motor_count": 1033, |
| 46 | + "curve_count": 1320, |
| 47 | + "sha256": "a1b2c3d4e5f6...", |
| 48 | + "sha256_gz": "a1b2c3d4e5f6...", |
| 49 | + "sig": "base64-signature...", |
| 50 | + "download_url": "https://openrocket.github.io/motor-database/motors.db.gz" |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +Signature notes: |
| 55 | +- `sig` is an Ed25519 signature over `openrocket-motordb-v1\n{database_version}\n{sha256_gz}\n`. |
| 56 | +- `sha256_gz` is the SHA-256 of the gzipped database; `sha256` is kept for backward compatibility. |
| 57 | +- `key_id` is optional for key rotation. |
| 58 | + |
| 59 | +## Database Schema |
| 60 | + |
| 61 | +The SQLite schema lives in `schema/V1__initial_schema.sql` and is optimized for fast client lookups. Foreign keys are enabled with cascade deletes. |
| 62 | + |
| 63 | +### Entity Relationship |
| 64 | + |
| 65 | +``` |
| 66 | +manufacturers motors thrust_curves thrust_data |
| 67 | +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ |
| 68 | +│ id (PK) │◄─────│ mfr_id (FK) │ │ id (PK) │◄───────│ curve_id(FK) │ |
| 69 | +│ name │ 1:N │ id (PK) │◄────│ motor_id(FK) │ 1:N │ id (PK) │ |
| 70 | +│ abbrev │ │ tc_motor_id │ 1:N │ tc_simfile_id│ │ time_seconds │ |
| 71 | +└──────────────┘ │ designation │ │ source │ │ force_newtons│ |
| 72 | + │ impulse_class│ │ format │ └──────────────┘ |
| 73 | + │ ... │ │ ... │ |
| 74 | + └──────────────┘ └──────────────┘ |
| 75 | +``` |
| 76 | + |
| 77 | +- **manufacturers** → **motors**: One manufacturer has many motors |
| 78 | +- **motors** → **thrust_curves**: One motor can have multiple thrust curves (different sources/measurements) |
| 79 | +- **thrust_curves** → **thrust_data**: One curve has many time/thrust data points |
| 80 | + |
| 81 | +### Tables and Columns |
| 82 | + |
| 83 | +**meta** |
| 84 | + |
| 85 | +| Column | Type | Notes | |
| 86 | +| :--- | :--- | :--- | |
| 87 | +| key | TEXT | Primary key | |
| 88 | +| value | TEXT | Required | |
| 89 | + |
| 90 | +Keys stored: `schema_version`, `database_version`, `generated_at`, `motor_count`, `curve_count`. |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +**manufacturers** |
| 95 | + |
| 96 | +| Column | Type | Notes | |
| 97 | +| :--- | :--- | :--- | |
| 98 | +| id | INTEGER | Primary key, autoincrement | |
| 99 | +| name | TEXT | Required, unique (e.g. "AeroTech") | |
| 100 | +| abbrev | TEXT | Short name (e.g. "AT") | |
| 101 | + |
| 102 | +--- |
| 103 | + |
| 104 | +**motors** |
| 105 | + |
| 106 | +| Column | Type | Notes | |
| 107 | +| :--- | :--- | :--- | |
| 108 | +| id | INTEGER | Primary key, autoincrement | |
| 109 | +| manufacturer_id | INTEGER | FK to `manufacturers.id` | |
| 110 | +| tc_motor_id | TEXT | ThrustCurve.org motor ID | |
| 111 | +| designation | TEXT | Required (e.g. "H128W") | |
| 112 | +| common_name | TEXT | Display name (e.g. "H128") | |
| 113 | +| impulse_class | TEXT | Letter class: A, B, C, ... O | |
| 114 | +| diameter | REAL | Motor diameter in mm | |
| 115 | +| length | REAL | Motor length in mm | |
| 116 | +| total_impulse | REAL | Total impulse in Ns | |
| 117 | +| avg_thrust | REAL | Average thrust in N | |
| 118 | +| max_thrust | REAL | Maximum thrust in N | |
| 119 | +| burn_time | REAL | Burn time in seconds | |
| 120 | +| propellant_weight | REAL | Propellant weight in grams | |
| 121 | +| total_weight | REAL | Total weight in grams | |
| 122 | +| type | TEXT | "SU" (single-use), "reload", "hybrid" | |
| 123 | +| delays | TEXT | Available delays, e.g. "0,6,10,14" | |
| 124 | +| case_info | TEXT | Case info, e.g. "RMS 38/360" | |
| 125 | +| prop_info | TEXT | Propellant info, e.g. "White Lightning" | |
| 126 | +| sparky | INTEGER | 1 if sparky motor, 0 otherwise | |
| 127 | +| info_url | TEXT | URL to motor info page | |
| 128 | +| data_files | INTEGER | Number of data files on ThrustCurve | |
| 129 | +| updated_on | TEXT | Last update date from ThrustCurve | |
| 130 | +| description | TEXT | RASP file comments (header notes, newlines removed) | |
| 131 | +| source | TEXT | Data source (e.g. "thrustcurve.org", "manual") | |
| 132 | + |
| 133 | +--- |
| 134 | + |
| 135 | +**thrust_curves** |
| 136 | + |
| 137 | +Each motor can have multiple thrust curves from different sources (certification, manufacturer, user submissions). |
| 138 | + |
| 139 | +| Column | Type | Notes | |
| 140 | +| :--- | :--- | :--- | |
| 141 | +| id | INTEGER | Primary key, autoincrement | |
| 142 | +| motor_id | INTEGER | FK to `motors.id`, cascade delete | |
| 143 | +| tc_simfile_id | TEXT | ThrustCurve.org simfile ID | |
| 144 | +| source | TEXT | "cert", "mfr", or "user" | |
| 145 | +| format | TEXT | "RASP" or "RSE" | |
| 146 | +| license | TEXT | "PD", "free", or other | |
| 147 | +| info_url | TEXT | URL to simfile info page | |
| 148 | +| data_url | TEXT | URL to download simfile | |
| 149 | +| total_impulse | REAL | Calculated total impulse (Ns) | |
| 150 | +| avg_thrust | REAL | Calculated average thrust (N) | |
| 151 | +| max_thrust | REAL | Calculated max thrust (N) | |
| 152 | +| burn_time | REAL | Calculated burn time (s) | |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +**thrust_data** |
| 157 | + |
| 158 | +Time/thrust data points for each thrust curve. |
| 159 | + |
| 160 | +| Column | Type | Notes | |
| 161 | +| :--- | :--- | :--- | |
| 162 | +| id | INTEGER | Primary key, autoincrement | |
| 163 | +| curve_id | INTEGER | FK to `thrust_curves.id`, cascade delete | |
| 164 | +| time_seconds | REAL | Time in seconds | |
| 165 | +| force_newtons | REAL | Thrust force in Newtons | |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +### Indices |
| 170 | + |
| 171 | +| Index | Table | Columns | Purpose | |
| 172 | +| :--- | :--- | :--- | :--- | |
| 173 | +| idx_motor_mfr | motors | manufacturer_id | Filter by manufacturer | |
| 174 | +| idx_motor_diameter | motors | diameter | Filter by size | |
| 175 | +| idx_motor_impulse | motors | total_impulse | Filter by impulse | |
| 176 | +| idx_motor_impulse_class | motors | impulse_class | Filter by class (A-O) | |
| 177 | +| idx_motor_tc_id | motors | tc_motor_id | Lookup by ThrustCurve ID | |
| 178 | +| idx_curve_motor | thrust_curves | motor_id | Get curves for a motor | |
| 179 | +| idx_curve_simfile | thrust_curves | tc_simfile_id | Lookup by simfile ID | |
| 180 | +| idx_thrust_curve | thrust_data | curve_id | Get data for a curve | |
| 181 | + |
| 182 | +## Manual Usage |
| 183 | + |
| 184 | +1. `pip install -r scripts/requirements.txt` |
| 185 | +2. `python scripts/fetch_updates.py` (Downloads new files) |
| 186 | +3. `python scripts/build_database.py` (Generates DB) |
| 187 | + |
| 188 | +## State Files |
| 189 | + |
| 190 | +- `state/last_update.json`: timestamp of the most recent data/metadata change detected by `scripts/fetch_updates.py`. |
| 191 | +- `state/last_check.json`: timestamp of the most recent update check, even if no changes were found. |
| 192 | +- `state/last_build.json`: input hash + build outputs used by `scripts/build_database.py` to skip rebuilding when the schema/data inputs are unchanged. |
| 193 | + |
| 194 | +## Signing |
| 195 | + |
| 196 | +Signing is done in CI after the database build completes. |
| 197 | + |
| 198 | +What is signed: |
| 199 | +- Canonical message: `openrocket-motordb-v1\n{database_version}\n{sha256_gz}\n` |
| 200 | +- `sha256_gz` is the SHA-256 of `motors.db.gz` (the compressed DB) |
| 201 | + |
| 202 | +What gets added to `metadata.json` by the signing step: |
| 203 | +- `sha256_gz`: SHA-256 of `motors.db.gz` (currently matches `sha256`) |
| 204 | +- `sig`: base64-encoded Ed25519 signature of the canonical message |
| 205 | +- `key_id` (optional): identifier for key rotation |
| 206 | + |
| 207 | +How CI handles it: |
| 208 | +- `.github/workflows/update-motors.yml` installs `cryptography` |
| 209 | +- It runs `python scripts/sign_database.py motors.db.gz metadata.json` |
| 210 | +- The private key is provided via secrets |
| 211 | + |
| 212 | +Set the private key in: |
| 213 | + |
| 214 | +- `MOTOR_DB_PRIVATE_KEY_BASE64` (Ed25519 private key, DER or PEM encoded, then base64) |
| 215 | +- `MOTOR_DB_KEY_ID` (optional, for key rotation) |
| 216 | + |
| 217 | +Manual signing: `python scripts/sign_database.py motors.db.gz metadata.json` |
| 218 | + |
| 219 | +## Unit Tests |
| 220 | + |
| 221 | +1. `pip install -r scripts/requirements.txt` |
| 222 | +2. `pip install pytest cryptography` |
| 223 | +3. `pytest` |
| 224 | + |
| 225 | +## Data Attribution & License |
| 226 | +The motor data in this repository is cached from [ThrustCurve.org](https://www.thrustcurve.org). |
| 227 | + |
| 228 | +* **Source:** ThrustCurve.org (maintained by John Coker). |
| 229 | +* **Copyright:** The data files (`.eng`, `.rse`) retain their original internal copyright headers. |
| 230 | +* **Usage:** This data is intended for use within OpenRocket. If you wish to use this data for other projects, please use the [ThrustCurve.org API](https://www.thrustcurve.org/info/api.html) directly to ensure you are respecting the latest updates and restrictions. |
0 commit comments