Skip to content

Commit 9442ef5

Browse files
committed
2 parents 2ba77a4 + 8322cbe commit 9442ef5

File tree

12 files changed

+168
-99
lines changed

12 files changed

+168
-99
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ _PackageInt
44
.rollup.cache
55
tsconfig.tsbuildinfo
66
.vs
7+
examples/aircraft/NavigationDataInterfaceAircraftProject.xml.user
78
examples/aircraft/PackageSources/html_ui/Pages/VCockpit/Instruments/Navigraph/NavigationDataInterfaceSample
89
examples/aircraft/PackageSources/SimObjects/Airplanes/Navigraph_Navigation_Data_Interface_Aircraft/panel/msfs_navigation_data_interface.wasm
910
out
@@ -32,4 +33,4 @@ target/
3233
.DS_Store
3334

3435
test_work/
35-
dist
36+
dist

Cargo.lock

Lines changed: 46 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
The Navigraph Navigation Data Interface enables developers to download and integrate navigation data from Navigraph directly into add-on aircraft in MSFS.
44

5-
65
## Key Features
6+
77
- Navigraph DFD Format: Leverage specialized support for Navigraph's DFD format, based on SQLite, which includes an SQL interface on the commbus for efficient data handling.
88
- Javascript and WASM support: The navdata interface is accessible from both Javascript (Coherent) and WASM, providing flexibility for developers.
99
- Supports updating of custom data formats.
@@ -29,22 +29,36 @@ Here's an overview on the structure of this repository, which is designed to be
2929
1. You'll need to either build the WASM module yourself (not recommended, but documented further down) or download it from [the latest release](https://github.com/Navigraph/msfs-navigation-data-interface/releases) (alternatively you can download it off of a commit by looking at the uploaded artifacts).
3030
2. Add the WASM module into your `panel` folder in `PackageSources`
3131
3. Add the following entry into `panel.cfg` (make sure to replace `NN` with the proper `VCockpit` ID):
32+
3233
```
3334
[VCockpitNN]
3435
size_mm=0,0
3536
pixel_size=0,0
3637
texture=NO_TEXTURE
3738
htmlgauge00=WasmInstrument/WasmInstrument.html?wasm_module=msfs_navigation_data_interface.wasm&wasm_gauge=navigation_data_interface,0,0,1,1
3839
```
40+
3941
- 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
42+
4. **Optional**: Create a `Navigraph/config.json` file to assist with Sentry reports. This info will be reported to us should any error occur in the library. We will use this to directly reach out to you (the developer) for these errors.
43+
44+
- The file must look like
45+
46+
```json
47+
{
48+
"addon": {
49+
"developer": "Navigraph",
50+
"product": "Sample Aircraft"
51+
}
52+
}
53+
```
4054

4155
## Dealing with Bundled Navigation Data
4256

43-
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/)
57+
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 `Navigraph/BundledData` directory in `PackageSources`. You can see an example [here](examples/aircraft/PackageSources/Navigraph/BundledData/)
4458

4559
## Where is the Navigation Data Stored?
4660

47-
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)
61+
The default location for navigation data is `work/NavigationData`.
4862

4963
## Building the Sample Aircraft
5064

examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@
2727
<AssetDir>PackageSources\Data\</AssetDir>
2828
<OutputDir>Data\</OutputDir>
2929
</AssetGroup>
30-
<AssetGroup Name="NavigationData">
30+
<AssetGroup Name="Navigraph">
3131
<Type>Copy</Type>
3232
<Flags>
3333
<FSXCompatibility>false</FSXCompatibility>
3434
</Flags>
35-
<AssetDir>PackageSources\NavigationData\</AssetDir>
36-
<OutputDir>NavigationData\</OutputDir>
35+
<AssetDir>PackageSources\Navigraph\</AssetDir>
36+
<OutputDir>Navigraph\</OutputDir>
3737
</AssetGroup>
3838
<AssetGroup Name="SimObject">
3939
<Type>SimObject</Type>
@@ -53,4 +53,3 @@
5353
</AssetGroup>
5454
</AssetGroups>
5555
</AssetPackage>
56-
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"addon": {
3+
"developer": "Navigraph",
4+
"product": "Sample Aircraft"
5+
}
6+
}

src/wasm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ serde = { version = "1.0.219", features = ["derive"] }
2424
serde_json = "1.0.140"
2525
serde_rusqlite = "0.36.0"
2626
serde_with = "3.12.0"
27-
uuid = { version = "1.16.0", features = ["v4"] }
27+
uuid = { version = "1.16.0", features = ["rng-rand", "v4"] }
2828
zip = { version = "2.5.0", default-features = false, features = ["deflate"] }

src/wasm/src/config.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use std::fs::File;
2+
3+
use serde::Deserialize;
4+
5+
/// The path to an optional addon-specific config file containing data about the addon
6+
const ADDON_CONFIG_FILE: &str = ".\\Navigraph/config.json";
7+
8+
/// Information about the current addon
9+
#[derive(Deserialize)]
10+
pub struct Addon {
11+
pub developer: String,
12+
pub product: String,
13+
}
14+
15+
/// Configuration data provided by the developer
16+
#[derive(Deserialize)]
17+
pub struct Config {
18+
pub addon: Addon,
19+
}
20+
21+
impl Config {
22+
/// Try to get the config
23+
pub fn get_config() -> Option<Self> {
24+
if let Ok(file) = File::open(ADDON_CONFIG_FILE) {
25+
serde_json::from_reader(file).ok()
26+
} else {
27+
None
28+
}
29+
}
30+
}

src/wasm/src/database/mod.rs

Lines changed: 41 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ mod utils;
33

44
use anyhow::{anyhow, Result};
55
use once_cell::sync::Lazy;
6+
use sentry::integrations::anyhow::capture_anyhow;
67
use serde::Deserialize;
78
use std::{
89
cmp::Ordering,
9-
fs::{self, File},
10+
fs::{self, read_dir, File},
1011
path::{Path, PathBuf},
1112
sync::Mutex,
1213
};
@@ -45,79 +46,41 @@ pub const WORK_NAVIGATION_DATA_FOLDER: &str = "\\work/NavigationData";
4546
pub const WORK_CYCLE_JSON_PATH: &str = "\\work/NavigationData/cycle.json";
4647
/// The path to the "master" SQLite DB
4748
pub const WORK_DB_PATH: &str = "\\work/NavigationData/db.s3db";
48-
/// The path to the layout.json in the addon folder
49-
pub const LAYOUT_JSON: &str = ".\\layout.json";
5049
/// The folder name for bundled navigation data
51-
pub const BUNDLED_FOLDER_NAME: &str = "NavigationData";
50+
pub const BUNDLED_FOLDER_NAME: &str = ".\\Navigraph/BundledData";
5251

5352
/// The global exported database state
5453
pub static DATABASE_STATE: Lazy<Mutex<DatabaseState>> =
55-
Lazy::new(|| Mutex::new(DatabaseState::new().unwrap())); // SAFETY: the only way this function can return an error is if layout.json is corrupt (which is impossible since the package wouldn't even mount), or if copying to the work folder is failing (in which case we have more fundamental problems). So overall, unwrapping here is safe
56-
57-
/// An entry in the layout.json file
58-
#[derive(Deserialize)]
59-
struct LayoutEntry {
60-
path: String,
61-
}
62-
63-
/// The representation of the layout.json file
64-
#[derive(Deserialize)]
65-
struct LayoutJson {
66-
content: Vec<LayoutEntry>,
67-
}
54+
Lazy::new(|| Mutex::new(DatabaseState::new()));
6855

6956
/// Find the bundled navigation data distribution
7057
fn get_bundled_db() -> Result<Option<DatabaseDistributionInfo>> {
71-
// Since we don't know the exact filenames of the bundled navigation data,
72-
// we need to find them through the layout.json file. In a perfect world,
73-
// we would just enumerate the bundled directory. However, fd_readdir is unreliable in the sim.
74-
let mut layout = fs::read_to_string(LAYOUT_JSON)?;
75-
let parsed = serde_json::from_str::<LayoutJson>(&mut layout)?;
76-
77-
// Filter out the files in the layout that are not in the bundled folder
78-
let bundled_files = parsed
79-
.content
80-
.iter()
81-
.filter_map(|e| {
82-
let path = Path::new(&e.path);
83-
84-
// Get parent
85-
let (Some(parent), Some(filename)) = (path.parent(), path.file_name()) else {
86-
return None;
87-
};
88-
89-
// Ensure the file is within our known bundled data path
90-
if parent != Path::new(BUNDLED_FOLDER_NAME) {
91-
return None;
92-
};
93-
94-
// Finally, return just the basename
95-
filename.to_str()
96-
})
97-
.collect::<Vec<_>>();
58+
let bundled_entries = match read_dir(BUNDLED_FOLDER_NAME) {
59+
Ok(dir) => dir.filter_map(Result::ok).collect::<Vec<_>>(),
60+
Err(_) => return Ok(None),
61+
};
9862

99-
// Try extracting the cycle info and DB files
100-
let cycle_info = if let Some(file) = bundled_files
63+
// Try finding cycle.json
64+
let Some(cycle_file_name) = bundled_entries
10165
.iter()
102-
.find(|f| f.to_lowercase().ends_with(".json"))
103-
{
104-
file
105-
} else {
66+
.filter_map(|e| e.file_name().to_str().map(|s| s.to_owned()))
67+
.find(|e| *e == String::from("cycle.json"))
68+
else {
10669
return Ok(None);
10770
};
10871

109-
let db_file = if let Some(file) = bundled_files
72+
// Try finding the DB (we don't know the full filename, only extension)
73+
let Some(db_file_name) = bundled_entries
11074
.iter()
111-
.find(|f| f.to_lowercase().ends_with(".s3db"))
112-
{
113-
file
114-
} else {
75+
.filter_map(|e| e.file_name().to_str().map(|s| s.to_owned()))
76+
.find(|e| e.ends_with(".s3db"))
77+
else {
11578
return Ok(None);
11679
};
11780

11881
Ok(Some(DatabaseDistributionInfo::new(
119-
Path::new(&format!(".\\{BUNDLED_FOLDER_NAME}\\{cycle_info}")), // We need to reconstruct the bundled path to include the proper syntax to reference non-work folder files
120-
Path::new(&format!(".\\{BUNDLED_FOLDER_NAME}\\{db_file}")),
82+
Path::new(&format!(".\\{BUNDLED_FOLDER_NAME}\\{cycle_file_name}")), // We need to reconstruct the bundled path to include the proper syntax to reference non-work folder files
83+
Path::new(&format!(".\\{BUNDLED_FOLDER_NAME}\\{db_file_name}")),
12184
)?))
12285
}
12386

@@ -177,11 +140,26 @@ pub struct DatabaseState {
177140

178141
impl DatabaseState {
179142
/// Create a database state, intended to only be instantiated once (held in the DATABASE_STATE static)
180-
///
181-
/// This searches for the best DB to use by comparing the cycle and revision of both the downloaded (in work folder) and bundled navigation data.
182-
fn new() -> Result<Self> {
143+
fn new() -> Self {
183144
// Start out with a fresh instance
184145
let mut instance = Self::default();
146+
147+
// Try to load a DB
148+
match instance.try_load_db() {
149+
Ok(()) => {}
150+
Err(e) => {
151+
capture_anyhow(&e);
152+
println!("[NAVIGRAPH]: Error trying to load DB: {e}");
153+
}
154+
}
155+
156+
instance
157+
}
158+
159+
/// Try to load a database (either bundled or downloaded)
160+
///
161+
/// This searches for the best DB to use by comparing the cycle and revision of both the downloaded (in work folder) and bundled navigation data.
162+
fn try_load_db(&mut self) -> Result<()> {
185163
// Get distribution info of both bundled and downloaded DBs, if they exist
186164
let bundled_distribution = get_bundled_db()?;
187165
let downloaded_distribution =
@@ -221,7 +199,7 @@ impl DatabaseState {
221199

222200
// If we somehow don't have a cycle in bundled or downloaded, return an empty instance
223201
let Some(latest) = latest else {
224-
return Ok(instance);
202+
return Ok(());
225203
};
226204

227205
// Ensure parent folder exists (ignore the result as it will return an error if it already exists)
@@ -236,9 +214,9 @@ impl DatabaseState {
236214
}
237215

238216
// The only way this can fail (since we know now that the path is valid) is if the file is corrupt, in which case we should report to sentry
239-
instance.open_connection()?;
217+
self.open_connection()?;
240218

241-
Ok(instance)
219+
return Ok(());
242220
}
243221

244222
fn get_database(&self) -> Result<&Connection> {

0 commit comments

Comments
 (0)