Using the Event Bus
What is the Event Bus?
Subscribable values are an excellent way to get data from parent components down to child components, but what about data that may be more instrument-wide or cut across components in your instrument? For that, there's EventBus
.
EventBus
allows you to define typesafe publishers and consumers that broadcast events with corresponding data on given topics. Each topic is a string key that consumers can subscribe to, and will have their subscribed callbacks called when a publisher publishes data on the topic key. Simply define an interface, and the fields of that interface become the keys, and the value types the data types.
interface HelloWorldEvents {
new_text: string;
}
const bus = new EventBus();
const publisher = bus.getPublisher<HelloWorldEvents>();
const subscriber = bus.getSubscriber<HelloWorldEvents>();
subscriber.on('new_text').handle(text => console.log(text));
publisher.pub('new_text', 'Hello, EventBus!'); //Logs 'Hello, EventBus!'
EventBus Subscriber Filters
The avionics framework helpfully provides a number of built-in filtering options to help reduce the frequency of subscriptions being called on topic data updates.
whenChanged()
//Only call the consumer when the value is different than prior, not on every topic publish
subscriber.on('distance_to_go')
.whenChanged()
.handle(distance => console.log(`Distance to go: ${distance} NM`));
whenChangedBy()
//Only call the consumer when the value is different than prior by at least the specified amount
subscriber.on('distance_to_go')
.whenChangedBy(0.1)
.handle(distance => console.log(`Distance to go: ${distance} NM`));
onlyAfter()
//Supress consuming events until a minimum time in milliseconds has elapsed since the previous event
subscriber.on('distance_to_go')
.onlyAfter(1000)
.handle(distance => console.log(`Distance to go: ${distance} NM`));
atFrequency()
//Only consume events at a specified frequency in Hz
subscriber.on('distance_to_go')
.atFrequency(4)
.handle(distance => console.log(`Distance to go: ${distance} NM`));
withPrecision()
//Only consume events when the numeric value rounded to the number of provided decimal places
//has changed since the previous rounded value. Accepts negative values for 10, 100s, etc.
subscriber.on('distance_to_go')
.withPrecision(1)
.handle(distance => console.log(`Distance to go: ${distance} NM`));
Publishing SimVar Data Via the Event Bus
Publishing SimVar data across EventBus
is an ideal application, as getting SimVar data requires a call into the Coherent GT framework and a serialization round trip from the sim, which is a (relatively) slow operation. Instead of peppering code with a number of local SimVar calls, we can use EventBus
to get a SimVar just once and then push that data to all consumers who are subscribed, keeping performance at a maximum.
Wiring the EventBus Consumer to the Component
Let's make a few modifications to our MyComponent
component. First, let's change the props interface and also add an interface for our events:
interface MyComponentProps extends ComponentProps {
bus: EventBus;
}
export interface SpeedEvents {
indicated_airspeed: number;
}
Then, we can use some tools at our disposal to subscribe to the bus and create a subscribable value from the consumer. First, add a private field to the class:
private readonly indicatedAirspeed: Subscribable<number>;
Then, we can create a constructor to subscribe to the bus and pipe to a subscribable value:
constructor(props: MyComponentProps) {
super(props);
const subscriber = props.bus.getSubscriber<SpeedEvents>();
const consumer = subscriber.on('indicated_airspeed');
this.indicatedAirspeed = ConsumerSubject.create(consumer, 0);
}
Just as in React, constructors must call super(props)
, and should do so as the first line in the constructor.
Finally, we can reference our new subscribable value in our render method:
public render(): VNode {
return (
<div class='my-component'>{this.indicatedAirspeed} IAS</div>
);
}
Setting Up the Event Bus in the Instrument
In MyInstrument
, we now need to create an instance of EventBus
so we can pass it as a prop to MyComponent
. Create the following field in the class:
private readonly eventBus = new EventBus();
Then we can pass it as a prop to our component:
public connectedCallback(): void {
super.connectedCallback();
FSComponent.render(<MyComponent bus={this.eventBus} />, document.getElementById('InstrumentContent'));
}
You should generally only instantiate a single instance of EventBus
within your instrument, and pass it to components as necessary. Different instances of the event bus will not automatically share topic publications, and any topics published to on one bus will not be published to on another.
In order to publish this SimVar data to the bus, we are going to want to get that data each simulation frame. Thankfully, the VCockpit system gives us a hook to do just that. Simply make a method on your instrument called Update()
, and give it the following code to read our indicated airspeed and publish it on the bus:
public Update(): void {
const indicatedAirspeed = SimVar.GetSimVarValue('AIRSPEED INDICATED', 'knots');
this.eventBus.getPublisher<SpeedEvents>().pub('indicated_airspeed', indicatedAirspeed);
}
SimVar
is another item that comes from the underlying MSFS SDK. You will need to pull in a reference from msfs-types
for the compiler to know where to find it. Add the following line to the top of your file to accomplish this:
/// <reference types="@microsoft/msfs-types/JS/SimVar" />
After you rebuild/resync, you will note that now your "Hello World" text has been replaced by an indicated airspeed value that updates every frame.
Manually writing code to publish data from multiple SimVars to the bus can get tedious. This section of the documentation will walk you through how to leverage framework-provided classes to abstract away much of the boilerplate.
Sometimes Too Much Precision is Too Much
After reloading, you may notice that the precision of your airspeed value is quite high. In fact, probably too high to be of any use in a display. Additionally, each time this value is re-rendered, it alters the DOM slightly and causes a repaint of the affected area, which is something we would like to avoid.
However, this can easily be addressed by simply adding a filter to the bus event consumer in MyComponent
:
const consumer = subscriber.on('indicated_airspeed').withPrecision(0);
Now you will find that your airspeed values will be nice, round, whole numbers, and will only re-render and repaint when that whole number value changes.