Today, I'm pleased to introduce you to Michael Gerfen and Andy Gray from Evolution Software, sharing a bit about their company and the Hazelnut Monitor product. Take it away Michael and Andy!
Evolution Software is a boutique software company which creates cutting-edge software solutions for a wide variety of industries, including healthcare, ecommerce, and agriculture. We spend a lot of time staying on top of the latest trends in software development, but we don't chase every fad and we generally only adopt technology that fits well into our development process.
Why Aurelia?
We predominately develop software using the .NET stack and have written apps using a variety of Microsoft technologies over the 25 years that Evolution has been in existence. When XAML-based frameworks (WPF and Silverlight) came along, we built apps using Caliburn.Micro - and we still do to this day, including Xamarin apps. Being comfortable with EisenbergEffect 's approach to MVVM, we took a hard look at Aurelia way back in 2014 when it was in beta. Five years is a long time in the JavaScript world; Aurelia has stood the test of time for us.
The first Aurelia application we built was an adaptive survey engine -- think TurboTax -- where user's answers affected the next set of questions presented to them. We took an early stab at server-side rendering by dynamically generating the JavaScript files used by Aurelia to render the UI. We published that app at the beginning of 2015 and it's still in production today.
We also used Aurelia to build an enterprise management system for a one hundred and fifty year-old company. Because the company is quite old, it is also quite conservative in its technology uptake. We used Aurelia for that project since programming in TypeScript is very familiar to the company's developers who have used ASP.NET MVC and C# to build web applications, and learning Aurelia is easy since it favors convention over configuration.
Hazelnut Monitor
Recently Microsoft did a case study on a solution we built for the hazelnut industry in Oregon. The application makes use of the ML.NET machine learning framework to predict the moisture content of hazelnuts during the drying process.
Originally we considered building native phone apps for the project, but we ended up developing a responsive single-page web application using Aurelia instead. Not only did using Aurelia reduce the cost of building the client application, it allowed the use of the same familiar UI across phones, tablets, and PCs. That choice also eliminated the approval process for the various app stores, allowing us to publish updates quickly.
Business Problem
The two companies which sponsored the project buy hazelnuts from many different orchards throughout Oregon's Willamette Valley. The farmers take their harvested nuts to a network of independently owned drying facilities. While the process itself is similar from facility to facility, each of the drying companies has its own unique approach to drying the hazelnuts, which can lead to inconsistency in the delivered moisture content of the nuts. If the nuts are dried too much, then the drying and processing companies lose money. If the moisture content of the nuts is too high, there's a chance for spoilage and more lost revenue. The processing companies wanted more insight into how the nuts were being dried.
Anatomy of the Application
To that end, we worked with all of the companies to instrument the hazelnut dryers with sensors which can measure temperature, relative humidity, and barometric pressure. We set up the sensors to report data once a minute by calling to a REST API. The server receives the data and uses the machine learning model to predict the moisture based on the collected data from the sensors, the variety of the hazelnuts, the weight of the batch, and the elapsed time since the start of the batch. The raw sensor data and the predicted moisture are then sent to Aurelia client applications via SignalR.
Note: some identifying information has been removed from this image.
Designing the User Interface with Aurelia
While some workers can only see the data for the dryers at a single location, there are also super users who need access to data across all of the drying locations. In order to display a lot of data on small screens we needed to be able to show and hide the data in an easy to consume manner -- one which our non-technical end users would find easy to use. Using Bootstrap as a front-end CSS framework, we designed a combination of nested cards with the collapse component to build an intuitive UI. At the top of each location card, we added a "quick indicator" Aurelia component which is a series of dots, each of which represents the status of one dryer, so that an end user can easily see which dryers may need attention without having to open.
The main UI component for the app is the location-tableau:
<template>
<require from="./location-card"></require>
<div id="accordian">
<template repeat.for="location of applicationState.locations">
<location-card model.bind="location"></location-card>
</template>
</div>
</template>
We utilize Aurelia's repeater to render a location-card
for each location a user has rights to view:
<template>
...markup elided for clarity
<div id="collapse${location.code}" class="collapse ${isFirst ? 'show' : ''}" aria-labelledby="heading${location.code}" data-parent="#accordion">
<div class="card-body">
<div class="card-deck">
<template repeat.for="structure of location.structures">
<dryer-overview-card model.bind="dryer"></dryer-overview-card>
<div if.bind="$odd" class="w-100 d-none d-sm-block d-md-none"></div>
</template>
</div>
</div>
</div>
...markup elided for clarity
</template>
Each location-card
similarly utilizes the repeater to render a dryer-overview-card
for each dryer.
Real-time Data
The client applications receive data from the server in real-time via
SignalR
. Handling SignalR messaging in Aurelia was straightforward: we created a TypeScript class called HubManager
to manage one LocationHub
per location. We use Aurelia's EventAggregator
to communicate when data arrives from SignalR to the rest of the application. One of the most difficult apsects of writing the app was handling disconnects and re-establishing reconnects with SignalR. Each LocationHub
signals to HubManager
when it disconnects and the HubManager
handles reconnecting to SignalR on a timer.
export class HubManager {
//...code elided for brevity
constructor(eventAggregator: EventAggregator) {
this.hubClosedSubscription = this.eventAggregator.subscribe(HubEvents.signalRDisconnect, locationName => {
if (this.locationHubDictionary.has(locationName)) {
applicationLogger.debug(`Removing "${locationName}" hub`);
const locationHub = this.locationHubDictionary.get(locationName);
locationHub.dispose();
this.locationHubDictionary.delete(locationName);
} else {
applicationLogger.debug(`Could not find "${locationName}" hub`);
}
});
// Force the SignalR hubs to be refreshed if the app has idled too long.
setInterval(() => {
var now = new Date().getTime();
if ((now - this.lastSync) > this.syncInterval) {
if (this.locationHubDictionary.size === 0) {
applicationLogger.debug("Conditions met to recreate all location hubs.");
this.lastSync = now;
this.refreshHubs();
}
}
}, 5000); // Check every 5 seconds whether a minute has passed since last sync
}
//...code elided for brevity
}
export class LocationHub {
//...code elided for brevity
createHubConnection(location: Location) {
this.hubConnection = new HubConnectionBuilder()
.withUrl(LocationHub.signalRUrl)
.build();
this.hubConnection.on(HubEvents.incomingEstimatedMoistureEvent,
(currentMeasurementData: CurrentMeasurementsData) => {
this.eventAggregator.publish(HubEvents.incomingEstimatedMoistureEvent, currentMeasurementData);
});
this.hubConnection.on(HubEvents.incomingDataEvent,
(sensorData: SensorDatum[]) => {
this.eventAggregator.publish(HubEvents.incomingDataEvent, sensorData);
});
this.hubConnection.on(HubEvents.incomingDeltaEvent,
(deltas: SensorDatum[]) => {
this.eventAggregator.publish(HubEvents.incomingDeltaEvent, deltas);
});
this.hubConnection.onclose(error => {
if (error) {
applicationLogger.debug("LocationHub: Connection closed", error);
// Signal to the HubManager that this hub has disconnected its connection to the server.
this.eventAggregator.publish(HubEvents.signalRDisconnect, this.location.code);
}
});
}
}
Localization
The folks who do the day-to-day operation of the Hazelnut dryers are a mixture of older farmers and non-native English speakers. We built localization into the Aurelia client application from the start using the aurelia-i18n
plugin. End users are able choose their preferred language and the user interface is automatically rendered accordingly.
Conclusion
This could easily be a multi-part blog series: we only touched on a handful of the Aurelia features and components we used to build the app, and didn't have space to discuss value converters, custom elements and attributes, aurelia-dialog, aurelia-notification, aurelia-fetch-client, and more. The main point, however, is that Aurelia made it easy for us to incorporate Bootstrap and SignalR into a responsive single-page application that works well on phones, tablets, and PCs. Aurelia's commitment to open standards and component-based architecture resonated with us in 2014, and it still does today.