Hello!
We’re excited to announce the first look at our new automation interface that we’re adding to the Logic 2 software. We’re still working out when we expect it to be ready for production use, but we’re hopeful that will take about one month more.
Update September 12th 2022
The official production release of the API version 1.0.0 has been released in the latest software release, 2.4.0!
We’ve also launched a new automation landing page on our website here: Logic2 Automation
Thanks again to all of the folks who provided feedback here and through other channels, we hope that we’ve been able to build a solid automation interface to meet your needs.
Of course there is still unlimited room for improvement and new functionality. We would love to continue to collect your feedback, and continue to make improvements!
Update July 12th 2022
The first pre-release version of the new automation interface is available now!
Check out this post below to get started automating Logic 2!
First, some background. Automation support for Logic 2 has been the most requested feature in Logic 2 for some time. When we started work on Logic 2, we didn’t initially prioritize automation support, because we (wrongly) believed that the automation interface in the Logic 1 software was largely unused, and that it would not be an important feature for most users. We’ve since learned this was not the case.
We’ve been collecting information from users for some time on their needs & experiences with the Logic 1.x API, and from the very beginning we’ve designed the new Logic 2 API to be a significant improvement over the Logic 1.x API. In some ways, the new API may appear similar to the old one; however every feature, function, parameter, and type has been carefully considered after reviewing an avalanche of feedback from the community. We’re continuing to refine the API, and would love your feedback.
Scope of the Automation Interface
There is an enormous amount of functionality that we would like to expose through our automation interface. To start with, we’re keeping a narrow focus on support for completely hands-off, automated environments like long term testing, or continuous integration servers. If you recall our 1.x software heavily relied on manual up-front configuration, like setting up analyzers. The first iteration of the Logic 2 interface is quite the opposite - 100% of the capture configuration is done from code, but with predictable defaults to avoid unnecessarily verbose code.
You can see an overview of the functionality in the examples and proto file below. Initially, we’re focusing on a narrow set of features, to keep the surface area smaller while we focus on improving reliability and testing over the coming weeks. Longer term, once this first release is ready for production use, we do want to expand the functionality of the interface dramatically.
Pre-Release
As mentioned above, we expect to have a beta available this Friday. However, until the automation interface is released in the production version of the software, the API is subject to change - and there is much to improve before then! Not the least of which is a “naming” pass we plan to do before production release where we will improve the understandability of the names of functions, arguments, and types of the API.
Feedback
We need your feedback! We would love for you to take a look at the sample python below, as well as the gRPC proto file below, and let us know what you think!
When providing feedback, we would appreciate it if you included some context:
- can you describe your use case, industry, and how automation helps?
- can you describe the environment where the Logic 2 software, automation software, and device are operating?
Technical / Examples
The new Logic 2 API is accessible via gRPC (https://grpc.io/). We will be open-sourcing both our gRPC .proto definition file so users can connect directly to the automation API using their preferred gRPC-supported language, and our own official Python wrapper. You can find examples of each below.
Example that runs a capture and exports the raw channel data to a CSV.
from saleae.automation import *
# Connect to an existing instance of the Logic 2 Application
#
# This is useful for local scripting or for debugging during development, but we
# will also provide a way to launch a new instance as a GUI or in a headless mode.
# We expect that the headless mode will be the preferred method for CI/testing environments.
#
manager = Manager.connect_to_existing()
# Quick analog/digital capture
# By default, the capture will use "Looping" mode, and will need to be stopped manually.
capture = manager.start_capture(
device_serial_number="F4241",
device_configuration=LogicDeviceConfiguration(
enabled_digital_channels=[3, 4],
enabled_analog_channels=[3, 4],
digital_sample_rate=500_000_000,
analog_sample_rate=12_500_000,
),
)
# Do something with the device-under-test.
dut.do_something()
# Manually stop the capture.
#
# When using a trigger or timer to stop the capture, `capture.wait()` can be used
# to wait until the trigger is hit.
#
capture.stop()
# Export all data to CSVs
capture.export_raw_data_csv(directory='test_output')
# Save the capture for later inspection
capture.save('test_capture.sal')
# Close the capture, releasing all resources
capture.close()
Example that runs a capture with a complete set of options, adds an analyzer, and exports the analyzer data to a CSV file.
# Start a new capture with a full set of capture options
capture = manager.start_capture(
device_serial_number="F4241",
device_configuration=LogicDeviceConfiguration(
enabled_digital_channels=[3, 4, 5, 6, 7],
digital_sample_rate=500_000_000,
digital_threshold=3.3,
glitch_filters=[GlitchFilterEntry(channel_index=3, pulse_width=100e-9)],
),
capture_settings=CaptureSettings(
buffer_size=2048,
capture_mode=CaptureMode.STOP_ON_DIGITAL_TRIGGER,
digital_trigger=DigitalTriggerSettings(
trigger_type=DigitalTriggerType.RISING,
record_after_trigger_time=1,
trigger_channel_index=6,
linked_channels=[
DigitalTriggerLinkedChannel(
7, DigitalTriggerLinkedChannelState.HIGH
)
],
),
),
)
# Do something with the device-under-test.
dut.do_something()
# Wait until the trigger fires
capture.wait()
# Add an SPI analyzer - parameters should match those shown in the Logic 2 GUI
spi_analyzer = capture.add_analyzer('SPI', label=f'My SPI Analyzer', settings={
'MISO': 4,
'Clock': 3,
'Enable': 5,
'Bits per Transfer': '16 Bits per Transfer'
})
# Export the SPI analyzer data to a CSV
capture.export_data_table(filepath='my_spi_data.csv', analyzers=[spi_analyzer])
# Close the capture
capture.close()
gRPC Proto Definition
syntax = "proto3";
option java_multiple_files = true;
option java_package = "saleae";
option java_outer_classname = "SaleaeProto";
option objc_class_prefix = "Saleae";
package saleae.automation;
/*****************************************************************************
*
* gRPC API
*
****************************************************************************/
service Manager {
// Get list of connected devices.
rpc GetDevices (GetDevicesRequest) returns (GetDevicesReply) {}
// Start a capture
rpc StartCapture (StartCaptureRequest) returns (StartCaptureReply) {}
// Stop an active capture
rpc StopCapture (StopCaptureRequest) returns (StopCaptureReply) {}
// Wait until a capture has completed
rpc WaitCapture (WaitCaptureRequest) returns (WaitCaptureReply) {}
// Load a capture from file.
rpc LoadCapture (LoadCaptureRequest) returns (LoadCaptureReply) {}
// Save a capture to file.
rpc SaveCapture (SaveCaptureRequest) returns (SaveCaptureReply) {}
// Close a capture.
// Note: It is recommended to close a capture once it is no longer being used so that any
// consumed resources can be released.
rpc CloseCapture (CloseCaptureRequest) returns (CloseCaptureReply) {}
// Add an analyzer to a capture.
rpc AddAnalyzer (AddAnalyzerRequest) returns (AddAnalyzerReply) {}
// Remove an analyzer from a capture.
rpc RemoveAnalyzer (RemoveAnalyzerRequest) returns (RemoveAnalyzerReply) {}
// Export raw channel data to CSV files.
rpc ExportRawDataCsv (ExportRawDataCsvRequest) returns (ExportRawDataCsvReply) {}
// Export raw channel data to binary files.
rpc ExportRawDataBinary (ExportRawDataBinaryRequest) returns (ExportRawDataBinaryReply) {}
// Export analyzer data to CSV file.
rpc ExportDataTable (ExportDataTableRequest) returns (ExportDataTableReply) {}
// Export custom analyzer export data to file.
rpc ExportAnalyzerLegacy (ExportAnalyzerLegacyRequest) returns (ExportAnalyzerLegacyReply) {}
}
/*****************************************************************************
*
* Core Types
*
****************************************************************************/
enum ErrorCode {
UNKNOWN_ERROR_CODE = 0; // Not used
// Unexpected Saleae Internal Error.
INTERNAL_EXCEPTION = 1;
// Request is invalid, usually because of invalid arguments.
//
// Examples:
// Invalid Capture Id - capture does not exist
// Missing filepath
INVALID_REQUEST = 10;
LOAD_CAPTURE_FAILED = 20;
CAPTURE_IN_PROGRESS = 21;
UNSUPPORTED_FILE_TYPE = 22;
MISSING_DEVICE = 50;
DEVICE_ERROR = 51;
OOM = 52;
}
enum RadixType {
UNKNOWN_RADIX_TYPE = 0;
BINARY = 1;
DECIMAL = 2;
HEXADECIMAL = 3;
ASCII = 4;
};
enum DeviceType {
// Invalid Device Type
UNKNOWN_DEVICE_TYPE = 0;
// Saleae Logic 8
LOGIC_8 = 1;
// Saleae Logic Pro 8
LOGIC_PRO_8 = 2;
// Saleae Logic Pro 16
LOGIC_PRO_16 = 3;
}
// Device descriptor object.
message Device {
// The id used to identify this device
uint64 device_id = 1;
// The type of this device
DeviceType device_type = 2;
// The serial number of this device
string serial_number = 3;
}
enum ChannelType {
UNKNOWN_CHANNEL_TYPE = 0;
// Digial channel.
DIGITAL = 1;
// Analog data.
ANALOG = 2;
}
// Identification for a channel.
message ChannelIdentifier {
// Device id
uint64 device_id = 1;
// Channel type.
ChannelType type = 2;
// Index of channel.
uint64 index = 3;
}
message CaptureInfo {
// Id of the capture.
uint64 capture_id = 1;
}
message LogicDeviceConfiguration {
// Digital channel indices to enabled
repeated uint32 enabled_digital_channels = 1;
// Analog channel indices to enabled
repeated uint32 enabled_analog_channels = 2;
// Digital Sample Rate
uint32 digital_sample_rate = 3;
// Analog Sample Rate
uint32 analog_sample_rate = 4;
// For Pro 8 and Pro 16, this can be one of: 1.2, 1.8, or 3.3
// For other devices this is ignored
double digital_threshold = 5;
// Glitch filter to apply to digital data
repeated GlitchFilterEntry glitch_filters = 6;
}
message GlitchFilterEntry {
uint32 channel_index = 1;
double pulse_width = 2;
}
message CaptureSettings {
// Capture buffer size (in megabytes)
uint32 buffer_size = 1;
CaptureMode capture_mode = 2;
// Time to stop capture after (in seconds)
//
// Only applies if capture_mode is `CaptureMode.STOP_AFTER_TIME`
double stop_after_time = 3;
// Duration to trim data down to after capture completes
//
// When trigger is active, we trim relative to the trigger, not the end of capture
double trim_time = 4;
DigitalTriggerSettings digital_trigger = 5;
}
enum CaptureMode {
CIRCULAR = 0;
STOP_AFTER_TIME = 1;
STOP_ON_DIGITAL_TRIGGER = 2;
}
message DigitalTriggerSettings {
DigitalTriggerType trigger_type = 1;
double record_after_trigger_time = 2;
uint32 trigger_channel_index = 3;
double min_pulse_duration = 4;
double max_pulse_duration = 5;
repeated DigitalTriggerLinkedChannel linked_channels = 6;
}
enum DigitalTriggerType {
RISING = 0;
FALLING = 1;
PULSE_HIGH = 2;
PULSE_LOW = 3;
}
message DigitalTriggerLinkedChannel {
uint32 channel_index = 1;
DigitalTriggerLinkedChannelState state = 2;
}
enum DigitalTriggerLinkedChannelState {
LOW = 0;
HIGH = 1;
}
/*****************************************************************************
*
* Request/Reply Messages
*
****************************************************************************/
message GetDevicesRequest {
}
message GetDevicesReply {
repeated Device devices = 1;
}
// Start Capture
message StartCaptureRequest {
string device_serial_number = 1;
oneof device_configuration {
LogicDeviceConfiguration logic_device_configuration = 2;
}
CaptureSettings capture_settings = 3;
}
message StartCaptureReply {
CaptureInfo capture_info = 1;
}
// Stop Capture
message StopCaptureRequest {
uint64 capture_id = 1;
}
message StopCaptureReply {
}
// Stop Capture
message WaitCaptureRequest {
uint64 capture_id = 1;
}
message WaitCaptureReply {
}
// Load Capture
message LoadCaptureRequest {
// Filepath of Logic 2 .sal capture file to load.
string filepath = 1;
}
message LoadCaptureReply {
// Information about the capture that was loaded.
CaptureInfo capture_info = 1;
}
message SaveCaptureRequest {
// Id of capture to save.
uint64 capture_id = 1;
// Full filepath to save the file to, usually ending in ".sal".
string filepath = 2;
}
message SaveCaptureReply {
}
message CloseCaptureRequest {
// Id of capture to close.
uint64 capture_id = 1;
}
message CloseCaptureReply {
}
message ExportRawDataCsvRequest {
// Id of capture to export data from.
uint64 capture_id = 1;
// Directory to create exported CSV files in.
string directory = 2;
// Channels to export.
repeated ChannelIdentifier channels = 3;
// Must be between 1 and 1,000,000, inclusive.
uint64 analog_downsample_ratio = 4;
// If true, timestamps will be in ISO8601 format.
bool iso8601 = 5;
}
message ExportRawDataCsvReply {
}
message ExportRawDataBinaryRequest {
// Id of capture to export data from.
uint64 capture_id = 1;
// Directory to create exported binary files in.
string directory = 2;
// Channels to export.
repeated ChannelIdentifier channels = 3;
// Must be between 1 and 1,000,000, inclusive.
uint64 analog_downsample_ratio = 4;
}
message ExportRawDataBinaryReply {
}
message AnalyzerSettingValue {
oneof value {
// String value
string string_value = 1;
// Integer value
int64 int64_value = 2;
// Boolean value
bool bool_value = 3;
// Double floating-point value
double double_value = 4;
}
}
message AddAnalyzerRequest {
// Id of capture to add analyzer to.
uint64 capture_id = 1;
// Name of analyzer. This should exactly match the name seen in the application.
// Examples: "SPI", "I2C", "Async Serial"
string analyzer_name = 2;
// User-facing name for the analyzer.
string analyzer_label = 3;
// Analyzer settings. These should match the names shown in analyzer's settings
// shown in the application.
map<string, AnalyzerSettingValue> settings = 4;
}
message AddAnalyzerReply {
// Id of the newly created analyzer.
uint64 analyzer_id = 1;
}
message RemoveAnalyzerRequest {
// Id of capture to remove analyzer from.
uint64 capture_id = 1;
// Id of analyzer to remove.
uint64 analyzer_id = 2;
}
message RemoveAnalyzerReply {
}
message ExportDataTableRequest {
// Id of capture to export data from.
uint64 capture_id = 1;
// Path to export CSV data to.
string filepath = 2;
// Id of analyzers to export data from.
repeated uint64 analyzer_ids = 3;
// If true, timestamps will be in ISO8601 format.
bool iso8601 = 5;
}
message ExportDataTableReply{
}
message ExportAnalyzerLegacyRequest {
// Id of capture to export data from.
uint64 capture_id = 1;
// Path to export data to.
string filepath = 2;
// Id of analyzer to export data from.
uint64 analyzer_id = 3;
// Radix to use for exported data.
RadixType radix_type = 4;
}
message ExportAnalyzerLegacyReply{
}