The Open-SCPI-Protocol-Emulator can be used to emulate Keysight measurement devices by implementing the SCPI protocol using TCP to enable integration tests in continuous integration pipelines or to allow developers to write software for Keysight devices without owning an expensive device.
- Open-SCPI-Protocol-Emulator
- Visual Studio 2019
- Plugin: Switchyard
- Plugin: AntlrVSIX (very buggy; consider using the Intellij plugin instead: antlr.org/tools)
- Optional:
- Rider with the ANTLR Intellij plugin
- .NET 5
- NuGet
- Git
To install run:
git clone https://github.com/bluehands/Open-SCPI-Protocol-Emulator.git
To restore NuGet packages navigate into the source directory and run:
dotnet restore
To build navigate into the source directory and run:
dotnet build EmulatorHost.sln
There are two primary ways to start the application:
- Pass the configuration (devicesettings.json) from the console as an escaped json
- Configure the devices inside the devicesettings.json and start the application parameterless (passing the devicesettings.json as parameter overwrites the file configuration)
Only devices with a valid and present configuration will be started. The root nodes for every registered device must be present inside the devicesettings.json but can be empty. Devices with empty root nodes do not start.
{
"Keysight34465AConfiguration": {
"Ip": "127.0.0.1",
"Port": 5025,
"Identification": "Keysight Technologies,34465A,21-BH-34465-EMULATOR,0.01.00-01.00-01.00-01.00-01-01",
"VoltageInterferenceFactors": [
0.0,
0.001,
0.002,
0.001,
0.0,
-0.001,
-0.002,
-0.001
],
"CurrentInterferenceFactors": [
0.0,
0.0001,
0.0002,
0.0001,
0.0,
-0.0001,
-0.0002,
-0.0001
],
"LowImpedanceInterferenceMultiplier": 2.0,
"HighImpedanceInterferenceMultiplier": 1.0,
"CurrentRangeAuto": 1E-05,
"CurrentRangeMin": 1E-05,
"CurrentRangeMax": 1E-05,
"CurrentRangeDef": 1E-05,
"VoltageRangeAuto": 300.0,
"VoltageRangeMin": 100.0,
"VoltageRangeMax": 1000.0,
"VoltageRangeDef": 500.0
},
"Keysight3458AConfiguration": {
"Ip": "127.0.0.1",
"Port": 5026,
"Identification": "Keysight Technologies, 3458A-EMULATOR,0.01.00-01.00-01.00-01.00-01-01",
"VoltageInterferenceFactors": [
0.0,
0.001,
0.002,
0.001,
0.0,
-0.001,
-0.002,
-0.001
],
"CurrentInterferenceFactors": [
0.0,
0.0001,
0.0002,
0.0001,
0.0,
-0.0001,
-0.0002,
-0.0001
],
"CurrentRangeAuto": 1E-05,
"CurrentRangeMin": 1E-05,
"CurrentRangeMax": 1E-05,
"CurrentRangeDef": 1E-05,
"VoltageRangeAuto": 300.0,
"VoltageRangeMin": 100.0,
"VoltageRangeMax": 1000.0,
"VoltageRangeDef": 500.0
}
}
Escape the devicesettings.json and then run:
EmulatorHost.exe "{\r\n \"Keysight34465AConfiguration\": {\r\n \"Ip\": \"127.0.0.1\",\r\n \"Port\": 5025,\r\n \"Identification\": \"Keysight Technologies,34465A,21-BH-34465-EMULATOR,0.01.00-01.00-01.00-01.00-01-01\",\r\n \"VoltageInterferenceFactors\": [\r\n 0.0,\r\n 0.001,\r\n 0.002,\r\n 0.001,\r\n 0.0,\r\n -0.001,\r\n -0.002,\r\n -0.001\r\n ],\r\n \"CurrentInterferenceFactors\": [\r\n 0.0,\r\n 0.0001,\r\n 0.0002,\r\n 0.0001,\r\n 0.0,\r\n -0.0001,\r\n -0.0002,\r\n -0.0001\r\n ],\r\n \"LowImpedanceInterferenceMultiplier\": 2.0,\r\n \"HighImpedanceInterferenceMultiplier\": 1.0,\r\n \"CurrentRangeAuto\": 1E-05,\r\n \"CurrentRangeMin\": 1E-05,\r\n \"CurrentRangeMax\": 1E-05,\r\n \"CurrentRangeDef\": 1E-05,\r\n \"VoltageRangeAuto\": 300.0,\r\n \"VoltageRangeMin\": 100.0,\r\n \"VoltageRangeMax\": 1000.0,\r\n \"VoltageRangeDef\": 500.0\r\n },\r\n \"Keysight3458AConfiguration\": {\r\n \"Ip\": \"127.0.0.1\",\r\n \"Port\": 5026,\r\n \"Identification\": \"Keysight Technologies, 3458A-EMULATOR,0.01.00-01.00-01.00-01.00-01-01\",\r\n \"VoltageInterferenceFactors\": [\r\n 0.0,\r\n 0.001,\r\n 0.002,\r\n 0.001,\r\n 0.0,\r\n -0.001,\r\n -0.002,\r\n -0.001\r\n ],\r\n \"CurrentInterferenceFactors\": [\r\n 0.0,\r\n 0.0001,\r\n 0.0002,\r\n 0.0001,\r\n 0.0,\r\n -0.0001,\r\n -0.0002,\r\n -0.0001\r\n ],\r\n \"CurrentRangeAuto\": 1E-05,\r\n \"CurrentRangeMin\": 1E-05,\r\n \"CurrentRangeMax\": 1E-05,\r\n \"CurrentRangeDef\": 1E-05,\r\n \"VoltageRangeAuto\": 300.0,\r\n \"VoltageRangeMin\": 100.0,\r\n \"VoltageRangeMax\": 1000.0,\r\n \"VoltageRangeDef\": 500.0\r\n }\r\n}"
Edit the devicesettings.json and then run:
EmulatorHost.exe
To add another measurement device the following steps are required:
- Create a new device inside Domain/YourDevice that implements the desired functionalities and states. Consider extending the Domain/KeysightBase/KeysightDeviceBase and using or creating new UnionTypes. Use the Switchyard project to genrate UnionTypes from enums. You can download the VS plugin from here: Switchyard.
- Create a configuration scheme for the new device that implements the Domain/Abstractions/IDeviceConfiguration interface inside Domain/YourDevice and add your device configuration class to the EmulatorHost/Configuration/DeviceConfigurations class.
- Create a new device controller inside the Emulator/Controller project for the new measurement device that implements the Emulator/Controller/IDeviceController<TDeviceCommand> where TDeviceCommand is the parsed representation of an SCPI command.
- Create a new command UnionType inside Emulator/Command
- Create a new parser grammar for the desired (SCPI) inside ProtocolParser/YourDevice commands and build the parser. Check the XML project file to see how to configure the MSBuild to generate an ANTLR parser. antlr.org. Also consider installing a syntax highlighting plugin for ANTLR v4 grammars (available for Rider and VS; Note: the Rider plugin is much better!).
- Create a new visitor for the parser / parser grammar inside Protocol/YourDevice
- Create a new protocol interpreter inside Protocol/Interpreter that implements the Protocol/Interpreter/IProtocolInterpreter
- Register your created services inside the EmulatorHost/Program by using the EmulatorHost/HostedDeviceService. Follow the existing scheme.
- Have fun!
The architecture of the application follows the rules of an Onion Architekture. The basic rule is that no layer must reference an outer layer of the onion structure but outer layer may reference inner layers.
The following sections describe the layers from the outside toward the inside of the onion architecture, which can also be seen in the dependency graph.
The outer layer ist the EmulatorHost layer. It composes all other layers together and is responsible for the startup and configuration of the application. It also contains the dependency injection system, which means that new components, services or devices are registered there.
Project structure:
- EmulatorHost
- EmulatorHostService: composes all parts for a device together and needs to be registered in the DI container
- Configuration
- Contains the configuration deserializer and DeviceConfiguration schema
- Network
- Contains the TCPServer and a fsm for escaping the protocol separator
The Protocol layer is responsible for interpreting and creating command objects from the incoming sequences, that can be passed to the controllers inside the Emulator layer. The Protocol layer references the ProtocolParser layer, which contains the ANTLR SCPI parsers.
Project structure:
- Protocol
- Interpreter
- Contains the device protocol interpreters
- Keysight34465A
- Contains AST visitors for the Keysight34465A SCPI parser
- Keysight3458
- Contains AST visitors for the Keysight3458 SCPI parser
- Interpreter
The ProtocolParser layer contains the ANTLR SCPI grammar and the resulting generated parser for each device.
Project structure:
- ProtocolParser
- Keysight34465A
- Contains grammar and generated parser for the Keysight34465A device
- Keysight3458
- Contains grammar and generated parser for the Keysight3458 device
- Keysight34465A
The Emulator layer is responsible for composing the functionalities of the Domain layer and contains a controller for every device, that accepts device specific commands and executes them. A controller also returns a execution result.
Project structure:
- Emulator
- Command
- Contains the commands an some execution logic for commands. The CommandExecutor is used to buffer the data output from the execution and provide it to the socket server.
- Controller
- Contains the controllers that compose the domain logic
- Command
The Domain layer contains the logic for the behavior of a device and some domain entities.
Project structure:
- Domain
- Abstractions
- Contains Interfaces for the configuration and SocketServer
- Keysight34465A
- Contains the logic for the device and configuration
- Keysight3458A
- Contains the logic for the device and configuration
- KeysightBase
- Contains some shared logic
- UnionTypes
- Contains the domain entities used by the devices and commands
- Abstractions
- A TCP port is opened for every device by the HostedDeviceService
- The TCPServer accepts requests and splits them at the SCPI separator "\n" using an fsm
- The HostedDeviceService subscribes to the TCPServer and passes incoming sequences to the ProtocolInterpreter
- The ProtocolInterpreter passes the sequence to the device specific parser
- The Parser generates an syntax tree which is passed to the device specific Visitor by the ProtocolInterpreter
- The Visitor visits the syntax tree and generates a device specific command containing parameters
- The HostedDeviceService passes the command to a CommandExecutor that contains an output queue. TCPServer of a device polls this output queue and sends the data just like a real device
- The CommandExecutor passes the command together with the output queue to the device specific Controller
- The Controller matches the command depending on the type and calls the domain logic of a device. The Controller also pushes output values generated by the domain to the output queue
- The Device accepts the parameters of a command and applies state changes. The device also generates the output values that are pushed to the output queue
- The HostedDeviceService writes the execution result of a Controller to the console
- The TCPServer server as already mentioned pools the output queue and sends the generated values
Long form:
- Keysight-34465A
- *IDN?
- READ?
- ABORt
- CONFigure:CURRent:AC
- CONFigure:CURRent:DC
- MEASure:CURRent:AC?
- MEASure:CURRent:DC?
- CONFigure:VOLTage:AC
- CONFigure:VOLTage:DC
- MEASure:VOLTage:AC?
- MEASure:VOLTage:DC?
- DISPlay:TEXT
- DISPlay:TEXT:CLEar
- SENSe:VOLTage:IMPedance:AUTO
- Keysight-3458
- *IDN?
- READ?
- ABORt
- CONFigure:CURRent:AC
- CONFigure:CURRent:DC
- MEASure:CURRent:AC?
- MEASure:CURRent:DC?
- CONFigure:VOLTage:AC
- CONFigure:VOLTage:DC
- MEASure:VOLTage:AC?
- MEASure:VOLTage:DC?
We're looking forward for your pull requests.
MIT License
- Transform the ANTLR grammar to a general grammar that describes the SCPI protocol as the current grammar only supports a small subset of the protocol, which is not quite good extensible.
- Produce an AST instead of just a CST to remove duplicate visitors for every device.
- Extend the functionalities of the existing devices (only command used for our purpose are implemented)
- Extend the tests