diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 755d93a3..8510f17e 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -46,7 +46,7 @@ jobs:
run: npm ci
- name: Run linting
- run: npm run lint:js -- -- --max-warnings 0
+ run: npm run lint:js -- --max-warnings 0
build-test:
name: Build & test 🛠️
diff --git a/README.md b/README.md
index 5bff43fa..80824e4e 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,14 @@ Here's an overview on the structure of this repository, which is designed to be
```
- Note that if you already have a `VCockpit` with `NO_TEXTURE` you can just add another `htmlgauge` to it, while making sure to increase the index
+## Dealing with Bundled Navigation Data
+
+If you bundle outdated navigation data in your aircraft and you want this module to handle updating it for users with subscriptions, place the navigation data into the `NavigationData` directory in `PackageSources`. You can see an example [here](examples/aircraft/PackageSources/NavigationData/)
+
+## Where is the Navigation Data Stored?
+
+The default location for navigation data is `work/NavigationData`. If you have bundled navigation data, its located in the `NavigationData` folder in the root of your project. (although it gets copied into the `work` directory at runtime)
+
## Building the Sample Aircraft
Before building, make sure you have properly created and set an `.env` file in `examples/gauge`! An example can be found in the `.env.example` file in that directory. Replace with your credentials
diff --git a/examples/aircraft/NavigationDataInterfaceAircraftProject.xml b/examples/aircraft/NavigationDataInterfaceAircraftProject.xml
index 9e2771a7..d80ada3f 100644
--- a/examples/aircraft/NavigationDataInterfaceAircraftProject.xml
+++ b/examples/aircraft/NavigationDataInterfaceAircraftProject.xml
@@ -1,11 +1,11 @@
-
-
- .
- _PackageInt
- _PublishingGroupInt
-
- PackageDefinitions\navigraph-aircraft-navigation-data-interface-sample.xml
-
-
-
-
+
+
+ .
+ _PackageInt
+ _PublishingGroupInt
+
+ PackageDefinitions\navigraph-aircraft-navigation-data-interface-sample.xml
+
+
+
+
diff --git a/examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml b/examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml
index 3277cd84..df4e18f5 100644
--- a/examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml
+++ b/examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml
@@ -1,48 +1,56 @@
-
-
-
- AIRCRAFT
- Navigraph Navigation Data Interface Sample Aircraft
- My Manufacturer
- Navigraph
-
-
- true
- true
-
-
-
- Copy
-
- false
-
- PackageDefinitions\navigraph-aircraft-navigation-data-interface-sample\ContentInfo\
- ContentInfo\navigraph-aircraft-navigation-data-interface-sample\
-
-
- Copy
-
- false
-
- PackageSources\Data\
- Data\
-
-
- SimObject
-
- false
-
- PackageSources\SimObjects\Airplanes\Navigraph_Navigation_Data_Interface_Aircraft\
- SimObjects\Airplanes\Navigraph_Navigation_Data_Interface_Aircraft\
-
-
- Copy
-
- false
-
- PackageSources\html_ui\
- html_ui\
-
-
-
-
+
+
+
+ AIRCRAFT
+ Navigraph Navigation Data Interface Sample Aircraft
+ My Manufacturer
+ Navigraph
+
+
+ true
+ true
+
+
+
+ Copy
+
+ false
+
+ PackageDefinitions\navigraph-aircraft-navigation-data-interface-sample\ContentInfo\
+ ContentInfo\navigraph-aircraft-navigation-data-interface-sample\
+
+
+ Copy
+
+ false
+
+ PackageSources\Data\
+ Data\
+
+
+ Copy
+
+ false
+
+ PackageSources\NavigationData\
+ NavigationData\
+
+
+ SimObject
+
+ false
+
+ PackageSources\SimObjects\Airplanes\Navigraph_Navigation_Data_Interface_Aircraft\
+ SimObjects\Airplanes\Navigraph_Navigation_Data_Interface_Aircraft\
+
+
+ Copy
+
+ false
+
+ PackageSources\html_ui\
+ html_ui\
+
+
+
+
diff --git a/examples/aircraft/PackageSources/NavigationData/cycle.json b/examples/aircraft/PackageSources/NavigationData/cycle.json
new file mode 100644
index 00000000..55cd1062
--- /dev/null
+++ b/examples/aircraft/PackageSources/NavigationData/cycle.json
@@ -0,0 +1 @@
+{"cycle":"2101","revision":"1","name":"Navigraph Avionics", "format": "dfd", "validityPeriod": "2021-01-25/2021-02-20"}
\ No newline at end of file
diff --git a/examples/aircraft/PackageSources/NavigationData/e_dfd_2101.s3db b/examples/aircraft/PackageSources/NavigationData/e_dfd_2101.s3db
new file mode 100644
index 00000000..d06d0826
Binary files /dev/null and b/examples/aircraft/PackageSources/NavigationData/e_dfd_2101.s3db differ
diff --git a/examples/gauge/Components/InterfaceSample.tsx b/examples/gauge/Components/InterfaceSample.tsx
index 5cfa6e84..67de4a83 100644
--- a/examples/gauge/Components/InterfaceSample.tsx
+++ b/examples/gauge/Components/InterfaceSample.tsx
@@ -22,7 +22,6 @@ export class InterfaceSample extends DisplayComponent {
private readonly dropdownRef = FSComponent.createRef()
private readonly downloadButtonRef = FSComponent.createRef()
private readonly executeButtonRef = FSComponent.createRef()
- private readonly setActiveButtonRef = FSComponent.createRef()
private readonly inputRef = FSComponent.createRef()
private cancelSource = CancelToken.source()
@@ -69,9 +68,6 @@ export class InterfaceSample extends DisplayComponent {
Download
-
- Set as Active
-
Execute SQL
@@ -103,16 +99,6 @@ export class InterfaceSample extends DisplayComponent {
.catch(e => console.error(e))
})
- this.setActiveButtonRef.instance.addEventListener("click", () => {
- const format = this.dropdownRef.instance.getNavigationDataFormat()
- if (!format) return
- // This will only work if the database specified is a SQLite database
- this.navigationDataInterface
- .set_active_database(format)
- .then(() => console.info("WASM set active database"))
- .catch(err => this.displayError(String(err)))
- })
-
AuthService.user.sub(user => {
if (user) {
this.qrCodeRef.instance.src = ""
@@ -173,7 +159,7 @@ export class InterfaceSample extends DisplayComponent {
const pkg = await packages.getPackage(format)
// Download navigation data to work dir
- await this.navigationDataInterface.download_navigation_data(pkg.file.url, pkg.format)
+ await this.navigationDataInterface.download_navigation_data(pkg.file.url)
this.displayMessage("Navigation data downloaded")
} catch (err) {
if (err instanceof Error) this.displayError(err.message)
diff --git a/package-lock.json b/package-lock.json
index 52c05fa1..fc1a28eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,13 +16,13 @@
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
+ "bigint-buffer": "^1.1.5",
"dotenv": "^16.3.1",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.7.0",
"prettier": "^3.0.3",
- "random-bigint": "^0.0.1",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"tsup": "^8.0.1",
@@ -3094,6 +3094,19 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/bigint-buffer": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
+ "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "bindings": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -3103,6 +3116,15 @@
"node": ">=8"
}
},
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dev": true,
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -4215,6 +4237,12 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "dev": true
+ },
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -7165,15 +7193,6 @@
}
]
},
- "node_modules/random-bigint": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/random-bigint/-/random-bigint-0.0.1.tgz",
- "integrity": "sha512-X+NTsf5Hzl/tRNLiNTD3N1LRU0eKdIE0+plNlV1CmXLTlnAxj6HipcTnOhWvFRoSytCz6l1f4KYFf/iH8NNSLw==",
- "dev": true,
- "engines": {
- "node": ">=10.0.0"
- }
- },
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
diff --git a/package.json b/package.json
index a203bb0e..166002c2 100644
--- a/package.json
+++ b/package.json
@@ -26,11 +26,10 @@
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.7.0",
"prettier": "^3.0.3",
- "random-bigint": "^0.0.1",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"tsup": "^8.0.1",
"typescript": "^5.2.2",
"uuid": "^9.0.1"
}
-}
+}
\ No newline at end of file
diff --git a/src/database/src/database.rs b/src/database/src/database.rs
index cb216115..fd29a511 100644
--- a/src/database/src/database.rs
+++ b/src/database/src/database.rs
@@ -27,35 +27,53 @@ use crate::{
vhf_navaid::VhfNavaid,
waypoint::Waypoint,
},
- sql_structs::{self},
- util,
+ sql_structs, util,
};
pub struct Database {
database: Option,
+ pub path: Option,
}
#[derive(Debug)]
struct NoDatabaseOpen;
impl Display for NoDatabaseOpen {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "No database open") }
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "No database open")
+ }
}
impl Error for NoDatabaseOpen {}
impl Database {
- pub fn new() -> Self { Database { database: None } }
+ pub fn new() -> Self {
+ Database {
+ database: None,
+ path: None,
+ }
+ }
- fn get_database(&self) -> Result<&Connection, NoDatabaseOpen> { self.database.as_ref().ok_or(NoDatabaseOpen) }
+ fn get_database(&self) -> Result<&Connection, NoDatabaseOpen> {
+ self.database.as_ref().ok_or(NoDatabaseOpen)
+ }
- pub fn set_active_database(&mut self, mut path: String) -> Result<(), Box> {
- // Check if the path is a directory and if it is, search for a sqlite file
- let formatted_path = format!("\\work/{}", path);
- if util::get_path_type(std::path::Path::new(&formatted_path)) == util::PathType::Directory {
- path = util::find_sqlite_file(&formatted_path)?;
+ pub fn set_active_database(&mut self, path: String) -> Result<(), Box> {
+ let path = match util::find_sqlite_file(&path) {
+ Ok(new_path) => new_path,
+ Err(_) => path,
+ };
+ println!("[NAVIGRAPH] Setting active database to {}", path);
+ self.close_connection();
+ if util::is_sqlite_file(&path)? {
+ self.open_connection(path.clone())?;
}
+ self.path = Some(path);
+ Ok(())
+ }
+
+ pub fn open_connection(&mut self, path: String) -> Result<(), Box> {
// We have to open with flags because the SQLITE_OPEN_CREATE flag with the default open causes the file to
// be overwritten
let flags = OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX;
@@ -541,5 +559,7 @@ impl Database {
Ok(data)
}
- pub fn close_connection(&mut self) { self.database = None; }
+ pub fn close_connection(&mut self) {
+ self.database = None;
+ }
}
diff --git a/src/database/src/math.rs b/src/database/src/math.rs
index ff1ce77a..24a98847 100644
--- a/src/database/src/math.rs
+++ b/src/database/src/math.rs
@@ -10,7 +10,9 @@ pub type Minutes = f64;
pub type KiloHertz = f64;
pub type MegaHertz = f64;
-pub(crate) fn feet_to_meters(metres: Meters) -> Feet { metres / 3.28084 }
+pub(crate) fn feet_to_meters(metres: Meters) -> Feet {
+ metres / 3.28084
+}
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
pub struct Coordinates {
diff --git a/src/database/src/sql_structs.rs b/src/database/src/sql_structs.rs
index 67c3a285..a66fbf71 100644
--- a/src/database/src/sql_structs.rs
+++ b/src/database/src/sql_structs.rs
@@ -1,22 +1,11 @@
use serde::Deserialize;
use super::enums::{
- AirwayDirection,
- AirwayLevel,
- AirwayRouteType,
- AltitudeDescriptor,
- LegType,
- SpeedDescriptor,
- TurnDirection,
+ AirwayDirection, AirwayLevel, AirwayRouteType, AltitudeDescriptor, LegType, SpeedDescriptor, TurnDirection,
};
use crate::enums::{
- ApproachTypeIdentifier,
- CommunicationType,
- ControlledAirspaceType,
- FrequencyUnits,
- IfrCapability,
- RestrictiveAirspaceType,
- RunwaySurfaceCode,
+ ApproachTypeIdentifier, CommunicationType, ControlledAirspaceType, FrequencyUnits, IfrCapability,
+ RestrictiveAirspaceType, RunwaySurfaceCode,
};
#[derive(Deserialize, Debug)]
diff --git a/src/database/src/util.rs b/src/database/src/util.rs
index ac9c06c5..7f387eb7 100644
--- a/src/database/src/util.rs
+++ b/src/database/src/util.rs
@@ -1,4 +1,9 @@
-use std::{fs, io::Read, path::Path};
+use std::{error::Error, fs, io::Read, path::Path};
+
+// From 1.3.1 of https://www.sqlite.org/fileformat.html
+const SQLITE_HEADER: [u8; 16] = [
+ 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00,
+];
#[derive(PartialEq, Eq)]
pub enum PathType {
@@ -31,27 +36,33 @@ pub fn get_path_type(path: &Path) -> PathType {
PathType::DoesNotExist
}
-pub fn find_sqlite_file(path: &str) -> Result> {
- // From 1.3.1 of https://www.sqlite.org/fileformat.html
- let sqlite_header = [
- 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00,
- ];
+pub fn find_sqlite_file(path: &str) -> Result> {
+ if get_path_type(&Path::new(path)) != PathType::Directory {
+ return Err("Path is not a directory".into());
+ }
+
// We are going to search this directory for a database
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if get_path_type(&path) == PathType::File {
let path = path.to_str().ok_or("Invalid path")?;
- // Get first 16 bytes of file
- let mut file = std::fs::File::open(path)?;
- let mut buf = [0; 16];
- file.read_exact(buf.as_mut())?;
- // Compare bytes to sqlite header
- if buf == sqlite_header {
- // We found a database
+
+ if is_sqlite_file(path)? {
return Ok(path.to_string());
}
}
}
Err("No SQL database found. Make sure the database specified is a SQL database".into())
}
+
+pub fn is_sqlite_file(path: &str) -> Result> {
+ if get_path_type(&Path::new(path)) != PathType::File {
+ return Ok(false);
+ }
+
+ let mut file = fs::File::open(path)?;
+ let mut buf = [0; 16];
+ file.read_exact(&mut buf)?;
+ Ok(buf == SQLITE_HEADER)
+}
diff --git a/src/js/interface/NavigationDataInterfaceTypes.ts b/src/js/interface/NavigationDataInterfaceTypes.ts
index df9b1bf1..5945cb61 100644
--- a/src/js/interface/NavigationDataInterfaceTypes.ts
+++ b/src/js/interface/NavigationDataInterfaceTypes.ts
@@ -25,7 +25,7 @@ export interface DownloadProgressData {
export enum NavigraphFunction {
DownloadNavigationData = "DownloadNavigationData",
SetDownloadOptions = "SetDownloadOptions",
- SetActiveDatabase = "SetActiveDatabase",
+ GetNavigationDataInstallStatus = "GetNavigationDataInstallStatus",
ExecuteSQLQuery = "ExecuteSQLQuery",
GetDatabaseInfo = "GetDatabaseInfo",
GetAirport = "GetAirport",
diff --git a/src/js/interface/NavigraphNavigationDataInterface.ts b/src/js/interface/NavigraphNavigationDataInterface.ts
index e7027a01..588852bc 100644
--- a/src/js/interface/NavigraphNavigationDataInterface.ts
+++ b/src/js/interface/NavigraphNavigationDataInterface.ts
@@ -18,6 +18,7 @@ import {
VhfNavaid,
Waypoint,
} from "../types"
+import { NavigationDataStatus } from "../types/meta"
import {
Callback,
CommBusMessage,
@@ -69,11 +70,10 @@ export class NavigraphNavigationDataInterface {
* Downloads the navigation data from the given URL to the given path
*
* @param url - A valid signed URL to download the navigation data from
- * @param path - The path to download the navigation data to
* @returns A promise that resolves when the download is complete
*/
- public async download_navigation_data(url: string, path: string): Promise {
- return await this.callWasmFunction("DownloadNavigationData", { url, path })
+ public async download_navigation_data(url: string): Promise {
+ return await this.callWasmFunction("DownloadNavigationData", { url })
}
/**
@@ -87,16 +87,12 @@ export class NavigraphNavigationDataInterface {
}
/**
- * Sets the active DFD database to the one at the given path
+ * Gets the installation status of the navigation data
*
- * @remarks
- * The path must be a valid path to a folder that contains a DFD file.
- *
- * @param path - The path to the folder that contains the DFD file
- * @returns A promise that resolves when the function is complete
+ * @returns A promise that resolves with the installation status
*/
- public async set_active_database(path: string): Promise {
- return await this.callWasmFunction("SetActiveDatabase", { path })
+ public async get_navigation_data_install_status(): Promise {
+ return await this.callWasmFunction("GetNavigationDataInstallStatus", {})
}
/**
diff --git a/src/js/types/meta.ts b/src/js/types/meta.ts
new file mode 100644
index 00000000..86fc0c28
--- /dev/null
+++ b/src/js/types/meta.ts
@@ -0,0 +1,15 @@
+export enum InstallStatus {
+ Bundled = "Bundled",
+ Manual = "Manual",
+ None = "None",
+}
+
+export interface NavigationDataStatus {
+ status: InstallStatus
+ installedFormat: string | null
+ installedRegion: string | null
+ installedCycle: string | null
+ installedPath: string | null
+ validityPeriod: string | null
+ lastestCycle: string | null
+}
diff --git a/src/test/RandomBigint.d.ts b/src/test/RandomBigint.d.ts
deleted file mode 100644
index 8ea90983..00000000
--- a/src/test/RandomBigint.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-declare module "random-bigint" {
- export default function random(size: number): bigint
-}
diff --git a/src/test/randomBigint.ts b/src/test/randomBigint.ts
new file mode 100644
index 00000000..460bdddc
--- /dev/null
+++ b/src/test/randomBigint.ts
@@ -0,0 +1,51 @@
+// https://github.com/bnoordhuis/random-bigint/blob/master/index.js
+
+import { randomBytes } from "crypto"
+
+export function random(bits: number) {
+ if (bits < 0)
+ throw new RangeError('bits < 0')
+
+ // @ts-ignore
+ const n = (bits >>> 3) + !!(bits & 7) // Round up to next byte.
+ const r = 8*n - bits
+ const s = 8 - r
+ const m = (1 << s) - 1 // Bits to mask off from MSB.
+
+ const bytes = randomBytes(n)
+
+ maskbits(m, bytes)
+
+ return bytes2bigint(bytes)
+}
+
+function maskbits(m: number, bytes: Buffer) {
+ // Mask off bits from the MSB that are > log2(bits).
+ // |bytes| is treated as a big-endian bigint so byte 0 is the MSB.
+ if (bytes.length > 0)
+ bytes[0] &= m
+}
+
+function bytes2bigint(bytes: Buffer) {
+ let result = BigInt(0)
+
+ const n = bytes.length
+
+ // Read input in 8 byte slices. This is, on average and at the time
+ // of writing, about 35x faster for large inputs than processing them
+ // one byte at a time.
+ if (n >= 8) {
+ const view = new DataView(bytes.buffer, bytes.byteOffset)
+
+ for (let i = 0, k = n & ~7; i < k; i += 8) {
+ const x = view.getBigUint64(i, false)
+ result = (result << BigInt(64)) + x
+ }
+ }
+
+ // Now mop up any remaining bytes.
+ for (let i = n & ~7; i < n; i++)
+ result = result * BigInt(256) + BigInt(bytes[i])
+
+ return result
+}
\ No newline at end of file
diff --git a/src/test/setup.ts b/src/test/setup.ts
index 06eace43..5e71977d 100644
--- a/src/test/setup.ts
+++ b/src/test/setup.ts
@@ -1,11 +1,11 @@
import { readFileSync } from "node:fs"
import { argv, env } from "node:process"
-import random from "random-bigint"
-import { v4 } from "uuid"
import { WASI } from "wasi"
+import { v4 } from "uuid"
import { NavigraphNavigationDataInterface } from "../js"
import { WEBASSEMBLY_PATH, WORK_FOLDER_PATH } from "./constants"
import "dotenv/config"
+import { random } from "./randomBigint"
enum PanelService {
POST_QUERY = 1,
@@ -157,6 +157,7 @@ let wasmFunctionTable: WebAssembly.Table // The table of callback functions in t
* Maps request ids to a tuple of the returned data's pointer, and the data's size
*/
const promiseResults = new Map()
+const failedRequests: bigint[] = []
wasmInstance = new WebAssembly.Instance(wasmModule, {
wasi_snapshot_preview1: wasiSystem.wasiImport,
@@ -205,7 +206,7 @@ wasmInstance = new WebAssembly.Instance(wasmModule, {
fsNetworkHttpRequestGet: (urlPointer: number, paramPointer: number, callback: number, ctx: number) => {
const url = readString(urlPointer)
- const requestId: bigint = random(32) // Setting it to 64 does... strange things
+ const requestId = random(16) // Extra bits get lopped off by WASM, this number works
// Currently the only network request is for the navigation data zip which is downloaded as a blob
fetch(url)
@@ -222,11 +223,20 @@ wasmInstance = new WebAssembly.Instance(wasmModule, {
func(requestId, 200, ctx)
})
.catch(err => {
- console.error(err)
+ failedRequests.push(requestId)
})
return requestId
},
+ fsNetworkHttpRequestGetState: (requestId: bigint) => {
+ if (failedRequests.includes(requestId)) {
+ return 4 // FS_NETWORK_HTTP_REQUEST_STATE_FAILED
+ }
+ if (promiseResults.has(requestId)) {
+ return 3 // FS_NETWORK_HTTP_REQUEST_STATE_DATA_READY
+ }
+ return 2 // FS_NETWORK_HTTP_REQUEST_STATE_WAITING_FOR_DATA
+ },
},
}) as WasmInstance
@@ -280,11 +290,16 @@ beforeAll(async () => {
throw new Error("Please specify the env var `NAVIGATION_DATA_SIGNED_URL`")
}
- // Download navigation data to a unique folder to prevent clashes
- const path = v4()
+ // Utility function to convert onReady to a promise
+ const waitForReady = (navDataInterface: NavigraphNavigationDataInterface): Promise => {
+ return new Promise((resolve, _reject) => {
+ navDataInterface.onReady(() => resolve())
+ })
+ }
+
+ await waitForReady(navigationDataInterface)
- await navigationDataInterface.download_navigation_data(downloadUrl, path)
- await navigationDataInterface.set_active_database(path)
+ await navigationDataInterface.download_navigation_data(downloadUrl)
}, 30000)
void lifeCycle()
diff --git a/src/wasm/src/consts.rs b/src/wasm/src/consts.rs
new file mode 100644
index 00000000..d43536a2
--- /dev/null
+++ b/src/wasm/src/consts.rs
@@ -0,0 +1,3 @@
+pub const NAVIGATION_DATA_DEFAULT_LOCATION: &str = ".\\NavigationData";
+pub const NAVIGATION_DATA_WORK_LOCATION: &str = "\\work/NavigationData";
+pub const NAVIGATION_DATA_INTERNAL_CONFIG_LOCATION: &str = "\\work/navigraph_config.json";
diff --git a/src/wasm/src/dispatcher.rs b/src/wasm/src/dispatcher.rs
index a1c64ad3..88edd976 100644
--- a/src/wasm/src/dispatcher.rs
+++ b/src/wasm/src/dispatcher.rs
@@ -1,16 +1,19 @@
-use std::{cell::RefCell, rc::Rc};
+use std::{cell::RefCell, path::Path, rc::Rc};
-use msfs::{commbus::*, sys::sGaugeDrawData, MSFSEvent};
+use msfs::{commbus::*, network::NetworkRequestState, sys::sGaugeDrawData, MSFSEvent};
use navigation_database::database::Database;
use crate::{
- download::downloader::NavigationDataDownloader,
+ consts,
+ download::downloader::{DownloadStatus, NavigationDataDownloader},
json_structs::{
events,
functions::{CallFunction, FunctionResult, FunctionStatus, FunctionType},
params,
},
- util,
+ meta::{self, InternalState},
+ network_helper::NetworkHelper,
+ util::{self, path_exists},
};
#[derive(PartialEq, Eq)]
@@ -26,6 +29,7 @@ pub struct Task {
pub id: String,
pub data: Option,
pub status: TaskStatus,
+ pub associated_network_request: Option,
}
impl Task {
@@ -76,20 +80,19 @@ impl<'a> Dispatcher<'a> {
}
fn handle_initialized(&mut self) {
- {
- // We need to clone twice because we need to move the queue into the closure and then clone it again
- // whenever it gets called
- let captured_queue = Rc::clone(&self.queue);
- self.commbus
- .register("NAVIGRAPH_CallFunction", move |args| {
- // TODO: maybe send error back to sim?
- match Dispatcher::add_to_queue(Rc::clone(&captured_queue), args) {
- Ok(_) => (),
- Err(e) => println!("[NAVIGRAPH] Failed to add to queue: {}", e),
- }
- })
- .expect("Failed to register NAVIGRAPH_CallFunction");
- }
+ self.load_database();
+ // We need to clone twice because we need to move the queue into the closure and then clone it again
+ // whenever it gets called
+ let captured_queue = Rc::clone(&self.queue);
+ self.commbus
+ .register("NAVIGRAPH_CallFunction", move |args| {
+ // TODO: maybe send error back to sim?
+ match Dispatcher::add_to_queue(Rc::clone(&captured_queue), args) {
+ Ok(_) => (),
+ Err(e) => println!("[NAVIGRAPH] Failed to add to queue: {}", e),
+ }
+ })
+ .expect("Failed to register NAVIGRAPH_CallFunction");
}
fn handle_update(&mut self, data: &sGaugeDrawData) {
@@ -104,6 +107,103 @@ impl<'a> Dispatcher<'a> {
self.process_queue();
self.downloader.on_update();
+
+ // Because the download process doesn't finish in the function call, we need to check if the download is finished to call the on_download_finish function
+ if *self.downloader.download_status.borrow() == DownloadStatus::Downloaded {
+ self.on_download_finish();
+ self.downloader.acknowledge_download();
+ }
+ }
+ fn load_database(&mut self) {
+ println!("[NAVIGRAPH] Loading database");
+
+ // Go through logic to determine which database to load
+
+ // Are we bundled? None means we haven't installed anything yet
+ let is_bundled = meta::get_internal_state()
+ .map(|internal_state| Some(internal_state.is_bundled))
+ .unwrap_or(None);
+
+ // Get the installed cycle (if it exists)
+ let installed_cycle = match meta::get_installed_cycle_from_json(
+ &Path::new(consts::NAVIGATION_DATA_WORK_LOCATION).join("cycle.json"),
+ ) {
+ Ok(cycle) => Some(cycle.cycle),
+ Err(_) => None,
+ };
+
+ // Get the bundled cycle (if it exists)
+ let bundled_cycle = match meta::get_installed_cycle_from_json(
+ &Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION).join("cycle.json"),
+ ) {
+ Ok(cycle) => Some(cycle.cycle),
+ Err(_) => None,
+ };
+
+ // Determine if we are bundled ONLY and the bundled cycle is newer than the installed (old bundled) cycle
+ let bundled_updated = if is_bundled.is_some() && is_bundled.unwrap() {
+ if installed_cycle.is_some() && bundled_cycle.is_some() {
+ bundled_cycle.unwrap() > installed_cycle.unwrap()
+ } else {
+ false
+ }
+ } else {
+ false
+ };
+
+ // If there is no addon config, we can assume that we need to copy the bundled database to the work location
+ let need_to_copy = is_bundled.is_none();
+
+ // If we are bundled and the installed cycle is older than the bundled cycle, we need to copy the bundled database to the work location. Or if we haven't installed anything yet, we need to copy the bundled database to the work location
+ if bundled_updated || need_to_copy {
+ match util::copy_files_to_folder(
+ &Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION),
+ &Path::new(consts::NAVIGATION_DATA_WORK_LOCATION),
+ ) {
+ Ok(_) => {
+ // Set the internal state to bundled
+ let res = meta::set_internal_state(InternalState { is_bundled: true });
+ if let Err(e) = res {
+ println!("[NAVIGRAPH] Failed to set internal state: {}", e);
+ }
+ },
+ Err(e) => {
+ println!(
+ "[NAVIGRAPH] Failed to copy database from default location to work location: {}",
+ e
+ );
+ return;
+ },
+ }
+ }
+
+ // Finally, set the active database
+ if path_exists(&Path::new(consts::NAVIGATION_DATA_WORK_LOCATION)) {
+ match self.database.set_active_database(consts::NAVIGATION_DATA_WORK_LOCATION.to_owned()) {
+ Ok(_) => {
+ println!("[NAVIGRAPH] Loaded database");
+ },
+ Err(e) => {
+ println!("[NAVIGRAPH] Failed to load database: {}", e);
+ },
+ }
+ } else {
+ println!("[NAVIGRAPH] Failed to load database: there is no installed database");
+ }
+ }
+
+ fn on_download_finish(&mut self) {
+ match navigation_database::util::find_sqlite_file(consts::NAVIGATION_DATA_WORK_LOCATION) {
+ Ok(path) => {
+ match self.database.set_active_database(path) {
+ Ok(_) => {},
+ Err(e) => {
+ println!("[NAVIGRAPH] Failed to set active database: {}", e);
+ },
+ };
+ },
+ Err(_) => {},
+ }
}
fn process_queue(&mut self) {
@@ -128,19 +228,18 @@ impl<'a> Dispatcher<'a> {
self.database.close_connection();
// Now we can download the navigation data
- self.downloader.download(Rc::clone(task))
+ self.downloader.download(Rc::clone(task));
},
FunctionType::SetDownloadOptions => {
Dispatcher::execute_task(task.clone(), |t| self.downloader.set_download_options(t))
},
- FunctionType::SetActiveDatabase => Dispatcher::execute_task(task.clone(), |t| {
- let params = t.borrow().parse_data_as::()?;
- self.database.set_active_database(params.path)?;
-
- t.borrow_mut().status = TaskStatus::Success(None);
+ FunctionType::GetNavigationDataInstallStatus => {
+ // We can't use the execute_task function here because the download process doesn't finish in the
+ // function call, which results in slightly "messier" code
- Ok(())
- }),
+ // We first need to initialize the network request and then wait for the response
+ meta::start_network_request(Rc::clone(task))
+ },
FunctionType::ExecuteSQLQuery => Dispatcher::execute_task(task.clone(), |t| {
let params = t.borrow().parse_data_as::()?;
let data = self.database.execute_sql_query(params.sql, params.params)?;
@@ -357,6 +456,33 @@ impl<'a> Dispatcher<'a> {
}
}
+ // Network request tasks
+ for task in queue
+ .iter()
+ .filter(|task| task.borrow().status == TaskStatus::InProgress)
+ {
+ let response_state = match task.borrow().associated_network_request {
+ Some(ref request) => request.response_state(),
+ None => continue,
+ };
+ let function_type = task.borrow().function_type;
+ if response_state == NetworkRequestState::DataReady {
+ match function_type {
+ FunctionType::GetNavigationDataInstallStatus => {
+ println!("[NAVIGRAPH] Network request completed, getting install status");
+ meta::get_navigation_data_install_status(Rc::clone(task));
+ println!("[NAVIGRAPH] Install status task completed");
+ },
+ _ => {
+ // Should not happen for now
+ println!("[NAVIGRAPH] Network request completed but no handler for this type of request");
+ },
+ }
+ } else if response_state == NetworkRequestState::Failed {
+ task.borrow_mut().status = TaskStatus::Failure("Network request failed".to_owned());
+ }
+ }
+
// Process completed tasks (everything should at least be in progress at this point)
queue.retain(|task| {
if let TaskStatus::InProgress = task.borrow().status {
@@ -416,6 +542,7 @@ impl<'a> Dispatcher<'a> {
id: json_result.id,
data: json_result.data,
status: TaskStatus::NotStarted,
+ associated_network_request: None,
})));
Ok(())
diff --git a/src/wasm/src/download/downloader.rs b/src/wasm/src/download/downloader.rs
index b253fa5c..2722cd54 100644
--- a/src/wasm/src/download/downloader.rs
+++ b/src/wasm/src/download/downloader.rs
@@ -3,9 +3,11 @@ use std::{cell::RefCell, io::Cursor, path::PathBuf, rc::Rc};
use msfs::network::*;
use crate::{
+ consts,
dispatcher::{Dispatcher, Task, TaskStatus},
download::zip_handler::{BatchReturn, ZipFileHandler},
json_structs::{events, params},
+ meta::{self, InternalState},
};
pub struct DownloadOptions {
@@ -18,12 +20,13 @@ pub enum DownloadStatus {
Downloading,
CleaningDestination,
Extracting,
+ Downloaded,
Failed(String),
}
pub struct NavigationDataDownloader {
zip_handler: RefCell