Skip to content

Commit e344471

Browse files
committed
Merge branch 'main' into chunked-downloads
2 parents 1caafca + 8322cbe commit e344471

File tree

10 files changed

+121
-93
lines changed

10 files changed

+121
-93
lines changed

.gitignore

+2-1
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

README.md

+17-3
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

+3-4
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-
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/src/config.rs

+30
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

+41-63
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> {

src/wasm/src/lib.rs

+6
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ use sentry::{integrations::anyhow::capture_anyhow, protocol::Context};
77
use sentry_gauge::{wrap_gauge_with_sentry, SentryGauge};
88
use serde::Serialize;
99

10+
/// Developer-configured values for interface
11+
mod config;
12+
/// SQLite mapping implementation
1013
mod database;
14+
/// Interface function definitions
1115
mod funcs;
16+
/// Futures implementations for use in interface functions
1217
mod futures;
18+
/// The sentry wrapper implementation around the MSFS gauge callbacks
1319
mod sentry_gauge;
1420

1521
/// Amount of MS between dispatches of the heartbeat commbus event

src/wasm/src/sentry_gauge.rs

+16-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
fs::{File, OpenOptions},
2+
fs::OpenOptions,
33
sync::{Arc, Mutex},
44
time::{Duration, Instant},
55
};
@@ -15,8 +15,7 @@ use sentry::integrations::anyhow::capture_anyhow;
1515
use serde::{Deserialize, Serialize};
1616
use uuid::Uuid;
1717

18-
/// The path to the manifest.json in the addon folder
19-
const MANIFEST_FILE_PATH: &str = ".\\manifest.json";
18+
use crate::config::Config;
2019

2120
/// The path to the sentry persistent state file
2221
const SENTRY_FILE: &str = "\\work/ng_sentry.json";
@@ -131,6 +130,8 @@ impl SentryPersistentState {
131130
///
132131
/// Note: This MUST be called every frame, otherwise we *will* miss state updates on requests as DataReady is only available for a single frame
133132
pub fn update(&mut self) -> Result<()> {
133+
let initial_reports_size = self.reports.len();
134+
134135
self.reports.retain_mut(|r| {
135136
// Get the request in the report. If one does not exist, create a request
136137
let Some(request) = r.request else {
@@ -142,7 +143,10 @@ impl SentryPersistentState {
142143
request.state() != NetworkRequestState::DataReady
143144
});
144145

145-
self.flush()?;
146+
// Only flush if reports size changed
147+
if self.reports.len() != initial_reports_size {
148+
self.flush()?;
149+
}
146150

147151
Ok(())
148152
}
@@ -242,38 +246,28 @@ where
242246
},
243247
));
244248

245-
// In order to track what addon the reports are coming from, we need to parse the manifest.json file to extract relevant info
246-
let manifest = {
247-
#[derive(Deserialize)]
248-
struct Manifest {
249-
title: String,
250-
creator: String,
251-
package_version: String,
252-
}
253-
let manifest_file = File::open(MANIFEST_FILE_PATH)?;
254-
serde_json::from_reader::<_, Manifest>(manifest_file)?
255-
};
256-
257249
// Get the user ID from persistent state
258250
let user_id = SENTRY_STATE
259251
.try_lock()
260252
.map_err(|_| anyhow!("Unable to lock SENTRY_STATE"))?
261253
.user_id
262254
.to_string();
263255

264-
// Configure the sentry scope to report the user ID and plugin info loaded from manifest.json
256+
// Configure the sentry scope to report the user ID and addon info
265257
sentry::configure_scope(|scope| {
266258
scope.set_user(Some(sentry::User {
267259
id: Some(user_id),
268260
..Default::default()
269261
}));
270262

263+
let config = Config::get_config();
264+
scope.set_tag(
265+
"developer",
266+
config.as_ref().map_or("unknown", |c| &c.addon.developer),
267+
);
271268
scope.set_tag(
272-
"plugin",
273-
format!(
274-
"{}/{}@{}",
275-
manifest.creator, manifest.title, manifest.package_version
276-
),
269+
"product",
270+
config.as_ref().map_or("unknown", |c| &c.addon.product),
277271
);
278272
});
279273

0 commit comments

Comments
 (0)