Saleae Logic 2 Automation API

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 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{
}
9 Likes

Hey all,

Very nice! I like it.

I have some main points, and some smaller points.

Main points:

  • A simple “rpc Ping (EmptyRequest) returns (PingReply) {}” would be handy for checking that the app is running. For long running tests, IF the application crashes we can just restart it. It could also be like a “rpc GetStatus(Empty) returns (GetStatusReply)” with information like if it’s currently capturing, current device, open tabs(if that’s applicable), current app version?, and any other nice to have info about the current state. If the GetStatusReply begins to grow with a lot of data, I would add a separate “Ping” rpc just to check that the app is running.

  • in message LogicDeviceConfiguration:
    I see no possibility to specify channel names. For manual review after tests that would be very handy for us. Maybe a repeated ChannelOptions channels = 1;… message ChannelOptions { int32 channel_index = 1; string name = 2; double glitch_filter = 3 …

  • To begin with this is not important I think, but maybe in the future (since protobuf is very forward/backwards compatible) you could add an rpc to select a preset with capture settings. Then the user can create some presets in the software and just select those from code, instead of specifying all the settings via rpcs every time. That would be a good beginner way to get complex settings set up remotely.

  • Lastly, perhaps some way to restore the whole application to default. I.e. close all captures, reset all channel settings (names, filters etc). Maybe that’s not necessary, just a thought. If you’re running many tests in succession you don’t want settings from a previous capture to hang around.

Some smaller points (mostly naming and stuff):

  • Since this is a public API I think it’s good to stick to the style guide: Style Guide  |  Protocol Buffers  |  Google Developers
    For enums especially, the enum type should be prepended to the value names, and the 0-value should be “ENUM_TYPE_UNSPECIFIED”. Having a specific “unspecified” value is good to distinguish between no value entered, and “unknown” value entered. If the user omits some enum field, it’s up to the software to determine what default values to use when it gets an unspecified value. For example in enum DigitalTriggerType, when the software gets an unspecified value, it selects rising as the default.

  • Some fields are not clear what unit they are. For example in message GlitchFilterEntry, is pulse_width in seconds? Specifying the unit like “pulse_width_seconds” would be helpful (or just a comment “// in seconds”). It could also be a uint64 pulse_width_nanoseconds = 2, since floats/doubles may work differently in different environments, with rounding and all that (I don’t trust them!). 2^64 nanoseconds still give a max time of ~580 years, which is plenty for me at least :slight_smile:
    Same in DigitalTriggerSettings, min/max_pulse_duration would be good to have clear units, or just uint64 nanos.
    Does the software handle pico-resolution for durations like this? And do people use that? Maybe then the times should be specified by a unique type, like “message Duration { int32 seconds = 1; int32 nanos = 2; int32 picos = 3; }” in that case.
    (Side note, google provides standard “well known types” like dates and durations, but in my experience using these adds dependencies that are annoying, so I personally define my own types for those things).

I’m not very much into analyzers yet so I don’t have any comments on that.

All in all I think it looks great! Looking forward to try it!

4 Likes

We’re working hard on this, but we’re running late! We won’t be able to get the beta out today, but we’ll get it released on Monday.

It’ll be worth it though!

Thanks @wilfor, we’ve already incorporated some of your feedback! We’re polishing up those enum names, adding units, and more, right now.

  • Mark

We’re almost there! Only thing left to do is more testing.

In the meantime, you can take a peak at the documentation (and even download the python library) here:
https://saleae.github.io/logic2-automation/

However it won’t work until we’ve released Logic 2.3.56 tomorrow.

Thanks for your patience!

Hello,

I’m super glad to hear this feature is being implemented!

Our team is working on creating an automated medical device testing system that will integrate a number of hardware fixtures including Saleae Logic Pro 16s. The Logic analyzers will be utilized to validate device communications for a number of products.

The system is built around pytest called from a Jenkins executor running on a slave service, with the intent of regularly and automatically qualifying both test hardware and products. Unfortunately, we are unable to utilize the previous automation API and software, as the GUI cannot launch within a service.

I know it has been discussed on the forum in the past, but with the addition of the automation API, we would like to request support for headless launch as well. This would open support for automation on systems like headless Linux and Windows services such as Jenkins.

1 Like

Fantastic news, and the API looks good. I think what I would really like is a streaming API providing the same data that HLAs and measurement extensions get. It would provide an alternative to this issue HLA: support Python dependencies - Logic 2 - Ideas and Feature Requests - Saleae which has really been limiting the usefulness of HLAs for me.

We’ve just published the first pre-release version of the automation API, included in the Logic 2.3.56 update!

You can now test the first beta release of the new Logic 2 automation interface!

First, update to the latest software release, 2.3.56.

Then, follow the getting started guide here: Getting Started — Saleae 0.0.1 documentation

Please post your feedback, questions, and requests here! We’re going to continue working on this for the next few weeks to improve reliability, improve the API, and more.

This is not ready for production use yet, but we need all the feedback we can get! If you encounter any problems with it, please let us know.

1 Like

Are you planning to publish the Python package on PyPI?

We are planning on adding headless mode soon. I’m not exactly sure what the final dependencies will look like though. In the meantime though, we’ve had luck with the X virtual framebuffer xvfb, which has allowed us to run our application in headless environments before.

Our goal is to allow you to easily launch and connect to the Logic 2 software in a headless mode from Python.

Could you provide us with a bit more information about your environment? That might help us simulate it over here.

1 Like

We have been talking about this a lot internally for the last few weeks, but unfortunately we won’t be adding it the very near future.

Could you send some details about what you would use this feature for, if we had it?

For background, extensions like HLAs and Measurements run inside of our software, in a python standalone build that we package with the application. Since it’s hosted inside of our application, we can expose access to the raw data directly without any IPC overhead.

This new automation API is instead a public interface that you can connect to through an IPC solution, specifically gRPC. This prevents us from directly exposing anything through the interface, since we’re not hosting the python environment.

An idea we’ve been thinking about is a concept similar to macros, where you can write python that has direct access to all the data and configuration within a session - channels, analyzers, etc. This would be running inside our application, and could be shared in the marketplace. Then from the outside, using this automation interface, you could optionally configure and interact with these macros, for example: you could automate captures, automate running the macros, collecting their results, and generating results.

Most likely! We are not going to do this during the pre-release stage though, since we’re expecting to make many breaking changes over the next few weeks. We’ll get back to this once we’re production ready.

Also, the saleae PyPI package name is in use for the community python Logic 1 automation library, so we’ll need a new name :slight_smile:

We are using a Windows 10 PC configured as a Jenkins node, where the Jenkins runtime is launched on boot using a Task in Windows Task Scheduler. Our Jenkinsfile creates a fresh Python virtual environment, which we use to run our test suite via PyTest. If you want, I can send an email containing the configuration instructions used to setup the node.

Since the Task doesn’t have access to a desktop, when it has attempted to launch Logic 1.X in the past the program closed immediately. We considered using xvfb and Logic within a container as a workaround, but Windows made it near impossible to forward the Saleae device, so we abandoned that idea.

A lot of our system is dependent on Windows, so it would be ideal if the solution supports multiple platforms. Looking into it some, there has been previous success getting electron to run headless.

Could you send some details about what you would use this feature for, if we had it?

As mentioned in the linked feature request the primary motivation for me is being able to run parser code with third-party or first-party dependencies, and a streaming API would solve that since the client would be in Python environment I control. I guess the real-time streaming aspect is not so important, and also this could be achieved by exporting the raw data and loading the resulting CSV. It’s just more convenient to have the data directly in memory without having to start capture → stop capture → export → load exported file.

I’ve investigated some more, and I’m not certain the original solution I saw will work for this. I’m going to investigate further and let you know if I find anything.

Thanks Luke, that was the solution I had been looking to try most recently.

That would be great, could you submit it to support? Add “Attn: Mark” and Tim can forward it along to me.
https://contact.saleae.com/hc/en-us/requests/new

Hi Mark, I’m pumped to see you guys are working on this and am looking forward to trying it out. Lack of an automation API has been the final piece holding my team back from moving off of Logic 1, and the new features you’ve been adding to Logic 2 have been making it harder and harder to willingly go back to Logic 1 :slight_smile:

I work in semiconductor manufacturing (USB-C / PD products), our engineers and several of our customers use your HW/SW on a nearly daily basis, mostly to investigate I2C/SPI/UART and USB-PD CC traffic.


In terms of feedback, I haven’t had the chance to use this yet, but it looks pretty sensible, I’ll be setting up some tests this week to give it a better exercise. I’d like to echo the request for Headless mode, sounds like you’re already working on that.

Another thing, not sure if this is really the right place for it, but it seems at least tangentially related. I recently published a HLA (SaleaeSocketTransportHLA) that opens up a socket and allows live-streaming data out of Logic so you can monitor the data in other software and take action as the data comes in. I have to admit that it works better than I expected as a HLA, but it has limitations and the whole time I was working on it I really felt like it should be something built directly into Logic, perhaps as part of the automation API. Is this something you have considered?

Also, the saleae PyPI package name is in use for the community python Logic 1 automation library, so we’ll need a new name

Might I suggest… saleae2

1 Like

Also, the saleae PyPI package name is in use for the community python Logic 1 automation library, so we’ll need a new name

you could make it specific to Logic2 maybe? saleae-logic2