Now that you have the code running from Part 1 of this series, let’s dive into the structure of the application, the motivations for its architecture, and some key concepts before we continue with extending the codebase for your application.
The package includes seven projects: four core libraries, the two main application projects, and a test project. Both the main application projects are dependent on the “Core Four” libraries for their configuration, common interfaces, dependency injection, and common UI elements. By decoupling these from their application projects, they can be developed independently from one another without code duplication and differences in implementations. Additionally, both applications will benefit from back-end improvements and can retain functional parity between them.
The ‘Core Four’
The motivation to extract out the common componentry was borne out of experience, and realizing that – if we consider the DRY principle – there are a few key things that are shared between the applications and should be encapsulated from active development. Why maintain multiple implementations of the same thing, if you don’t have to?
The ‘Core Four’ are comprised of the following libraries:
Configuration and Core Services
The main driver for this architecture is to maintain configurability, reduce hard-coding, and allow for a single SW version to exist on multiple HW configurations without having to deploy a new version of the application. Using the configuration/DI system classes, services, settings and calibration values can be injected at runtime, without having to recompile the code. Using a configuration driven approach generally allows both developers and users to progress in their development tasks without blocking the other from their deliverable. It allows an Interface driven approach to GUI building without a dependency on concrete implementations.
See here for the documentation on how to use the configuration system in this package.
Devices and UI
To foster code reuse and extensibility, the devices and their associated UI components are organized into their own project structure. In this manner, both the Control and Engineering (and other applications, such as dedicated workstation applications), can be composed with common device logic and a unified UI experience.
The DeviceUI project contains the ‘widgets’ for interacting with the back-end device logic. Following the MVVM pattern, the UI controls are composed of a view and view-model that can be incorporated into your XAML like standard tags. For example, if one wants to add a motor control to one of their engineering views, you can accomplish this as follows:
And will render the following for a single motor system:
Now, anywhere this control is utilized will have consistent behavior that will give users a more unified experience across the different applications. Additionally, any back-end changes that are made will automatically be reflected in the UI, without having to comb through the code. You can even extend these controls if more functionality is required to be exposed.
One thought that may come to mind is, what to do when there are multiple of the same device in the system? A developer shouldn’t have to rewrite the same controls because (let’s say) the mechanical team has added a second motor stage to the platform. This is where the configuration system comes into play.
As an example, let’s say your instrument has three motors (one for each axis: x,y,z). First, the configuration file needs to be updated to reflect the composition of the instrument. Find the system_config.ini in the Engineering project (Control has its own config file as well).
Note the view-model for the main device showcase view takes an array of IMotor, meaning it will inject the concrete instances of that interface based on the system configuration.
public DeviceShowcaseViewModel(IMotor[] motors, ISensor[] sensors, ICamera[] cameras) |
For the three different motors, update the config file with the following entries:
[XMotor] |
Finally, the control needs to be updated to use the SimpleMotorControlView as a template, instead of hard-coding three separate instances in the XAML for the device showcase, you can do the following:
<TabControl> ... </TabControl> |
Starting the application now renders the three motors which can be separately interacted with, without any issue:
You can follow this pattern for any devices that exist on your system. The package comes with a few simulated devices that would exist in a DNA sequencing machine, as a guide to this implementation. Real devices will require their own implementations, and as such the application will need a way to connect to their firmware. Included is a ‘FirmwareProtocol’ folder with implementations to connect to the device over TCP/IP and a FirmwareLed class that utilizes it as an example.
This will get you started on an application that exposes more manual controls of the devices in the system. In the next part of the series, we’ll explore the structure of a customer/end-user facing application, and the patterns required to extend your application.
Need more help? Have questions? Reach out to us.