Architecture¶
InstrumentControl is a layered .NET 10 WPF application. This page describes the major layers, their responsibilities, and how they interact.
Solution Structure¶
InstrumentControl.sln
├── src/
│ ├── InstrumentControl.Core/ ← Core library (no WPF UI code)
│ └── InstrumentControl.App/ ← WPF executable
└── instruments/
├── HP34401A/ ← Plugin DLL
├── Agilent34970A/ ← Plugin DLL
├── Keithley2000/ ← Plugin DLL
├── ItechIT6922B/ ← Plugin DLL
├── RTB2004/ ← Plugin DLL
└── CTSChamber/ ← Plugin DLL
Layered Architecture¶
graph TD
subgraph App ["InstrumentControl.App (WPF)"]
MW[MainWindow + ViewModel]
SE[SequenceEditorView + ViewModel]
DV[DataViewerView + ViewModel]
CM[ConnectionManagerWindow]
BC[BlockCanvas control]
BP[BlockPropertiesEditor control]
end
subgraph Core ["InstrumentControl.Core (.NET library)"]
VS[VisaService]
PL[PluginLoader]
ENG[SequenceEngine]
DM[DataManager]
subgraph Ifaces ["Interfaces"]
IID[IInstrumentDriver]
ISB[ISequenceBlock]
ICP[IConnectionProvider]
end
subgraph Bases ["Base Classes"]
IDB[InstrumentDriverBase]
SBB[SequenceBlockBase]
end
end
subgraph Plugins ["Plugin DLLs (instruments/)"]
HP[HP34401A.dll]
AG[Agilent34970A.dll]
KE[Keithley2000.dll]
IT[ItechIT6922B.dll]
RS[RTB2004.dll]
CT[CTSChamber.dll]
end
App --> Core
Plugins --> Core
PL --> Plugins
VS --> ICP
HP --> IID
HP --> ISB
InstrumentControl.Core¶
The core library has no dependency on WPF beyond what is strictly required for the FrameworkElement return type in IInstrumentDriver.CreateFrontPanel(). All business logic lives here.
Services¶
| Service | File | Responsibility |
|---|---|---|
VisaService |
Services/VisaService.cs |
P/Invoke to NI-VISA DLL; simulation mode fallback |
PluginLoader |
Services/PluginLoader.cs |
Reflection-based DLL scanning from instruments/ folder |
SequenceEngine |
Services/SequenceEngine.cs |
Block execution state machine; BlockRegistry |
DataManager |
Services/DataManager.cs |
Thread-safe result storage; CSV export |
Interfaces¶
| Interface | Responsibility |
|---|---|
IInstrumentDriver |
Plugin contract: connect, disconnect, create front panel, provide blocks |
ISequenceBlock |
Block contract: define properties, execute, serialize |
IConnectionProvider |
Transport abstraction: open, close, write, query, read |
IHasBodyOutput |
Marker for blocks with body/next dual outputs (LoopBlock, ConditionBlock) |
Base Classes¶
| Class | Responsibility |
|---|---|
InstrumentDriverBase |
Default ConnectAsync (IDN parse, firmware extraction); helper methods Query(), Write(), QueryDouble() |
SequenceBlockBase |
Auto-ID generation; GetProp<T>() helper; Clone(); Serialize()/Deserialize() |
Models¶
| Model | Purpose |
|---|---|
MeasurementResult |
One data point: timestamp, instrument, channel, parameter, value, unit |
BlockData |
JSON-serializable block state (type, position, properties) |
SequenceDefinition |
A named collection of BlockData with metadata |
BlockExecutionResult |
Outcome of a block run: success flag, next block ID, optional output value |
BlockPropertyDefinition |
Metadata for a block property: name, editor type, options, default value |
SequenceContext |
Per-run context passed to every block: instruments, variables, log callback, cancellation token |
InstrumentInfo |
Instrument metadata: resource name, driver, firmware, serial number, connection status |
InstrumentControl.App¶
The WPF application follows the MVVM pattern using CommunityToolkit.Mvvm source generators.
Views and ViewModels¶
| View | ViewModel | Responsibility |
|---|---|---|
MainWindow |
MainWindowViewModel |
Instrument list; tab navigation; toolbar commands |
SequenceEditorView |
SequenceEditorViewModel |
Canvas state; run/pause/stop; file open/save |
FrontPanelView |
(plugin-provided) | Host for the plugin's CreateFrontPanel() element |
DataViewerView |
DataViewerViewModel |
OxyPlot series; data table; CSV export |
ConnectionManagerWindow |
ConnectionManagerViewModel |
VISA scan; IDN query; driver selection |
AboutWindow |
(none) | Version and license info |
Custom Controls¶
| Control | Responsibility |
|---|---|
BlockCanvas |
Renders blocks as colored rectangles on a drag-and-drop canvas; draws connection arrows; handles mouse events |
BlockPropertiesEditor |
Dynamically generates WPF input controls (TextBox, ComboBox, CheckBox, etc.) from BlockPropertyDefinition[] |
Execution Flow¶
Application Startup¶
sequenceDiagram
participant P as Program.cs
participant V as Velopack
participant A as App.xaml.cs
participant PL as PluginLoader
participant MW as MainWindow
P->>V: Check for update (GitHub Releases)
V-->>P: Update available / not available
P->>A: Start WPF application
A->>PL: LoadPlugins("instruments/")
PL-->>A: List of IInstrumentDriver instances
A->>MW: Show window
Connecting an Instrument¶
sequenceDiagram
participant U as User
participant CM as ConnectionManager
participant VS as VisaService
participant DRV as Driver
U->>CM: Click "Add Instrument"
CM->>VS: FindResources()
VS-->>CM: ["GPIB0::1::INSTR", ...]
U->>CM: Select resource
CM->>VS: OpenSession(resource)
VS-->>CM: IConnectionProvider
CM->>DRV: ConnectAsync(provider)
DRV->>DRV: Send *IDN?, parse response
DRV-->>CM: Success, InstrumentInfo
CM-->>U: Instrument added to sidebar
Sequence Execution¶
sequenceDiagram
participant U as User
participant ENG as SequenceEngine
participant CTX as SequenceContext
participant BLK as ISequenceBlock
participant DM as DataManager
U->>ENG: RunAsync(sequence, instruments)
ENG->>CTX: Create context
ENG->>BLK: Find StartBlock
loop Until null NextBlockId or cancellation
ENG->>BLK: ExecuteAsync(context)
BLK->>CTX: SetVariable / Log / OnMeasurement
CTX->>DM: AddResult(MeasurementResult)
BLK-->>ENG: BlockExecutionResult{NextBlockId}
end
ENG-->>U: Completed / Error
Plugin Loading¶
PluginLoader scans instruments/*.dll at startup:
// Pseudocode — see PluginLoader.cs for full implementation
foreach (var dllPath in Directory.GetFiles("instruments", "*.dll"))
{
var assembly = Assembly.LoadFrom(dllPath);
foreach (var type in assembly.GetTypes())
{
if (typeof(IInstrumentDriver).IsAssignableFrom(type) && !type.IsAbstract)
{
var driver = (IInstrumentDriver)Activator.CreateInstance(type)!;
drivers.Add(driver);
}
}
}
No MEF exports are required. The [InstrumentDriver] attribute that appears on some drivers is for documentation only.
Block Registry¶
SequenceEngine contains a static BlockRegistry — a dictionary mapping block type strings to factory functions:
// Each block class registers itself in its static constructor:
static WaitBlock()
{
BlockRegistry.Register("WaitBlock", () => new WaitBlock());
}
When the sequence engine loads a saved sequence JSON, it calls BlockRegistry.Create("WaitBlock") to instantiate the correct type. Instrument plugins register their blocks the same way — because the DLLs are loaded before any sequence is opened, all blocks are available.
Connection Provider Architecture¶
The IConnectionProvider interface decouples transport from driver logic:
Driver code calls: provider.QueryAsync("CONF:VOLT:DC")
↓
VisaConnectionProvider → NI-VISA viWrite / viRead
CTSSerialConnectionProvider → encode frame → COM port → decode frame → return ASCII
SimulatedConnectionProvider → return hardcoded default response
Drivers do not know which provider is in use. Custom serial devices override ConnectAsync() to swap in their provider before the base class helper methods are called.
Threading Model¶
- UI thread: All WPF rendering, ViewModel property changes
- Sequence engine: Runs on a background thread (
Task.Run); posts results back viaDispatcher.InvokeAsyncandObservableCollection.Addthread-safe wrappers - DataManager: Thread-safe via
lock (_results)— can receive results from any thread - VISA calls: Blocking I/O on the sequence engine thread; no async VISA API used
- Pause/Resume: Implemented via
TaskCompletionSource<bool>— no polling, zero CPU while paused