# Saleae API Documentation (Complete) > All three Saleae Python API suites: Extension, Automation, and MSO. --- # Saleae Extension API > Build custom protocol analyzers, measurement extensions, and data processing tools for Saleae Logic 2. ## Guides ## Create and Use Extensions ### Creating an Extension In this guide, we will walk your through creating an HLA (High Level Analyzer) extension, however, the process is identical for other types. 1. Click the 'Extensions' panel button on the right of the software [Image] 2\. Click 'Create Extension'\ 3\. Under 'Generate from template', choose the type of extension you would like to create. For this example, we will create a High Level Analyzer.\ 4\. _**(Optional)**_ Click 'Additional Information' to fill in information about your extension. [Image] 5\. Click 'Save As...' to save and select your location.\ 6\. You should now see your new extension listed as 'Local' in the software. [Image] ### Using a High Level Analyzer Extension 1\. To test the new Sample HLA, capture any protocol data for [one of the supported analyzers](/extensions/guides/analyzer_framev2), and add the appropriate protocol analyzer. We've provided an I2C capture below in case you don't have a protocol data recording available. [download:](static/I2C.sal) 2\. Click the Analyzers '+' button to add our Sample HLA. [Image] 3\. In the settings popup, select 'I2C' under Input Analyzer. For the rest of the settings, you can leave them as default and click 'Finish'. Once you add the HLA, you can see it as a virtual channel as shown in the image below. [Image] #### Customize your High Level Analyzer To edit the Sample HLA (perhaps as a starting point to creating your own HLA), you can click the 'Local' button next to 'Sample HLA' under the Extensions panel. This will open the containing folder for your extension files which you can update for your needs. Check out our [High Level Analyzer](/extensions/guides/high-level-analyzer-extensions) article to learn more about customizing your HLA. ### Using a Measurement Extension The software currently has a few built-in measurements already installed and ready to use. The gif below demonstrates how to use them. You can also hold the shift key while dragging across your recorded data to add a measurement without using the sidebar. The use of measurement extensions allows additional custom measurements to be made. [Image: Logic 2 measurements] To see your new measurement in action, take a capture of digital data and add a measurement to it as shown above. You should see the new measurements: [Image: Measurement metrics] #### Customize your Measurement ‌To edit the Sample Measurement, you can click the 'Local' button next to 'Sample Measurement' under the Extensions panel. This will open the containing folder for your extension files which you can update for your needs.‌ Check out our [Measurement](/extensions/guides/measurement-extensions) article to learn more about customizing your Measurement. ## Extension Installation There are 2 methods to installing an extension. You can install an extension from within the software via our in-app Extensions Marketplace, or you can install it via local source files on your PC. #### Extension Installation within the Software This will be the easiest method of installing an extension. Clicking on the Extensions button on the right will open up a list of published extensions from us and from the community. Afterwards, you can click "Install" for the extension of your choice. [Image] #### Install Extensions Manually In some cases, the extensions list may not appear properly within the Extensions panel of the software. This may be due to security settings such as a firewall, or simply having the PC offline. To get around this, extensions can be found on GitHub and can be downloaded manually using the following steps. [Image] 1. On a computer where the marketplace loads, click the extension of interest so that it is highlighted. This will open up the extension information within the software. 2. Click the "Repository" link. This opens the [github.com](http://github.com/) repository for that extension. 3. Click the green "Code" button, and select download as zip. 4. Extract the zip file anywhere. 5. In the Logic 2 software, on the Extensions panel, click the "three-dots" menu icon, select "Load Existing Extension..." and select the downloaded extension.json file. [Image] #### Uninstalling an Extension To uninstall an extension, click on the 3 dots next to an extension in the Extensions panel. Then click Uninstall. [Image: Uninstalling an Extension] ## Extension File Format Extensions are composed of at least three files, `extension.json`, `readme.md`, and one or more python files. The Logic 2 software uses **Python version 3.8**. #### extension.json File Layout A single extension can contain multiple high level analyzers, measurements, or both. This example is a for a single extension that contains one high level analyzer and one digital measurement. ```javascript { "version": "0.0.1", "apiVersion": "1.0.0", "author": "Mark Garrison", "description": "Utilities to measure I2C speed and utilization", "name": "I2C Utilities", "extensions": { "I2C EEPROM Reader": { "type": "HighLevelAnalyzer", "entryPoint": "I2cUtilities.Eeprom" }, "I2C clock speed measurement": { "type": "DigitalMeasurement", "entryPoint": "I2cUtilities.I2cClock", "metrics": { "i2cClockSpeed": { "name": "I2C Clock Speed", "notation": "N MHz" }, "i2cBusUtilization": { "name": "I2C Bus Utilization", "notation": "N %" } } } } } ``` The `author`, `description`, and `name` properties manage what appears in the Extensions panel where the extensions are managed. | Property | Example | Description | | ----------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | version | "0.0.1" | The version if this extension. Increase this when publishing updates in order to allow users to update. | | apiVersion | "1.0.0" | The Saleae API version used. This should remain "1.0.0" until we make changes to the extension API. | | author | "Mark Garrison" | Your name | | description | "Utilities to measure I2C..." | A one line description for display in the marketplace | | name | "I2C Utilities" | The name of the extension package, for display. Note - this does not have to match the names of the individual extensions, which are shown elsewhere. | | extensions | see below | An object with one key per high level analyzer or measurement. | The `extensions` section describes each high level analyzer or measurement that is contained in this extension package. In the example above, there are 2 extensions listed. ```javascript "extensions": { "I2C EEPROM Reader": { "type": "HighLevelAnalyzer", "entryPoint": "I2cUtilities.Eeprom" }, "I2C clock speed measurement": { "type": "DigitalMeasurement", "entryPoint": "I2cUtilities.I2cClock", "metrics": { "i2cClockSpeed": { "name": "I2C Clock Speed", "notation": "N MHz" }, "i2cBusUtilization": { "name": "I2C Bus Utilization", "notation": "N %" } } } } ``` There are three different types of extension here, as indicated by the type property. "HighLevelAnalyzer", "DigitalMeasurement", and "AnalogMeasurement". There are some differences between these types. ##### Type: `HighLevelAnalyzer` | Property | Example | Description | | ---------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | type | "HighLevelAnalyzer" | This must be the string "HighLevelAnalyzer" | | entryPoint | "I2cUtilities.Eeprom" | The python entry point. The first part is the python filename without the extension, and the second part is the name of the class in that file | ##### Type: `AnalogMeasurement` and `DigitalMeasurement` | Property | Example | Description | | -------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | type | "DigitalMeasurement" | This must be either "DigitalMeasurement" or "AnalogMeasurement" | | entryPoint | "I2cUtilities.I2cClock" | The python entry point. The first part is the python filename without the extension, and the second part is the name of the class in that file | | metrics | "i2cClockSpeed": { "name": "I2C Clock Speed", "notation": "N MHz" } | Object that contains one property for each single numeric metric you wish to display. If your measurement class produces 4 different numbers, all 4 must be present here. Each metric property name must match the name of the metric produced by your python class. | | metrics\[X].name | "I2C Clock Speed" | The display name for a given metric | | metrics\[X].notation | "N MHz" |

The display string for formatting the metric. Limited HTML tags are supported: ['b', 'i', 'em', 'strong', 'sub']

Example: "N <sub>t</sub>"

| ## High-Level Analyzer (HLA) Extensions This guide assumes that you have familiarity with the [Python](https://www.python.org/) programming language. It is what will be used to customize our HLA. ### Overview This guide assumes [you have generated](/extensions/guides/extensions-quickstart) a new High-Level Analyzer. In this guide you will learn about: 1. The files included in the HLA template extension and what they are. 2. The different parts of `HighLevelAnalyzer.py`. 3. How to process input analyzer frames and output new analyzer frames. ### High Level Analyzer Files In your new High Level Analyzer (HLA) extension folder you will find 3 files: * `README.md` * Documentation for your extension, shown within Logic 2 when you select an extension, and what users will see if you put your extension on the Marketplace. * `extension.json` * Every extension must have this file in its root directory. * Contains metadata about the extension, and the HLAs and Measurement scripts that are included with the extension. * See [Extension File Format](/extensions/guides/extension-file-format) for more information. * `HighLevelAnalyzer.py` * Python source code for your HLA. For the purposes of this document, we will be focusing on `HighLevelAnalyzer.py` ### HighLevelAnalyzer.py Breakdown Let's break down the contents of `HighLevelAnalyzer.py` . ```python ## HighLevelAnalyzer.py from saleae.analyzers import HighLevelAnalyzer, AnalyzerFrame, StringSetting, NumberSetting, ChoicesSetting class MyHla(HighLevelAnalyzer): # Settings: my_string_setting = StringSetting() my_number_setting = NumberSetting(min_value=0, max_value=100) my_choices_setting = ChoicesSetting(['A', 'B']) # Output formats result_types = { 'mytype': { 'format': 'Output type: {{type}}, Input type: {{data.input_type}}' } } # Initialization def __init__(self): print("Settings:", self.my_string_setting, self.my_number_setting, self.my_choices_setting) # Data Processing def decode(self, frame: AnalyzerFrame): return AnalyzerFrame('mytype', frame.start_time, frame.end_time, { 'input_type': frame.type }) ``` ##### Imports ```python from saleae.analyzers import HighLevelAnalyzer, AnalyzerFrame, StringSetting, NumberSetting, ChoicesSetting ``` ##### Declaration and Settings All HLAs must subclass [`HighLevelAnalyzer`](/extensions/api/analyzers/HighLevelAnalyzer), and additionally output [`AnalyzerFrame`s](/extensions/api/analyzers/AnalyzerFrame). The `Setting` classes are included so we can specify settings options within the Logic 2 UI. ```python class MyHla(HighLevelAnalyzer): my_string_setting = StringSetting(label='My String') my_number_setting = NumberSetting(label='My Number', min_value=0, max_value=100) my_choices_setting = ChoicesSetting(label='My Choice', ['A', 'B']) ``` This declares our new HLA class, which extends from HighLevelAnalyzer, and 3 settings options that will be shown within the Logic 2 UI. Note: if the name of the class MyHla() is used it must be referenced in the accompanying json file. Note that Hla() is the default value. ##### Output formats ```python result_types = { 'mytype': { 'format': 'Output type: {{type}}, Input type: {{data.input_type}}' } } ``` This specifies how we want output AnalyzerFrames to be displayed within the Logic 2 UI. We will come back to this later. ##### Initialization ```python def __init__(self): print("Settings:", self.my_string_setting, self.my_number_setting, self.my_choices_setting) ``` This is called when your HLA is first created, before processing begins. The values for the settings options declared at the top of this class will be available as instance variables here. In this case, the settings values will be printed out and visible within the Logic 2 terminal view. ##### Data Processing ```python def decode(self, frame: AnalyzerFrame): return AnalyzerFrame('mytype', frame.start_time, frame.end_time, { 'input_type': frame.type }) ``` This is where the bulk of the work will be done. This function is called every time the input to this HLA produces a new frame. It is also where we can return and output new frames, to be displayed within the Logic 2 UI. In this case we are outputting a new frame of type `'mytype'`, which spans the same period of time as the input frame, and has 1 data value `'input_type'` that contains the value of the `type` of the input frame. ### HLA Debugging Tips Although we don't have the ability to attach debuggers to Python extensions at the moment, here are some suggestions to help debug your HLA. * Use `print()` statements to print debug messages to our in-app terminal. More information on our in-app terminal can be found below. [Data Table & Terminal View](https://support.saleae.com/user-guide/using-logic/data-table-and-terminal) * Use the Wall Clock Format and Timing Markers to locate the exact frame listed in your error message. [Time Bar Settings](https://support.saleae.com/product/user-guide/using-logic/time-bar-settings) [Measurements, Timing Markers & Notes](https://support.saleae.com/user-guide/using-logic/measurements-timing-markers) * Use the reload source button in the app to quickly re-run your HLA after editing your source code. [Image: "Reload Extension" button] ### Example - Writing an HLA to search for a value Now that we've gone over the different parts of an HLA, we will be updating our example HLA to search for a value from an Async Serial analyzer. #### Example Data In the Extensions Quickstart you should have downloaded and opened a capture of i2c data. For this quickstart we will be using a capture of Async Serial data that repeats the message "Hello Saleae". [download:Async Serial Example Data](static/hla-quickstart.zip) #### Remove Unneeded Code To start, let's remove most of the code from the example HLA, and replace the settings with a single `search_for` setting, which we will be using later. ```python from saleae.analyzers import HighLevelAnalyzer, AnalyzerFrame, StringSetting class MyHla(HighLevelAnalyzer): search_for = StringSetting() result_types = { 'mytype': { 'format': 'Output type: {{type}}, Input type: {{data.input_type}}' } } def __init__(self): pass def decode(self, frame: AnalyzerFrame): return AnalyzerFrame('mytype', frame.start_time, frame.end_time, { 'input_type': frame.type }) ``` If you open the example data from above and add this analyzer, selecting the Async Serial analyzer as input, you should see the following when zooming in: [Image] Our HLA (top) is outputting a frame for every frame from the input analyzer (bottom), and displaying their types. #### Understanding the Input Frames The goal is to search for a message within the input analyzer, but first we need to understand what frames the input analyzer (Async Serial in this case) produces so we can know what frames will be passed into the `decode(frame:` [`AnalyzerFrame`](/extensions/api/analyzers/AnalyzerFrame)`)` function. The frame formats are documented under [Analyzer Frame Types](/extensions/guides/analyzer_framev2), where we can find [Async Serial](/extensions/guides/analyzer_framev2#serial-analyzer). The Async Serial output is simple - it only outputs one frame type, `data`, with 3 fields: `data` , `error`, and `address`. The serial data we are looking at will not be configured to produce frames with the `address` field, so we can ignore that. To recap, the `decode(frame)` function in our HLA will be called once for each frame from the Async Serial analyzer, where: * `frame.type` will always be `data` * `frame.data['data']` will be a \`bytes\` object with the data for that frame * `frame.data['error']` will be set if there was an error #### Updating \`decode()\` to search for "H" or "l" Now that we we understand the input data, let's update our HLA to search for the character `"H"`. ```python def decode(self, frame: AnalyzerFrame): # The `data` field only contains one byte try: ch = frame.data['data'].decode('ascii') except: # Not an ASCII character return # If ch is 'H' or 'l', output a frame if ch in 'Hl': return AnalyzerFrame('mytype', frame.start_time, frame.end_time, { 'input_type': frame.type }) ``` After applying the changes, you can open the menu for your HLA and select `Reload Source Files` to reload your HLA: [Image] You should now only see HLA frames where the Async Serial frame is an `H` or `l`: [Image] #### Replace the hardcoded search with a setting Now that we can search for characters, it would be much more flexible to allow the user to choose the characters to search for - this is where our `search_for` setting that we added earlier comes in. ```python class MyHla(HighLevelAnalyzer): search_for = StringSetting() ``` Instead of using the hardcoded `'Hl'`, let's replace that with the value of `search_for`: ```python ## In decode() ## If the character matches the one we are searching for, output a new frame if ch in self.search_for: return AnalyzerFrame('mytype', frame.start_time, frame.end_time, { 'input_type': frame.type }) ``` Now if you can specify the characters to search for in your HLA settings: [Image: Click Edit to show the settings] [Image: Set the "Search For" setting] [Image: Now only the values 'S' and 'H' have frames] ##### Updating the display string To update the display string shown in the analyzer bubbles, the `format` string in `result_types` variable will need to be updated. `'mytype'` will also be updated to `'match'` to better represent that the frame represents a matched character. ```python result_types = { 'match': { 'format': 'Found: {{data.char}}' } } ``` And in `decode()`: we need to update the data in `AnalyzerFrame` to include `'char'`, and update the frame `'type'` to `'match'`. ```python ## If the character matches the one we are searching for, output a new frame if ch in self.search_for: return AnalyzerFrame('match', frame.start_time, frame.end_time, { 'char': ch }) ``` After reloading your HLA you should see the new display strings: [Image: That's a lot more descriptive!] ##### Using time `AnalyzerFrame`s include a `start_time` and `end_time`. These get passed as the second and third parameter of `AnalyzerFrame`, and can be used to control the time span of a frame. Let's use it to fill in the gaps between the matching frames. Let's add a `__init__()` to initialize the 2 time variables we will use to track the span of time that doesn't have a match: ```python def __init__(self): self.no_match_start_time = None self.no_match_end_time = None ``` And update `decode()` to track these variables: ```python ## If the character matches the one we are searching for, output a new frame if ch in self.search_for: frames = [] # If we had a region of no matches, output a frame for it if self.no_match_start_time is not None and self.no_match_end_time is not None: frames.append(AnalyzerFrame( 'nomatch', self.no_match_start_time, self.no_match_end_time, {})) # Reset match start/end variables self.no_match_start_time = None self.no_match_end_time = None frames.append(AnalyzerFrame('match', frame.start_time, frame.end_time, { 'char': ch })) return frames else: # This frame doesn't match, so let's track when it began, and when it might end if self.no_match_start_time is None: self.no_match_start_time = frame.start_time self.no_match_end_time = frame.end_time ``` And lastly, add an entry in `result_types` for our new `AnalyzerFrame` type `'nomatch'`: ```python result_types = { 'match': { 'format': 'Match: {{data.char}}' }, 'nomatch': { 'format': 'No Match' } } ``` The final output after reloading: [Image] ### What's Next? * Find out about other analyzers and the AnalyzerFrames they output in the [Analyzer Frame Types](/extensions/guides/analyzer_framev2) documentation. * Use the [API Documentation](/extensions/api/analyzers/) as a reference. * Browse the Saleae Marketplace in Logic 2 for more ideas and examples of extensions you can create. * [Publish your extension](/extensions/guides/publish-an-extension) to the Saleae Marketplace! ## HLA - Analyzer Frame Format Python High Level Analyzers allow users to write custom code that processes the output of an analyzer. The below list of pre-installed low level analyzers can be immediately used with high level analyzers (HLAs). * [Async Serial](/extensions/guides/analyzer_framev2#serial-analyzer) * [I2C](/extensions/guides/analyzer_framev2#i2c-analyzer) * [SPI](/extensions/guides/analyzer_framev2#spi-analyzer) * [CAN](/extensions/guides/analyzer_framev2#can-analyzer) * [LIN](/extensions/guides/analyzer_framev2#lin-analyzer) * [Manchester](/extensions/guides/analyzer_framev2#manchester-analyzer) * [Parallel](/extensions/guides/analyzer_framev2#simple-parallel-analyzer) * [LED](/extensions/guides/analyzer_framev2#async-rgb-led-analyzer) * [1-Wire](/extensions/guides/analyzer_framev2#one-wire-analyzer) * [I2S / PCM](/extensions/guides/analyzer_framev2#i2s-analyzer) #### Adding HLA Support for More Analyzers We've released documentation on our FrameV2 API below, which can be used to add HLA support for any low level analyzer that is not listed above, including custom analyzers that were created using our Protocol Analyzer SDK. [FrameV2 HLA Support - Analyzer SDK](https://support.saleae.com/saleae-api-and-sdk/protocol-analyzer-sdk/framev2-hla-support-analyzer-sdk) #### Writing an HLA In order to write a high level analyzer, the data format produced by the connected source analyzer must be understood. For example, a high level analyzer which consumes serial data needs to understand the serial analyzer output format in order to extract bytes, and this code is very different from the code required to extract data bytes from CAN data. **Reading Serial Data** ```python def decode(self, frame: AnalyzerFrame): print(frame.data['data']) ``` **Reading CAN Data** ```python def decode(self, frame: AnalyzerFrame): if frame.type == 'identifier_field': print(frame.data['identifier']) elif frame.type == 'data_field': print(frame.data['data']) elif frame.type == 'crc_field': print(frame.data['crc']) ``` To write a Python high level analyzer for a specific input analyzer, navigate to the section for that analyzer. There is one page for each analyzer that is compatible with python HLAs. Each analyzer produces one or more frame types. These frame types have string names and will be in the `type` member of the frame class. Each frame type may have data properties. The documentation page will list the data properties for that frame type, along with the data type and a description. These can be accessed from the `frame.data` dictionary. Here is an example of you you might handle different frame types from I2C: ```python def decode(self, frame): if frame.type == 'address': if frame.data['read'] == True: print('read from ' + str(frame.data['address'][0])) else print('write to ' + str(frame.data['address'][0])) elif frame.type == 'data': print(frame.data['data'][0]) elif frame.type == 'start': print('I2C start condition') elif frame.type == 'stop': print('I2C stop condition') ``` #### State Managment In some cases, such in I2C HLA programming, each frame only contains a single byte. Therefore a state machine is needed to keep track of the byes as they are received so that thye can be properly interpreted, specifically if multiple control modes or multi-byte instructions are required. For example the first byte of the data payload is often the slave addres, followed by a control byte. The control byte would determine if follow-on bytes should be interpreted as commands or memory register data. The following example provides some recommendations for state mangement. ##### An example set of states follows: 1. *Idle:* Waiting for I2C transaction to begin - Next State: waiting for Start 2. *Start:* I2C start condition signals the beginning of the transaction - Next State: waiting for the expected slave device Address 3. *I2C Slave Address:* Typically a 7-bit device address is read, direction bit (read/write) is determined - Next State: waiting for wainting for control byte - Note that the I2C LLA automatically shifts the address back right by 1 bit to recover the original 7-bit address. 4. *I2C Control Byte:* When used, it often determines how follow-on bytes should be interpreted (commands or data) - Next State: waiting for data 4. *Data:* Data can be supplied as one or more bytes - The I2C LLA (low level analyzer) pass data bytes, one byte at a time to the HLA for decoding. - If a multi-byte instruction is received, tracking of the previous byte(s) may be requied to interpret the current byte correctly. - Next State: Loop to receive next Data byte until Stop is received 6. *Stop:* I2C stop condition signals the end of the transaction - Next State: return to Idle #### Instruction Set Lookup Table Developing an instruction set lookup table in JSON is best practice that allows you to quickly build an analyzer that interprets received data into human readable annotations. Often at a minum the instruction, name, and numbrer of parameters is needed to build out the state machine. ``` instructions = { 0x81: {"name": "Set Contrast Control", "param_description": "Contrast values (0-255)", "params": 1}, 0xA4: {"name": "Entire Display OFF", "param_description": "", "params": 0}, 0xA5: {"name": "Entire Display ON", "param_description": "", "params": 0} } ``` ## Measurement Extensions This guide assumes that you have familiarity with the [Python](https://www.python.org/) programming language. It is what will be used to customize our Measurement. Browse user shared [Analog Measurement Extensions](https://support.saleae.com/extensions/measurement-extensions/analog-measurement-extensions) and [Digital Measurement Extensions](https://support.saleae.com/extensions/measurement-extensions/digital-measurement-extensions) for own use or inspiration. ### Overview This guide assumes [you have generated](/extensions/guides/extensions-quickstart) a new Digital Measurement. In this guide you will learn about: 1. The files included in the Digital Measurement template extension and what they are. 2. The different parts of `DigitalMeasurement.py`. 3. How to update the template to make your own measurements. ### Measurement Files In your new Measurement extension folder you will find 3 files: * `README.md` * Documentation for your extension, shown within Logic 2 when you select an extension, and what users will see if you put your extension on the Marketplace. * `extension.json` * Every extension must have this file in its root directory. * Contains metadata about the extension, and the HLAs and Measurement scripts that are included with the extension. * See [Extension File Format](/extensions/guides/extension-file-format) for more information. * `DigitalMeasurement.py` or `AnalogMeasurement.py` * Python source code for your measurement. #### DigitalMeasurement.py and AnalogMeasurement.py Digital measurements are implemented with a class template that looks like below. Take a look at [`pulseCount.py`](https://github.com/saleae/logic2-extensions-examples/blob/master/pulseCount/pulseCount.py) to see how this was modified for our Pulse Count extension. ```python from saleae.range_measurements import DigitalMeasurer class MyDigitalMeasurement(DigitalMeasurer): # Add supported_measurements here. This includes the metric # strings that were defined in the extension.json file. def __init__(self, requested_measurements): super().__init__(requested_measurements) # Initialize your variables here def process_data(self, data): for t, bitstate in data: # Process data here def measure(self): values = {} # Assign the final metric results here to the values object return values ``` Analog measurements are implemented with a class template that looks like below. Take a look at [`voltage_peak_to_peak.py`](https://github.com/saleae/logic2-extensions-examples/blob/master/voltagePeakToPeak/voltage_peak_to_peak.py) to see how this was modified for an example analog extension. ```python from saleae.range_measurements import AnalogMeasurer class VoltageStatisticsMeasurer(AnalogMeasurer): # Add supported_measurements here. This includes the metric # strings that were defined in the extension.json file. def __init__(self, requested_measurements): super().__init__(requested_measurements) # Initialize your variables here def process_data(self, data): # Process data here def measure(self): values = {} # Assign the final metric results here to the values object return values ``` #### Measurer creation In python, your class will be constructed when the user adds or edits a measurement. This instance of your class will be used for a single computation. Your class can either process analog data or digital data, but not both. A class may handle as many metrics as you want though. If you want to implement both digital and analog measurements, you will need at a minimum two classes. #### Constructor The constructor will be called with an array of requested measurements, which are taken from the `extension.json` file. In our [`pulseCount.py`](https://github.com/saleae/logic2-extensions-examples/blob/master/pulseCount/pulseCount.py) example, this is declared like so: ```python POSITIVE_PULSES = 'positivePulses' NEGATIVE_PULSES = 'negativePulses' class PosNegPulseMeasurer(DigitalMeasurer): supported_measurements = [POSITIVE_PULSES, NEGATIVE_PULSES] ``` #### Process Data Immediately after construction, the function `def process_data(self, data):` will be called one or more times. This function takes a parameter `data` which differs between analog and digital measurements. The Saleae Logic software stores collected data in chunks. To keep python processing performant, the Logic software passes these blocks, or sections of these blocks, one at a time to your measurement. If the requested measurement range does not line up with the internal block storage, the objects passed to python will already be adjusted to the measurement range, so no work needs to be done to handle this condition. This makes it impossible to know exactly how much data will be needed for the given measurement range the first time `process_data` is called. Be sure to update the internal state of your class in such a way that this isn't a problem. For example, when computing the average analog value over a range, it would be best to hold the sum of all values passed to `process_data` and the total count of samples in data members, and only compute the average in the `measure` function. ##### Process Analog Measurements For analog measurements, `data` is an instance of `AnalogData`, which is an `iterable` class with the properties `sample_count` and `samples`. `sample_count` is a number, and is the number of analog samples in the data instance. Note - this might not be the total number of analog samples passed to your measurement, since `process_data` may be called more than once if the user selected range spans multiple chunks. `samples` is a [numpy](https://numpy.org/) [ndarray](https://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html). For information handling this type, please refer to the numpy documentation. The `process_data` function should not return a value. Instead, it should update the internal state of your class, such that the `measure` function can produce your measurement's results. ##### Process Digital Measurements For digital measurement classes, the `data` parameter is an instance of the `iterable` Saleae class `DigitalData`. Each iteration returns a pair of values - the current time, as a `GraphTime` class instance, and the current bit state as a `bool`. (`True` = signal high). The object is essentially a list with the timestamp of each transition inside of the user selected region of digital data. The `GraphTime` has one feature. One `GraphTime` can be subtracted from another to compute the difference in seconds, as a `GraphTimeDelta`. `GraphTimeDelta` can be converted to a float using `float(graph_time_delta)`. This allows your code to compute the time in between transitions, or the time duration between the beginning of the measurement and any transition inside of the measurement range, but it does not expose absolute timestamps. For example, to compute the total number of transitions over the user selected range, this could be used: ```python def __init__(self, requested_measurements): super().__init__(requested_measurements) self.first_transition_time = None self.edge_count = 0 def process_data(self, data): for t, bitstate in data: if self.first_transition_time is None: self.first_transition_time = t else # note: the first entry does not indicate a transition, it's simply the bitstate and time at the beginning of the user selected range. self.edge_count += 1 ``` Currently, the `DigitalData` collection will first include the starting time and bit state, and then every transition that exists in the user selected range, if any. It also exposes the starting and ending time of the user-selected measurement range. Consult the [API Documentation](/extensions/api/analyzers/) for details. #### Measure The `def measure(self):` function will be called on your class once all data has been passed to `process_data`. This will only be called once and should return a dictionary with one key for every `requested_measurements` entry that was passed into your class's constructor. In the future, we may allow the user to select which metrics to compute. To avoid unnecessary processing, it's recommended to check the `requested_measurements` provided by the constructor before computing or returning those values. However, returning measurements that were not requested is allowed. The results will just be ignored. ### What's Next? * Browse the Saleae Marketplace in Logic 2 for more ideas and examples of extensions you can create. * [Publish your extension](/extensions/guides/publish-an-extension) to the Saleae Marketplace! ## Publish an Extension Publishing your extension to the Saleae Marketplace will make it readily available to anyone who uses our software. With your help, we're hoping to provide a growing list of feature extensions that our users can benefit from! #### Prerequisites Before publishing your extension, you will need to have the following completed. * You have finished developing your extension and are ready to share it. You can follow along with our Extensions Quickstart guide below as a starting point. [Extensions Quickstart](/extensions/guides/extensions-quickstart) * Your extension must be uploaded to a [GitHub](https://github.com/) repository. * You must [create a release](https://help.github.com/en/github/administering-a-repository/managing-releases-in-a-repository) for your extension. You can take a look at an [example GitHub repository](https://github.com/timreyes/sampleHLA) for our Sample HLA, which we will use in the following guide. #### Publishing an Extension 1\. Once you've finished developing your extension, click 'Publish' under the Extensions panel for your extension. [Image] 2\. Clicking 'Publish' should open your browser and load our extensions submission page. Provide your GitHub repository URL here and click 'Submit'. [Image] If clicking the Publish button doesn't open your browser, you can manually reach our Extensions Marketplace Publish site via the link below: [https://marketplace.saleae.com/publish](https://marketplace.saleae.com/publish) 3\. Afterwards, you'll be taken to a new page to authorize Saleae Marketplace to access your GitHub account. If clicking "Submit" takes you to an error page on GitHub's website, please ensure you are logged in with an account that has administrator permissions over the repository you are attempting to publish. Also, if the repository is part of an organization, there may be some policies for the organization that might be causing the error. 4\. Click 'Authorize' and you should immediately receive an email confirming that your extension has been added to the Marketplace. The web page should also confirm that your extension was submitted successfully. [Image] 5\. Click the three dots at the top of the Extensions panel and click 'Check for Extension Updates.' [Image] 6\. Congratulations! If the publish was successful, your extension should now appear in the software like below. [Image] ## About Third-Party Extensions Our Extensions Marketplace allows you to publish extensions that you create, or to install extensions published by other members of the community. Extensions are community-written Python modules. Extensions are not installed automatically, except for a few Saleae-created extensions. Before installing an extension, please keep in mind the following: * Non-Saleae extensions were created and published by members of the community. * Saleae cannot endorse their contents. * Please treat extensions the same way you would other programs downloaded from the internet. * All extensions are open source and can viewed by clicking the “Repository” link in the extension details. [Image: Repository link from within the extension details] If you have any problems with an extension, please be sure to create an issue on the extension’s GitHub repository. If you suspect an extension does not comply with the [Saleae Marketplace Partner Agreement](https://downloads.saleae.com/Saleae+Marketplace+Partner+Agreement.pdf), please [contact Saleae support](https://contact.saleae.com/hc/en-us/requests/new) or click on the "Report" button from within the extension details. ## Disabling Marketplace Extensions In case you don't plan on using Marketplace Extensions with our software and would like to disable access to third party extensions entirely, you may follow the instructions below. This may be common in cases where your IT department does not approve usage of external extensions with our software. #### Disabling via an Environment Variable Our software provides a mechanism to turn off the marketplace. Specifically, you can prevent the software from fetching the list of available extensions by setting the environment variable below. `SALEAE_OFFLINE_MARKETPLACE` For example, from the command line, you can execute the following: `cd "\Program Files\Logic" set SALEAE_OFFLINE_MARKETPLACE=1 Logic.exe` For PowerShell, it would be as follows: `$Env:SALEAE_OFFLINE_MARKETPLACE=1` When set, the software will show an error on launch saying it was unable to load the marketplace, and the marketplace sidebar will only show locally installed extensions (in other words, extensions we shipped with the software, or extensions the user manually installed locally). Please note that this capability was not intended to be used for security purposes. We added this to make it easy for us to test the offline behavior of the marketplace without needing to go offline. ## API Reference ### saleae.range_measurements Deprecated: v1 range measurement extension API. This module is still functional but has been superseded by ``saleae.measurements``. New measurement extensions should use that module instead. #### `Measurer` Deprecated: Base class for v1 range measurement extensions. **Attributes:** | Name | Type | Description | |------|------|-------------| | `requested_measurements` | `List[str]` | The list of measurement keys the user has enabled for this run. | **Methods:** ##### `__init__(requested_measurements: List[str])` Create a measurer for the given requested measurements. - **requested_measurements**: Measurement keys the user has enabled. ##### `measure() -> Dict[str, float]` Return computed measurement values. Called after all data has been processed. Must return a dictionary mapping measurement keys to their computed float values. Only keys present in `requested_measurements` need to be included. - **Returns**: A dict mapping measurement keys to float values. #### `AnalogMeasurer` Deprecated: Base class for v1 analog range measurement extensions. **Attributes:** | Name | Type | Description | |------|------|-------------| | `processed_sample_count` | `int` | The number of samples processed so far. | **Methods:** ##### `__init__(args = (), kwargs = {})` ##### `process_data(data: AnalogData)` Process a chunk of analog data. The default implementation calls `process_sample` for each sample. Override this to process data in bulk for better performance. - **data**: A chunk of analog sample data. ##### `process_sample(sample: float, index: int)` Process a single analog sample. Called by the default `process_data` implementation. Override this for simple per-sample processing. - **sample**: The voltage value of the sample. - **index**: The absolute sample index within the measurement range. #### `DigitalMeasurer` Deprecated: Base class for v1 digital range measurement extensions. **Methods:** ##### `process_data(data: DigitalData)` Process a chunk of digital data. Override this to iterate over digital edges and accumulate measurement state. - **data**: A chunk of digital signal data. ### saleae.data #### `AnalogData` **Attributes:** | Name | Type | Description | |------|------|-------------| | `raw_samples` | `ndarray` | | | `voltage_transform` | | | | `start_time` | | | | `end_time` | | | | `sample_count` | `int` | The number of samples contained in this instance. | | `samples` | `ndarray` | Samples after applying voltage scaling. | **Methods:** ##### `__init__(raw_samples: ndarray, voltage_transform_gain: float, voltage_transform_offset: float, start_time: GraphTime, end_time: GraphTime)` ##### `slice_samples(r: slice) -> AnalogData` Allows creating an `AnalogData` from a subset of this one's samples. #### `DigitalData` A segment of digital signal data. **Attributes:** | Name | Type | Description | |------|------|-------------| | `start_time` | `SaleaeTime` | The start of the time range covered by this data segment. | | `end_time` | `SaleaeTime` | The end of the time range covered by this data segment. | | `time_slice` | `slice` | A ``slice(start_time, end_time)`` covering this data segment. | **Methods:** ##### `slice_samples(s: slice) -> DigitalData` Return a sub-range of this digital data. The slice uses sample indices (integers), not times. Step is not supported. - **s**: A slice specifying the sample range. - **Returns**: A new DigitalData covering the sliced range. #### `SaleaeTime` A high-precision wall clock time. **Methods:** ##### `__init__(datetime: datetime, *, millisecond: float = 0, microsecond: float = 0, nanosecond: float = 0, picosecond: float = 0) -> None` Construct a SaleaeTime from a datetime and optional sub-millisecond values. - **datetime**: A Python datetime used as the base time. - **millisecond**: Additional milliseconds (can be fractional). - **microsecond**: Additional microseconds (can be fractional). - **nanosecond**: Additional nanoseconds (can be fractional). - **picosecond**: Additional picoseconds (can be fractional). ##### `as_datetime() -> datetime` Convert to the nearest Python `datetime` value. The returned datetime is always timezone-aware and in UTC. Use standard Python datetime conversion functions to get local time. - **Returns**: A timezone-aware UTC datetime. #### `SaleaeTimeDelta` A high-precision duration. **Methods:** ##### `__init__(second: float = 0, *, millisecond: float = 0, microsecond: float = 0, nanosecond: float = 0, picosecond: float = 0) -> None` Construct a SaleaeTimeDelta from numerical values. All values must be convertible to `float`. Multiple units may be specified; the resulting duration is the sum of all values. - **second**: Seconds. - **millisecond**: Milliseconds. - **microsecond**: Microseconds. - **nanosecond**: Nanoseconds. - **picosecond**: Picoseconds. #### `SaleaeTimeInterval` A time interval between two points. **Attributes:** | Name | Type | Description | |------|------|-------------| | `begin` | `SaleaeTime` | The start of the interval. | | `end` | `SaleaeTime` | The end of the interval (exclusive). | **Methods:** ##### `__init__(begin: SaleaeTime, end: SaleaeTime) -> None` Create an interval between `begin` and `end`. - **begin**: The beginning of the interval. - **end**: The end of the interval. #### `AnalogSpan` A contiguous span of analog sample data. **Attributes:** | Name | Type | Description | |------|------|-------------| | `min` | `float` | The minimum voltage value in this span. | | `max` | `float` | The maximum voltage value in this span. | | `acquisition_info` | `AcquisitionInfo` | Information about the underlying data acquisition limits. | **Methods:** ##### `time_interval() -> SaleaeTimeInterval` Return the time interval covered by this span. ##### `sample_index_to_time(index: int) -> SaleaeTime` Convert a sample index to a `SaleaeTime`. - **index**: The sample index to convert. - **Returns**: The time corresponding to that sample. ##### `time_to_sample_index_floor(time: SaleaeTime) -> int` Convert a time to the nearest sample index, rounding down. Clamps to valid range: returns 0 if before the first sample, or the last index if after the last sample. - **time**: The time to convert. - **Returns**: The sample index at or before the given time. ##### `time_to_sample_index_ceil(time: SaleaeTime) -> int` Convert a time to the nearest sample index, rounding up. Clamps to valid range: returns 0 if before the first sample, or the last index if after the last sample. - **time**: The time to convert. - **Returns**: The sample index at or after the given time. ##### `find_gt(value: float, *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[int]` Find the first sample greater than `value`, searching forward. - **value**: The threshold voltage to compare against. - **start**: Start of search range (inclusive). Defaults to 0. - **end**: End of search range (exclusive). Defaults to end of data. - **Returns**: Sample index, or ``None`` if not found. ##### `find_ge(value: float, *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[int]` Find the first sample greater than or equal to `value`, searching forward. - **value**: The threshold voltage to compare against. - **start**: Start of search range (inclusive). Defaults to 0. - **end**: End of search range (exclusive). Defaults to end of data. - **Returns**: Sample index, or ``None`` if not found. ##### `find_lt(value: float, *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[int]` Find the first sample less than `value`, searching forward. - **value**: The threshold voltage to compare against. - **start**: Start of search range (inclusive). Defaults to 0. - **end**: End of search range (exclusive). Defaults to end of data. - **Returns**: Sample index, or ``None`` if not found. ##### `find_le(value: float, *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[int]` Find the first sample less than or equal to `value`, searching forward. - **value**: The threshold voltage to compare against. - **start**: Start of search range (inclusive). Defaults to 0. - **end**: End of search range (exclusive). Defaults to end of data. - **Returns**: Sample index, or ``None`` if not found. ##### `rfind_gt(value: float, *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[int]` Find the first sample greater than `value`, searching backward. - **value**: The threshold voltage to compare against. - **start**: Start of search range (inclusive). Defaults to 0. - **end**: End of search range (exclusive). Defaults to end of data. - **Returns**: Sample index, or ``None`` if not found. ##### `rfind_ge(value: float, *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[int]` Find the first sample greater than or equal to `value`, searching backward. - **value**: The threshold voltage to compare against. - **start**: Start of search range (inclusive). Defaults to 0. - **end**: End of search range (exclusive). Defaults to end of data. - **Returns**: Sample index, or ``None`` if not found. ##### `rfind_lt(value: float, *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[int]` Find the first sample less than `value`, searching backward. - **value**: The threshold voltage to compare against. - **start**: Start of search range (inclusive). Defaults to 0. - **end**: End of search range (exclusive). Defaults to end of data. - **Returns**: Sample index, or ``None`` if not found. ##### `rfind_le(value: float, *, start: Optional[int] = None, end: Optional[int] = None) -> Optional[int]` Find the first sample less than or equal to `value`, searching backward. - **value**: The threshold voltage to compare against. - **start**: Start of search range (inclusive). Defaults to 0. - **end**: End of search range (exclusive). Defaults to end of data. - **Returns**: Sample index, or ``None`` if not found. ##### `histogram() -> Tuple[np.ndarray, np.ndarray]` Generate a histogram of sample values. Returns a tuple of `(counts, bin_edges)` where `counts` has one entry per ADC code and `bin_edges` contains the voltage value at each bin edge. - **Returns**: A ``(counts, edges)`` tuple of numpy arrays. ##### `raw_chunks() -> Iterator[AnalogData]` Iterate over the underlying raw `AnalogData` chunks. This is targeted at power users who need direct access to the raw sample buffers. Because sample data is stored in separate buffers, the chunks may be non-uniform in size. - **Returns**: An iterator of ``AnalogData`` objects. #### `AcquisitionInfo` Information about the data acquisition hardware limits. **Attributes:** | Name | Type | Description | |------|------|-------------| | `min_adc_code` | `Optional[int]` | The minimum ADC code of the underlying data. | | `max_adc_code` | `Optional[int]` | The maximum ADC code of the underlying data. | **Methods:** ##### `__init__(min_adc_code: Optional[int], max_adc_code: Optional[int]) -> None` ### saleae.analyzers #### `StringSetting` A text input setting that accepts a string value. **Methods:** ##### `__init__(kwargs = {})` ##### `validate(value: str)` #### `NumberSetting` A numeric input setting that accepts an int or float value. **Attributes:** | Name | Type | Description | |------|------|-------------| | `min_value` | | | | `max_value` | | | **Methods:** ##### `__init__(*, min_value: int | float | None = None, max_value: int | float | None = None, kwargs = {})` - **min_value**: A minimum numeric value (optional). - **max_value**: A maximum numeric value (optional). ##### `validate(value: int | float)` #### `ChoicesSetting` A dropdown setting that accepts one value from a list of choices. **Attributes:** | Name | Type | Description | |------|------|-------------| | `choices` | | | **Methods:** ##### `__init__(choices: List[str], kwargs = {})` - **choices**: A list of choices for the user to choose from. ##### `validate(value)` #### `HighLevelAnalyzer` Base class for High Level Analyzer (HLA) extensions. #### `AnalyzerFrame` A single frame of analyzer output. **Attributes:** | Name | Type | Description | |------|------|-------------| | `type` | `str` | The frame type name. | | `start_time` | `SaleaeTime` | The start time of this frame. | | `end_time` | `SaleaeTime` | The end time of this frame. | | `data` | `dict | None` | Data dictionary containing the frame's decoded values. | **Methods:** ##### `__init__(type: str, start_time: SaleaeTime, end_time: SaleaeTime, data: dict | None = None)` ### saleae.measurements Measurement extension API. This is the current measurement API for building custom measurement extensions in Saleae Logic 2. #### `AnalogMeasurer` Base class for custom measurement extensions. **Methods:** ##### `measure_range(data: AnalogSpan)` This method is called when running a measurement over a span of analog data. This must be overridden by subclasses. - **data**: The span of data to run the measurement on. #### `HorizontalRule` An annotation value represented as a horizontal rule. **Attributes:** | Name | Type | Description | |------|------|-------------| | `value` | | | **Methods:** ##### `__init__(*, value: Union[int, float])` - **value**: The vertical value of the rule. #### `VerticalRule` An annotation value represented as a vertical rule, as an absolute time. **Attributes:** | Name | Type | Description | |------|------|-------------| | `time` | | | **Methods:** ##### `__init__(*, time: SaleaeTime)` - **value**: The time value of the rule. #### `Annotation` An annotation declaration for a measurement extension. **Attributes:** | Name | Type | Description | |------|------|-------------| | `measures` | | | **Methods:** ##### `__init__(*, measures: List[Measure])` - **measures**: The list of measures that this annotation depends on. If those measures are all disabled, this anotation will not be shown. #### `MeasureError` **Attributes:** | Name | Type | Description | |------|------|-------------| | `label` | | | | `description` | | | **Methods:** ##### `__init__(label: str, *, description: str = '')` #### `Measure` A measure declaration for a measurement extension. **Attributes:** | Name | Type | Description | |------|------|-------------| | `label` | | | | `type` | | | | `description` | | | | `units` | | | | `value` | `Optional[MeasureType]` | The value of this measure. | | `error` | `Optional[MeasureError]` | Mark this measure as having an error. | **Methods:** ##### `__init__(label: str, *, type: Type[MeasureType] = float, description: str = '', units: str = '') -> None` - **label**: Short label used to identify this measure. - **type**: Type of value that this measure will be assigned. - **description**: Description of the measure. - **units**: Unit of this measure. ##### `enabled() -> bool` True if this Measure has been enabled --- # Saleae Automation API > Automate Saleae Logic 2 captures, exports, and analysis. For Saleae Logic Analyzers only (not Logic MSO). ## Guides ## Getting Started ### Installing the Python Automation API package To get started, you will need the latest build of the Logic 2 Software (2.4.0+), the logic2-automation (1.0.0+) python package, and Python 3.8, 3.9, or 3.10. First, let's install the logic2-automation package: ```bash pip install logic2-automation ``` ### Launching Logic2 The automation interface can be enabled in the software UI. Open the preferences dialog from the main menu, and scroll to the bottom. [Image: Automation server UI] When the checkbox is checked, the automation server will start running in the Logic 2 software on the default port, 10430. ### Using the Python Automation API Next, let's run a simple example. You don't need to have a device connected for this. This example uses a demo device, if you would like to use a connected device follow the steps [here](#finding-the-serial-number-device-id-of-a-device) to find your device's serial number and replace the demo value in the example. Create a new python file called `saleae_example.py`, and paste in these contents: ```python from saleae import automation import os import os.path from datetime import datetime ## Connect to the running Logic 2 Application on port `10430`. ## Alternatively you can use automation.Manager.launch() to launch a new Logic 2 process - see ## the API documentation for more details. ## Using the `with` statement will automatically call manager.close() when exiting the scope. If you ## want to use `automation.Manager` outside of a `with` block, you will need to call `manager.close()` manually. with automation.Manager.connect(port=10430) as manager: # Configure the capturing device to record on digital channels 0, 1, 2, and 3, # with a sampling rate of 10 MSa/s, and a logic level of 3.3V. # The settings chosen here will depend on your device's capabilities and what # you can configure in the Logic 2 UI. device_configuration = automation.LogicDeviceConfiguration( enabled_digital_channels=[0, 1, 2, 3], digital_sample_rate=10_000_000, digital_threshold_volts=3.3, ) # Record 5 seconds of data before stopping the capture capture_configuration = automation.CaptureConfiguration( capture_mode=automation.TimedCaptureMode(duration_seconds=5.0) ) # Start a capture - the capture will be automatically closed when leaving the `with` block # Note: The serial number 'F4241' is for the Logic Pro 16 demo device. # To use a real device, you can: # 1. Omit the `device_id` argument. Logic 2 will choose the first real (non-simulated) device. # 2. Use the serial number for your device. See the "Finding the Serial Number # of a Device" section for information on finding your device's serial number. with manager.start_capture( device_id='F4241', device_configuration=device_configuration, capture_configuration=capture_configuration) as capture: # Wait until the capture has finished # This will take about 5 seconds because we are using a timed capture mode capture.wait() # Add an analyzer to the capture # Note: The simulator output is not actual SPI data spi_analyzer = capture.add_analyzer('SPI', label=f'Test Analyzer', settings={ 'MISO': 0, 'Clock': 1, 'Enable': 2, 'Bits per Transfer': '8 Bits per Transfer (Standard)' }) # Store output in a timestamped directory output_dir = os.path.join(os.getcwd(), f'output-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}') os.makedirs(output_dir) # Export analyzer data to a CSV file analyzer_export_filepath = os.path.join(output_dir, 'spi_export.csv') capture.export_data_table( filepath=analyzer_export_filepath, analyzers=[spi_analyzer] ) # Export raw digital data to a CSV file capture.export_raw_data_csv(directory=output_dir, digital_channels=[0, 1, 2, 3]) # Finally, save the capture to a file capture_filepath = os.path.join(output_dir, 'example_capture.sal') capture.save_capture(filepath=capture_filepath) ``` With the software is running, and the automation interface enabled (as shown above), run the script: ```bash python saleae_example.py ``` There you have it! Take a look at the documentation for Manager and Capture to see how the functionality all works! Also, for most automated applications, you won't want to start the software manually. See [this section](launching-logic2) for more information about different ways to launch the Logic software. ### Finding the Serial Number (Device Id) of a Device To find the serial number of a connected device, open capture info sidebar and click the device dropdown in the top right: [Image: Device info] If the device you want the serial number for is not selected, select it. Then click "Device Info" - this will open a popup with information about your device, including its serial number. [Image: Device serial number] You can copy the serial number from here and use it in your Python script where a "device_id" is required. ### Versioning The `saleae.proto` file contains a version (`major.minor.patch`). It can be found in the file header, and also in the `ThisApiVersion` enum. When generating language bindings, you can get the version of the .proto that was used through the protobuf `ThisApiVersion` enum - `THIS_API_VERSION_MAJOR`, `THIS_API_VERSION_MINOR`, and `THIS_API_VERSION_PATCH`. The version of the .proto file that the server is using can be retrieved using the `GetAppInfo` gRPC method, or the `Manager.get_app_info()` call in the Python API. * For a given major version, the API strives to be forward and backwards compatible. * The major version will change when: * There are any breaking changes * The minor version will change when: * New features are added * Additions are made to the existing API * The patch version will change when: * There are fixes to the API When implementing a client that uses the gRPC API directly, it is recommended to always retrieve the api version via GetAppInfo to validate that the major version is the same, and that the minor version is not older than the client. The Python API does this automatically on creation of the Manager object. ### Headless on Linux We do not currently support running Logic 2 in a headless mode, but it is possible to run Logic 2 in headless Linux environments using XVFB. The specifics for your environment may differ, but on Ubuntu 20.04 we have had success with the following setup. Install xvfb and other depdendencies: ```bash sudo apt install xvfb libatk1.0-0 libatk-bridge2.0-0 libgtk-3-0 libgbm1 ``` Run Logic 2: ```bash xvfb-run path/to/Logic-2.4.0.AppImage ``` ### Troubleshooting #### Failure during install due to `ModuleNotFoundError: No module named 'hatchling'` `logic2-automation` is packaged as a source distribution and built locally on install using `hatchling`. If you are building without isolation (for example, `pip install --no-build-isolation logic2-automation`) and you don't have `hatchling` installed, you will see this error. If you can't install with isolation, you can install `hatchling` (example: `pip install hatchling`) in your local environment to resolve the issue. #### Failure when importing saleae.automation / saleae.grpc If you see an error like this when you import from `saleae.automation`, it may be a protobuf/grpc version incompatibility. This can happen when you upgrade protobuf and/or grpc after installing `logic2-automation`. ```text TypeError: Descriptors cannot not be created directly. If this call came from a _pb2.py file, your generated code is out of date and must be regenerated with protoc >= 3.19.0. If you cannot immediately regenerate your protos, some other possible workarounds are: 1. Downgrade the protobuf package to 3.20.x or lower. 2. Set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python (but this will use pure-Python parsing and will be much slower). ``` You can regenerate the protobuf/grpc files by reinstalling `logic2-automation`: ```bash pip install --force-reinstall logic2-automation ``` #### Can't find a solution? Contact us at https://contact.saleae.com/hc/en-us/requests/new ## Launching the Logic 2 Software and Starting the Socket Interface The software can be launched with the automation server enabled with the following commands: ```bash ## windows cmd Logic.exe --automation ## Windows powershell .\Logic.exe --automation ## MacOS ./Logic2/Contents/MacOS/Logic --automation ## Linux ./Logic-2.4.0-master.AppImage --automation ## By default, the gRPC server port number is 10430. However, it can be set with --automationPort N ## Note: When using --automationPort, you will still need to pass --automation to enable the gRPC server. ## Example: Logic.exe --automation --automationPort 10500 ``` Note, the command line argument needs to be set in order for the automation interface to be enabled by default. Additionally, the automation interface can be manually enabled in the UI by following the instructions in the [Getting Started](getting-started) guide. ## Errors The Logic 2 Automation Interface will return errors in certain situations. These errors fall into two main categories: 1. Errors produced by misuse of the API. These errors will contained detailed information about exactly why the command failed, and in some cases, list out what parameter was invalid, as well as the valid options. Be sure to read each error message closely! If these errors occur in production, it may mean there are still bugs in your code. These should not be handled in code. 2. Occasional errors while recording due to USB connectivity problems. These are rare errors, but they can occur at random - like if another USB device consumes a spike of bandwidth, causing the logic analyzer stream buffer to overflow, stopping the capture. We recommend that the developer handle these errors, and restart the capture as appropriate. You will want to catch `CaptureError` errors raised by `start_capture`, `stop`, and `wait`. If an error occurs during the capture start process, then `start_capture` will raise an error. If an error occurs during the capture, for example if the software wasn’t able to receive data over USB fast enough, then the capture will end prematurely. However, the python client won’t be aware of this until stop or wait is called, at which point `CaptureError` will be raised. If any of these commands raise the `CaptureError` exception, we recommend simply starting a new capture. if `stop` or `wait` raise the error, be sure to dispose of the capture before starting the next one. - `SaleaeError` - `UnknownError` - `InternalServerError` - `InvalidRequestError` - `LoadCaptureFailedError` - `ExportError` - `MissingDeviceError` - `CaptureError` - `DeviceError` - `OutOfMemoryError` ## API Reference ### saleae.automation Automation API for controlling the Saleae Logic 2 software. #### `Version` **Attributes:** | Name | Type | Description | |------|------|-------------| | `major` | `int` | | | `minor` | `int` | | | `patch` | `int` | | **Methods:** ##### `__init__(major: int, minor: int, patch: int) -> None` #### `AppInfo` Logic 2 Application Information **Attributes:** | Name | Type | Description | |------|------|-------------| | `api_version` | `Version` | | | `app_version` | `str` | | | `app_pid` | `int` | | **Methods:** ##### `__init__(api_version: Version, app_version: str, app_pid: int) -> None` #### `DeviceType` **Attributes:** | Name | Type | Description | |------|------|-------------| | `LOGIC` | | | | `LOGIC_4` | | | | `LOGIC_8` | | | | `LOGIC_16` | | | | `LOGIC_PRO_8` | | | | `LOGIC_PRO_16` | | | #### `DeviceDesc` **Attributes:** | Name | Type | Description | |------|------|-------------| | `device_id` | `str` | | | `device_type` | `DeviceType` | | | `is_simulation` | `bool` | | **Methods:** ##### `__init__(device_id: str, device_type: DeviceType, is_simulation: bool) -> None` #### `GlitchFilterEntry` Represents the glitch filter specifications for a single digital channel **Attributes:** | Name | Type | Description | |------|------|-------------| | `channel_index` | `int` | | | `pulse_width_seconds` | `float` | | **Methods:** ##### `__init__(channel_index: int, pulse_width_seconds: float) -> None` #### `LogicDeviceConfiguration` Represents the capture configuration for one of the following devices: **Attributes:** | Name | Type | Description | |------|------|-------------| | `enabled_analog_channels` | `List[int]` | | | `enabled_digital_channels` | `List[int]` | | | `analog_sample_rate` | `Optional[int]` | | | `digital_sample_rate` | `Optional[int]` | | | `digital_threshold_volts` | `Optional[float]` | | | `glitch_filters` | `List[GlitchFilterEntry]` | | **Methods:** ##### `__init__(enabled_analog_channels: List[int] = list(), enabled_digital_channels: List[int] = list(), analog_sample_rate: Optional[int] = None, digital_sample_rate: Optional[int] = None, digital_threshold_volts: Optional[float] = None, glitch_filters: List[GlitchFilterEntry] = list()) -> None` #### `DigitalTriggerType` **Attributes:** | Name | Type | Description | |------|------|-------------| | `RISING` | | | | `FALLING` | | | | `PULSE_HIGH` | | | | `PULSE_LOW` | | | #### `DigitalTriggerLinkedChannelState` **Attributes:** | Name | Type | Description | |------|------|-------------| | `LOW` | | | | `HIGH` | | | #### `DigitalTriggerLinkedChannel` Represents a digital channel that must be either high or low while the trigger event (edge or pulse) is active **Attributes:** | Name | Type | Description | |------|------|-------------| | `channel_index` | `int` | | | `state` | `DigitalTriggerLinkedChannelState` | | **Methods:** ##### `__init__(channel_index: int, state: DigitalTriggerLinkedChannelState) -> None` #### `DigitalTriggerCaptureMode` This class represents the Digital Trigger Settings, when using start_capture with a digital trigger. Note: When using this mode, the wait() function will wait until the trigger is found and the post-trigger recording length is complete and the capture has ended. **Attributes:** | Name | Type | Description | |------|------|-------------| | `trigger_type` | `DigitalTriggerType` | | | `trigger_channel_index` | `int` | | | `min_pulse_width_seconds` | `Optional[float]` | | | `max_pulse_width_seconds` | `Optional[float]` | | | `linked_channels` | `List[DigitalTriggerLinkedChannel]` | | | `trim_data_seconds` | `Optional[float]` | | | `after_trigger_seconds` | `Optional[float]` | | **Methods:** ##### `__init__(trigger_type: DigitalTriggerType, trigger_channel_index: int, min_pulse_width_seconds: Optional[float] = None, max_pulse_width_seconds: Optional[float] = None, linked_channels: List[DigitalTriggerLinkedChannel] = list(), trim_data_seconds: Optional[float] = None, after_trigger_seconds: Optional[float] = None) -> None` #### `TimedCaptureMode` This class represents the capture settings when a simple timer mode is used. Note: when using this mode, the wait() function will wait until the specified duration is recorded and the capture has ended. **Attributes:** | Name | Type | Description | |------|------|-------------| | `duration_seconds` | `float` | | | `trim_data_seconds` | `Optional[float]` | | **Methods:** ##### `__init__(duration_seconds: float, trim_data_seconds: Optional[float] = None) -> None` #### `ManualCaptureMode` When this is used, a capture must be triggered/stopped manually. Note: use the stop() command to stop the capture. The wait() function will return an error. **Attributes:** | Name | Type | Description | |------|------|-------------| | `trim_data_seconds` | `Optional[float]` | | **Methods:** ##### `__init__(trim_data_seconds: Optional[float] = None) -> None` #### `CaptureConfiguration` The top-level capture configuration provided to the start_capture function. **Attributes:** | Name | Type | Description | |------|------|-------------| | `buffer_size_megabytes` | `Optional[int]` | | | `capture_mode` | `CaptureMode` | | The capture mode. This can be one of the following: | **Methods:** ##### `__init__(buffer_size_megabytes: Optional[int] = None, capture_mode: CaptureMode = ManualCaptureMode()) -> None` #### `Manager` Manager is the main class for interacting with the Logic 2 software. **Attributes:** | Name | Type | Description | |------|------|-------------| | `logic2_process` | | | | `channel` | | | | `stub` | `saleae_pb2_grpc.ManagerStub` | :meta private: | **Methods:** ##### `__init__(*, port: int, address: str = _DEFAULT_GRPC_ADDRESS, connect_timeout_seconds: Optional[float] = None, grpc_channel_arguments: Optional[List[Tuple[str, Any]]] = None, logic2_process: Optional[subprocess.Popen] = None)` It is recommended that you use Manager.launch() or Manager.connect() instead of using __init__ directly. Create an instance of the Manager class, and connect to the Logic 2 software. This library currently assumes the Logic 2 software is running on the same machine, and will attempt to connect to 127.0.0.1. In the future, we'll add support for supplying an IP address, as well as functions to help launch local copies of the application. Manager.close() is called. - **port**: Port number. By default, Logic 2 uses port 10430. - **address**: Address to connect to. - **connect_timeout_seconds**: Number of seconds to attempt to connect to gRPC server, after which an exception will be thrown. - **grpc_channel_arguments**: A set of arguments to pass through to gRPC. - **logic2_process**: Process object for Logic2 if launched from Python. The process will be shutdown automatically when ##### @classmethod `launch(application_path: Optional[Union[Path, str]] = None, connect_timeout_seconds: Optional[float] = None, grpc_channel_arguments: Optional[List[Tuple[str, Any]]] = None, port: Optional[int] = None) -> Manager` Launch the Logic2 application and shut it down when the returned Manager is closed. a locally installed copy of Logic2 will be searched for. - **application_path**: The path to the Logic2 binary to run. If not specified, - **connect_timeout_seconds**: See __init__ - **grpc_channel_arguments**: See __init__ - **port**: Port to use for the gRPC server. If not specified, 10430 will be used. ##### @classmethod `connect(*, address: str = _DEFAULT_GRPC_ADDRESS, port: int = _DEFAULT_GRPC_PORT, connect_timeout_seconds: Optional[float] = None, grpc_channel_arguments: Optional[List[Tuple[str, Any]]] = None) -> Manager` Connect to an existing instance of Logic 2. - **port**: Port number. By default, Logic 2 uses port 10430. - **address**: Address to connect to. - **connect_timeout_seconds**: See __init__ - **grpc_channel_arguments**: See __init__ ##### `get_app_info() -> AppInfo` Get information about the connected Logic 2 instance. - **Returns**: AppInfo object for the connected Logic 2 instance. ##### `close()` Close connection to Saleae backend, and shut it down if it was created by Manager. ##### `get_devices(*, include_simulation_devices: bool = False) -> List[DeviceDesc]` Returns a list of connected devices. Use this to find the device id of the attached devices. - **include_simulation_devices**: If True, the return value will also include simulation devices. This can be useful for testing without a physical device. ##### `start_capture(*, device_configuration: DeviceConfiguration, device_id: Optional[str] = None, capture_configuration: Optional[CaptureConfiguration] = None) -> Capture` Start a new capture All capture settings need to be provided. The existing software settings, like selected device or added analyzers, are ignored. Be sure to catch DeviceError exceptions raised by this function, and handle them accordingly. See the error section of the library documentation. - **device_configuration**: An instance of LogicDeviceConfiguration, complete with enabled channels, sample rates, and more. - **device_id**: The id of device to record with. - **capture_configuration**: The capture configuration, which selects the capture mode: timer, digital trigger, or manual., defaults to None, indicating manual mode. - **Returns**: Capture instance class. Be sure to call either wait() or stop() before trying to save, export, or close the capture. ##### `load_capture(filepath: str) -> Capture` Load a capture. The returned Capture object will be fully loaded (`wait_until_done` not required). Raises: InvalidFileError - **Returns**: Capture instance class. #### `RadixType` **Attributes:** | Name | Type | Description | |------|------|-------------| | `BINARY` | | | | `DECIMAL` | | | | `HEXADECIMAL` | | | | `ASCII` | | | #### `AnalyzerHandle` **Attributes:** | Name | Type | Description | |------|------|-------------| | `analyzer_id` | `int` | | **Methods:** ##### `__init__(analyzer_id: int) -> None` #### `DataTableExportConfiguration` **Attributes:** | Name | Type | Description | |------|------|-------------| | `analyzer` | `AnalyzerHandle` | | | `radix` | `RadixType` | | **Methods:** ##### `__init__(analyzer: AnalyzerHandle, radix: RadixType) -> None` #### `DataTableFilter` **Attributes:** | Name | Type | Description | |------|------|-------------| | `columns` | `List[str]` | | | `query` | `str` | | **Methods:** ##### `__init__(columns: List[str], query: str) -> None` #### `Capture` This class represents a single capture in the Logic 2 software. **Attributes:** | Name | Type | Description | |------|------|-------------| | `manager` | | | | `capture_id` | | | **Methods:** ##### `__init__(manager: saleae.automation.Manager, capture_id: int)` This class cannot be constructed by the user, and is only returned from the Manager class. ##### `add_analyzer(name: str, *, label: Optional[str] = None, settings: Optional[Dict[str, Union[str, int, float, bool]]] = None) -> AnalyzerHandle` Add an analyzer to the capture Note: analyzers already added to a loaded_capture cannot be accessed from the API at this time. - **name**: The name of the Analyzer, as shown in the Logic 2 application add analyzer list. This must match exactly. - **label**: The user editable display string for the analyzer. This will be shown in the analyzer data table export, defaults to None - **settings**: All settings for the analyzer. The keys and values here must exactly match the Analyzer settings as shown in the UI, defaults to None - **Returns**: Returns an AnalyzerHandle ##### `add_high_level_analyzer(extension_directory: str, name: str, *, input_analyzer: AnalyzerHandle, settings: Optional[Dict[str, Union[str, float]]] = None, label: Optional[str] = None) -> AnalyzerHandle` Add a high level analyzer to the capture. Note: high level analyzers already added to a loaded_capture cannot be accessed from the API at this time. - **extension_directory**: The directory of the extension that the HLA is in. - **name**: The name of the HLA, as specifiied in the extension.json of the extension. - **input_analyzer**: Handle to analyzer (added via add_analyzer) to use as input to this HLA. - **settings**: All settings for the analyzer. The keys and values here must match the HLA settings as shown in the HLA class. - **label**: The user editable display string for the high level analyzer. This will be shown in the analyzer data table export. - **Returns**: Returns an AnalyzerHandle ##### `remove_analyzer(analyzer: AnalyzerHandle)` Removes an analyzer from the capture. - **analyzer**: AnalyzerHandle returned by add_analyzer() ##### `remove_high_level_analyzer(high_level_analyzer: AnalyzerHandle)` Removes a high level analyzer from the capture. - **high_level_analyzer**: AnalyzerHandle returned by add_analyzer() ##### `save_capture(filepath: str)` Saves the capture to a .sal file, which can be loaded later either through the UI or with the load_capture() function. - **filepath**: path to the .sal file. Can be absolute, or relative to the Logic 2 software current working directory. ##### `legacy_export_analyzer(filepath: str, analyzer: AnalyzerHandle, radix: RadixType)` Exports the specified analyzer using the analyzer plugin export format, and not the data table format. Use the export_data_table() function to export analyzer results from the data table. - **filepath**: file name and path to export to. Should include the file name and extension, typically .csv or .txt. - **analyzer**: AnalyzerHandle returned from add_analyzer() - **radix**: Display Radix, from the RadixType enumeration. ##### `export_data_table(filepath: str, analyzers: List[Union[AnalyzerHandle, DataTableExportConfiguration]], *, columns: Optional[List[str]] = None, filter: Optional[DataTableFilter] = None, iso8601_timestamp: bool = False)` Exports the Analyzer Data Table We will be adding more options to this in the future, including the query string, specific columns, specific query columns, and more. - **filepath**: The specified output file, including extension, .csv. - **analyzers**: A list of AnalyzerHandles that should be included in the export, returned from add_analyzer() - **columns**: Columns to include in export. - **filter**: Filter to apply to the exported data. - **iso8601_timestamp**: Use this to output wall clock timestamps, instead of capture relative timestamps. Defaults to False. ##### `export_raw_data_csv(directory: str, *, analog_channels: Optional[List[int]] = None, digital_channels: Optional[List[int]] = None, analog_downsample_ratio: int = 1, iso8601_timestamp: bool = False)` Exports raw data to CSV file(s) This produces exactly the same format as used in the Logic 2 software when using the "Export Raw Data" dialog with the "CSV" option selected. Note, the directory parameter is a specific folder that must already exist, and should not include a filename. The export system will produce an analog.csv and/or digital.csv file(s) in that directory. All selected analog channels will be combined into the analog.csv file, and likewise for digital channels and digital.csv If no channels are specified, all channels will be exported. - **directory**: directory path (not including a filename) to where analog.csv and/or digital.csv will be saved. - **analog_channels**: list of analog channels to export, defaults to None - **digital_channels**: list of digital channels to export, defaults to None - **analog_downsample_ratio**: optional analog downsample ratio, useful to help reduce export file sizes where extra analog resolution isn't needed, defaults to 1 - **iso8601_timestamp**: Use this to output wall clock timestamps, instead of capture relative timestamps. Defaults to False. ##### `export_raw_data_binary(directory: str, *, analog_channels: Optional[List[int]] = None, digital_channels: Optional[List[int]] = None, analog_downsample_ratio: int = 1)` Exports raw data to binary files This produces exactly the same format as used in the Logic 2 software when using the "Export Raw Data" dialog with the "binary" option selected. Documentation for the format can be found here: https://support.saleae.com/faq/technical-faq/binary-export-format-logic-2 Note, the directory parameter is a specific folder that must already exist, and should not include a filename. The export system will produce one .bin file for each channel exported. If no channels are specified, all channels will be exported. - **directory**: directory path (not including a filename) to where .bin files will be saved - **analog_channels**: list of analog channels to export, defaults to None - **digital_channels**: list of digital channels to export, defaults to None - **analog_downsample_ratio**: optional analog downsample ratio, useful to help reduce export file sizes where extra analog resolution isn't needed, defaults to 1 ##### `close()` Closes the capture. Once called, do not use this instance. ##### `stop()` Stops the capture. Can be used with any capture mode, but this is recommended for use with ManualCaptureMode. stop() and wait() should never both be used for a single capture. Do not call stop() more than once. stop() should never be called for loaded captures. If an error occurred during the capture (for example, a USB read timeout, or an out of memory error) that error will be raised by this function. Be sure to catch DeviceError exceptions raised by this function, and handle them accordingly. See the error section of the library documentation. ##### `wait()` Waits for the capture to complete. This should only be used with TimedCaptureMode or DigitalTriggerCaptureMode. for TimedCaptureMode, this will wait for the capture duration to complete. For DigitalTriggerCaptureMode, this will wait for the digital trigger to be found and the capture to complete. stop() and wait() should never both be used for a single capture. Do not call wait() more than once. wait() should never be called for loaded captures. Be sure to catch DeviceError exceptions raised by this function, and handle them accordingly. See the error section of the library documentation. ### saleae.automation.errors Error types raised by the Saleae Automation API. #### `SaleaeError` The base class for all Saleae exceptions. Do not catch this directly. #### `UnknownError` This indicates that the error message from the Logic 2 software was not understood by the python library. This could indicate a version mismatch. #### `Logic2AlreadyRunningError` This indicates that there was an instance of Logic 2 already running. #### `IncompatibleApiVersionError` This indicates that the server is running an incompatible API version. This only happens on major version changes. It is recommended to upgrade to the latest release of Logic 2 and the Python API #### `InternalServerError` An unexpected error occurred in the Logic 2 software. Please submit these errors to Saleae support. #### `InvalidRequestError` The socket request was invalid. See exception message for details. #### `LoadCaptureFailedError` Error loading a saved capture. This could indicate that the file does not exist, or the file was saved with a newer version of the Logic 2 software, or that the file was not a valid saved file. Try manually loading the file in the Logic 2 software for more information. #### `ExportError` An error occurred either while exporting raw data, a single protocol analyzer, or the protocol analyzer data table. Check the exception message for more details. #### `MissingDeviceError` The device ID supplied to the start_capture function is not currently attached to the system, or it has not been detected by the software. For general support for device not detected errors, see this support article: https://support.saleae.com/troubleshooting/logic-not-detected #### `CaptureError` The base class for all capture related errors. We recommend all automation applications handle this exception type. Capture failures occur rarely, and for a handful of reasons. We recommend logging the exception message for later troubleshooting. We do not recommend attempting to access or save captures that end in an error. Instead, we recommend starting a new capture. Check the exception message for details. #### `DeviceError` This error represents any device related error while capturing. USB communication or bandwidth errors, missing calibration, device disconnection while recording, and more. Check the exception message for details. #### `OutOfMemoryError` This exception indicates that the capture was automatically terminated because the capture buffer was filled. --- # Saleae MSO API > Control Saleae Logic MSO mixed-signal oscilloscope devices. For Logic MSO hardware only (not Logic Analyzers). ## Guides ## Getting Started This guide will help you get started with the Saleae MSO API. ### Prerequisites Before starting, please follow the [Installation](installation) guide to set up your environment with the necessary requirements and dependencies. We recommend opening up Logic2 and testing that your Logic MSO can perform basic functions on your machine before diving into automation. ### Basic Usage Here's a simple example of how to capture an analog signal: ```python from pathlib import Path import numpy as np from saleae import mso_api mso = mso_api.MSO() capture_config = mso_api.CaptureConfig( enabled_channels=[mso_api.AnalogChannel(channel=0, name="clock")], analog_settings=mso_api.AnalogSettings(sample_rate=100e6), capture_settings=mso_api.TimedCapture(capture_length_seconds=0.1), ) for n in range(3): save_dir = Path('my-captures') / f'{n:02d}' capture = mso.capture(capture_config, save_dir=save_dir) avg_voltage = np.mean(capture.analog_data["clock"].voltages) print(f"Capture {n:02d} in {save_dir} avg voltage: {avg_voltage:.3f} V") ``` As you can see there's 4 basic things happening in this example: - Initializing the `MSO` object - Configuring the capture by building a `CaptureConfig` object - Triggering one or more captures with that configuration - Processing the data from those captures (however you want) Let's dive into each one a little bit below. ### Initializing the `MSO` object The `MSO` object is your main interface to the Logic MSO hardware. You can initialize it with no arguments to connect to the first available device, or provide a serial number to connect to a specific device. ### Configuring a capture The `CaptureConfig` object controls all aspects of how a capture will be performed - which channels to capture from, sample rates, trigger conditions, and capture duration. You can configure analog channels, digital channels, or both for mixed-signal captures. Not all sample rates will be supported by all devices, so the API will make the capture at the closest supported sampling rate to the one in the configuration. Each capture object is returned with a configuration object which accurately represents the configuration the device used to make the capture, including the exact sample rate. ### Triggering a capture with that configuration Once you have an `MSO` object and a `CaptureConfig`, you can trigger a capture using the `Mso.capture` method. Capture files are saved to disk directly by the underlying MSO communications binary, and can be absolutely enormous depending on their length and sample rate. Top line Logic MSO devices can take >6GB single captures (2 bytes per sample with a tiny header in each file). The method returns a `Capture` object that provides access to the captured data. `Capture` objects use `numpy.memmap()` as their underlying storage mechanism so huge captures don't necessarily have to be stored in memory for certain operations to work on them. `Capture` objects are also persisted on disk by default, and are really easy to re-create quickly from their folder locations. ### Processing the data Here's where the fun begins! If you're using huge capture files, be mindful of what operations might cause them to be loaded into memory vs. operated on in-place. If you are only storing computation results from your captures, use `capture.delete()` to delete the underlying data files, or just `rm` the entire directory after calling `del` on your capture. If you have an interesting use case, we'd love to hear about it - send us a message on the support site! ### Next Steps - See the [Installation](installation) page for detailed setup instructions - Check out the [Usage](usage) guide for more detailed examples ## Installation This guide will help you install the Saleae MSO API. ### Requirements - Python 3.11 or higher - Logic 2.4.20 or newer installed (for the Logic MSO drivers, or udev rules) - A supported operating system, either: - Windows 10, or 11 (x64), or - Ubuntu 20.04+ (64-bit) - Mac OSX 10.14 (Mohave) or newer - Enough disk space for your capture files The remaining package requirement will be installed automatically by `uv` (or `pip`). We recommend installing a recent version of Logic2 on your system and checking that it can connect with and get data from your MSO before installing this library. We hope there will be no issues installing this library and getting your Saleae products operating automatically, but if there are issues then that step will make debugging them a much smoother and more targeted process. ### Installation Methods #### Saleae Python Package Index This package is distributed via a private python package index hosted by Saleae. You can install it with `uv` (recommended, see the [uv installation instructions](https://docs.astral.sh/uv/getting-started/installation/) for information), `pip`, `poetry` or any other python package manager. Saleae recommends using both `uv` and a virtual environment. You will need to provide the URL for our index when you install: ```bash uv add saleae-mso-api --index https://downloads.saleae.com/pypi --index-strategy unsafe-best-match ## or pip install saleae-mso-api --extra-index-url https://downloads.saleae.com/pypi/ ``` #### Linux udev Rules If you have not installed Logic2 before on your Linux system, you will need to install the `udev` rules into the proper place. After installing the package with the above instructions, run: ```bash ## For standard use (will prompt for password when it calls 'sudo') uv run python -m saleae.mso_api.utils.install_udev ## On pip with virtualenv source .venv/bin/activate python -m saleae.mso_api.utils.install_udev ``` This will put the rules into `/etc/udev/rules.d` and reload them with `udevadm`. You will see a new rules file called `99-SaleaeLogic.rules` that contains the following: ``` SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="0925", ATTR{idProduct}=="3881", MODE="0666" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="21a9", ATTR{idProduct}=="1001", MODE="0666" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="21a9", ATTR{idProduct}=="1003", MODE="0666" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="21a9", ATTR{idProduct}=="1004", MODE="0666" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="21a9", ATTR{idProduct}=="1005", MODE="0666" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="21a9", ATTR{idProduct}=="1006", MODE="0666" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="21a9", ATTR{idProduct}=="1007", MODE="0666" ``` ### Troubleshooting #### Common Issues - **ImportError: No module named 'saleae'**: Make sure you've installed the package correctly. - **ImportError: cannot import name 'X' from 'saleae'**: You might be using an older version of the package. Try updating. #### Getting Help If you encounter any issues during installation, please contact us at . ## Usage Guide This guide provides examples of how to use the Saleae MSO API for common tasks. For basic setup and your first capture, see the [Getting Started](getting_started) guide. ### Initiating a mixed-signal capture ```python from pathlib import Path from saleae import mso_api mso = mso_api.MSO() capture_config = mso_api.CaptureConfig( enabled_channels=[ mso_api.AnalogChannel(channel=0, name="clock"), mso_api.DigitalChannel(port=0, channel=0, name="MOSI", threshold_volts=1.6), ], analog_settings=mso_api.AnalogSettings(sample_rate=100e6), capture_settings=mso_api.TimedCapture(capture_length_seconds=0.1), ) save_dir = Path('my-capture') capture = mso.capture(capture_config, save_dir) ``` ### Loading a previously saved (or exported) capture The Logic2 software can export analog waveforms in a binary format using `File > Export Data` (not save!) To load a capture from a directory containing those binary export files, use the following: ```python from pathlib import Path from saleae import mso_api ## Specify the directory containing the capture files cap_dir = Path("../tests/data/capture1") ## Load the capture cap = mso_api.Capture.from_dir(cap_dir) ## Print information about the capture print(f"Channels: {cap.analog_channel_names}") print(f"Sample rate: {cap.analog_sample_rate} Hz") print(f"Duration: {cap.analog_stop_time - cap.analog_start_time:.6f} seconds") ``` ### Working with Triggers You can define triggers to identify specific events in your captures: ```python from saleae.mso_api import EdgeTrigger, EdgeTriggerDirection ## Create a trigger for rising edges on channel 1 (channel_index 0) at 0V trigger = EdgeTrigger( channel_index=0, threshold_volts=0, direction=EdgeTriggerDirection.RISING ) ## Create a trigger for falling edges on the "clock" channel at 1.65V with a 1ms holdoff trigger_with_holdoff = EdgeTrigger( channel_name="clock", threshold_volts=1.65, direction=EdgeTriggerDirection.FALLING, holdoff_seconds=0.001 ) ``` ### Applying Synthetic Triggers If you have a long capture that you need to split into multiples, use `split_capture()` ```python from pathlib import Path from saleae import mso_api from saleae.mso_api.synthetic_trigger import split_capture ## Load a capture cap_dir = Path("../tests/data/capture1") cap = mso_api.Capture.from_dir(cap_dir) ## Set up a new trigger to split on trigger = mso_api.EdgeTrigger( channel_index=0, threshold_volts=0, direction=mso_api.EdgeTriggerDirection.RISING ) ## Split the capture caps = split_capture(cap, trigger, pre_trigger_seconds=-6e-6, post_trigger_seconds=56e-6) print(f"Number of triggered segments: {len(caps)}") ## Output: Number of triggered segments: 13810 ``` ### Processing Triggered Segments Once you have split a capture into triggered segments, you can process each segment: ```python ## Process each triggered segment for i, segment in enumerate(caps[:5]): # Process just the first 5 segments as an example print(f"Segment {i}:") print(f" Duration: {segment.analog_stop_time - segment.analog_start_time:.9f} seconds") print(f" Samples: {segment.num_analog_samples}") # You can perform analysis on each segment for ch_name, ch_data in segment.analog_data.items(): print(f" {ch_name} min: {ch_data.voltages.min():.3f}V, max: {ch_data.voltages.max():.3f}V") ``` ## Release Notes ### v0.5.5 - February 25, 2026 - Update Logic MSO support to match Logic 2.4.42, which includes a new firmware version. ### v0.5.4 - December 12, 2025 - Fixed a crash when loading a binary capture with more than one trigger saved ### v0.5.3 - October 22, 2025 - Fixed executable permissions for macOS binaries (both x64 and arm64) - Binary files now automatically get execute permissions set on first use on macOS ### v0.5.2 - October 2025 - Added macOS support (x64 and arm64 architectures) ### v0.5.1 - September 16, 2025 - Added support for Rev0F analog front-end boards (missing from v0.5.0) ### v0.5.0 - July 2, 2025 - Added support for taking digital captures and mixed-signal captures - Added a debug plot capability ### v0.4.0 - May 23, 2025 - Added support for Linux ### v0.3.1 - May 13, 2025 - First public release - Support timed and triggered analog captures ## Contributing Thank you for your interest in contributing to the Saleae MSO API! This guide will help you get started with contributing to the project. ### Prerequisites Install the `uv` package manager. Their [installation instructions are here](https://docs.astral.sh/uv/getting-started/installation/), but the quick version may be to just do: ``` powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` ### Development Setup 1. Clone the repository: ```bash git clone git@github.com:saleae/mso-api.git cd mso-api ``` 2. Create a virtual environment using `uv` and activate it: ```bash uv venv source .venv/bin/activate # On Windows: .venv\Scripts\activate ``` 3. Install the package in development mode with the development (`dev`) dependencies into your virtualenv: ```bash uv sync --all-extras ``` 4. Install VISA drivers on your machine to run instrument-dependent tests These can be downloaded from [National Instruments here](https://www.ni.com/en/support/downloads/drivers/download.ni-visa.html). 5. Run the test suite - Plug in a Siglent function generator to your machine over USB - Plug the MSO into your machine - Supply power to the MSO's power USB-C port - Using a BNC to SMB cable (or a BNC cable with the MSO's included BNC-to-SMB adapter), plug both channels of the signal generator directly into the MSO's first two channels (CH1 to CH1, CH2 to CH2) - Run the test suite ``` uv run pytest tests ``` Or ``` .venv\Scripts\activate # VSCode wil typically do this automatically with the Python extension installed pytest tests ``` ### Code Style We use [ruff](https://github.com/charliermarsh/ruff) for code linting and formatting. To auto-format your code (do before each commit): ```bash ruff format ``` To lint: ```bash ruff check . ``` ### Testing We use pytest for testing. To run the tests: ```bash pytest tests ``` ### Documentation This project uses Jupyter Book for documentation. The documentation includes: - Getting started guide - Installation instructions - Usage examples - API reference - Contributing guidelines Building the docs cleanly currently requires an MSO attached to your system, as many of the code examples in the docs run live on hardware to get capture objects. They will build without an MSO, but the examples that do both run and depend on hardware will have tracebacks printed right after them. To build the documentation: 1. Install the development dependencies: ```bash uv sync --all-extras ## first time installation of the PDF generator requires this playwright install ``` 2. Build the HTML documentation: ```bash python build_docs.py --clean ``` This will generate HTML documentation in the `docs/_build/html` directory. 3. To also build a PDF version: ```bash python build_docs.py --pdf ``` 4. The documentation will be available at: - HTML: `docs/_build/html/index.html` - PDF: `docs/_build/pdf/book.pdf` (if built with `--pdf`) ### Pull Request Process 1. Fork the repository 2. Create a new branch for your feature or bugfix 3. Make your changes 4. Run the tests to ensure they pass 5. Update the documentation if necessary 6. Submit a pull request ### Reporting Issues If you find a bug or have a feature request, please open an issue on the GitHub repository. Please include: - A clear and descriptive title - A detailed description of the issue or feature request - Steps to reproduce the issue (if applicable) - Expected behavior - Actual behavior - Any relevant logs or screenshots ### Code of Conduct Please be respectful and considerate of others when contributing to this project. We strive to maintain a welcoming and inclusive environment for all contributors. ## Saleae MSO API Documentation Welcome to the documentation for the Saleae Mixed Signal Oscilloscope (MSO) API. ```{image} saleae_logo.jpg :alt: Saleae Logo :width: 150px :align: center ``` ### Overview This API provides programmatic access to Saleae Mixed Signal Oscilloscopes, allowing you to: - Configure and run analog, digital, or mixed-signal captures - Ingest the resulting binary data files into memory-mapped Numpy arrays, and - Some other things, like simple plotting of those captures The MSO API runs on Windows, Linux, and Mac computers, see for the exact versions of each supported. ### Installation See for detailed installation instructions. ### Quick Example The following demonstrates how to configure and execute a series of simple timed captures with the `saleae-mso-api` and a Logic MSO. ```{code-cell} python from pathlib import Path import numpy as np from saleae import mso_api mso = mso_api.MSO() capture_config = mso_api.CaptureConfig( enabled_channels=[mso_api.AnalogChannel(channel=0, name="clock")], analog_settings=mso_api.AnalogSettings(sample_rate=100e6), capture_settings=mso_api.TimedCapture(capture_length_seconds=0.1), ) for n in range(3): save_dir = Path('my-captures') / f'{n:02d}' capture = mso.capture(capture_config, save_dir=save_dir) avg_voltage = np.mean(capture.analog_data["clock"].voltages) print(f"Capture {n:02d} in {save_dir} avg voltage: {avg_voltage:.3f} V") ``` Learn more about how this example is structured by reading the following: - for configuring captures - for running them on an attached Logic MSO :::{warning} This is package is currently under heavy development, some significant features are missing and others may be evolving quickly. Things may change! ::: ### More * {ref}`genindex` * {ref}`modindex` * {ref}`search` ## Binary Files The `binary_files` module provides utilities for reading and parsing Verion 0 and Version 1 Saleae Logic2 binary export files. The Version 1 format was released in June 2025. See our [support website](https://support.saleae.com/faq/technical-faq/binary-and-csv-export-formats-2025-update) for information on the new format. ### Overview This module handles the low-level details of reading and parsing the binary file formats used by Saleae Logic devices. It's primarily used internally by the `Capture` class, but can also be used directly for advanced use cases. See the [Capture API Reference](capture.md) for more information. It defaults to using `np.memmap()` to access (what can be) very large capture files. This allows loading into system RAM to be done as late as practical. ### Functions #### `read_file` ```python def read_file(file_path: Path) -> SaleaeFile ``` Reads and parses a binary file exported from Saleae Logic. **Parameters:** - `file_path`: Path to the binary file **Returns:** - A `SaleaeFile` object containing the parsed contents ### Classes #### `SaleaeFile` Contains the parsed contents of a binary file. **Attributes:** - `version`: the file format version, either 0 or 1 - `type`: our internal Enum (e.g. Filetype_DigitalExport, _AnalogExport, _WaveformAdcExport, etc.) - `contents`: The parsed contents of the file (e.g., AnalogExport_V0, DigitalExport_V1, etc.) ### Note This module is primarily used internally by the `Capture` class. Most users should use the `Capture` class instead of working with this module directly. The module is exposed for advanced use cases where direct access to the binary file format is needed. ### Error Handling The module will raise appropriate errors if: - The file cannot be found - The file format is invalid - The file is not an analog export - The file is corrupted or incomplete ## Capture The `capture` module provides the core `Capture` class for loading, manipulating, and analyzing capture data from Saleae Logic devices. ### Overview The `Capture` class represents an individual capture from a Logic MSO device. It provides methods to: - Load capture data from binary files using `np.memmap` to avoid reading the whole capture into memory - Access channel data and metadata - Query information about the capture (channels, time range, etc.) ### Classes #### `Capture` ```python @dataclass class Capture: # the data in the capture, mappings from channel name to data analog_data: Dict[str, BaseAnalogData] digital_data: Dict[str, BaseDigitalData] # the config used to capture the data config: CaptureConfig ``` **Properties:** - `analog_start_time`: The start time of the capture in MSO-relative seconds - `analog_sample_rate`: The sample rate that analog traces were taken at in Hz - `analog_channel_names`: Returns a list of analog channel names in the capture - `analog_time_series`: Returns the time array for analog data - `analog_stop_time`: Returns the end time of the capture in seconds - `num_analog_samples`: Returns the number of analog samples in the capture **Class Methods:** - `from_dir`: Create a Capture from a directory containing .bin files ```python @classmethod def from_dir(cls, dir_path: Path) -> "Capture" ``` Call this method on previously saved capture directories to re-create the exact `Capture` object that existed when the capture was first returned, including the `CaptureConfig` object used to generate the capture in the first place. - `from_config`: Create a Capture from a CaptureConfig and a save path ```python @classmethod def from_config(cls, config: CaptureConfig, dir_path: Path) -> "Capture" ``` Call this when your previously saved capture doesn't have any .json files detailing the capture config in it. This may be the case if your capture came from a Logic2 export. **Methods:** - `delete`: Delete the capture files from disk Deletes all capture data files from disk. The capture must have been previously stored to disk (i.e., created via `from_dir` or `from_config`). Raises `RuntimeError` if the capture was not stored to disk. This method will: 1. Clear the analog and digital data arrays from memory 2. Run garbage collection to free memory, and 3. Delete all .bin files from the capture directory on disk ### Working with Capture Data The `analog_data` and `digital_data` attributes are dictionaries mapping channel names to `AnalogData` and `DigitalData` objects respectively. Channels must have unique names which will be generated for you if not supplied. #### Analog Data You can access continuous analog data using standard NumPy indexing. ```python ## Get voltage data for channel 1 (channel_index 0) ch1_volts = cap.analog_data["CH0_volts"].voltages ## Get a specific sample sample_index = 1000 voltage_at_sample = cap.analog_data["CH0_volts"].voltages[sample_index] ``` #### Digital Data Digital data is stored as an initial state and a list of transition times, which are relative to the trigger position (or the start of the capture). There are a couple of helper methods that we use internally to validate data integrity that we've exposed for your use: ```python def state_at_idx(self, idx: int) -> bool: """ Returns the state of the digital channel just before the given transition_time index """ ``` This one in particular may come in handy: ```python def state_at_time(self, time: float) -> bool: """ Returns the state of the digital channel at the given time """ ``` ### Re-Loading Previously Saved Captures The `Capture` class provides a method for creating a `Capture` instance exactly as it was when first captured. Call the following to re-load both the configuration and data files: ```{code-cell} python from pathlib import Path from saleae.mso_api import Capture capture = Capture.from_dir(Path("../my-capture")) print(f"Re-loaded capture with channels: {capture.analog_channel_names}") ``` ## Capture Configuration The `capture_config` module provides classes for configuring capture operations on the Saleae Logic MSO device. A `CaptureConfig` object must be built and passed to `MSO.capture()` to initiate an actual capture on the hardware. There are a lot of different knobs that control a capture, and the class `CaptureConfig` handles most of the validation of those various settings. When a capture happens, sometimes values on the `CaptureConfig` object change slightly, i.e. to reflect the actual sample rate that was used during the capture (not all sample rates are supported, some interpolation happens, especially at high sample rates). A `Capture` object always contains the `CaptureConfig` objects that best represents the capture settings along with it. Not all settings need to be set explicitly to get started, many will get sensible defaults (or device defaults) when a capture is taken. Note: capture durations are not exact. The Logic MSO will always give you at least the requested number of samples, but may give up to 255 more samples than requested due to it's truncation logic. ### Overview This module defines the configuration objects that control: - Which channels to capture (analog and digital) - Sample rates and other analog settings - Capture duration and trigger settings ### Classes #### `CaptureConfig` ```python @dataclass class CaptureConfig: enabled_channels: list[ChannelConfig] analog_settings: Optional[AnalogSettings] = None capture_settings: Optional[CaptureSettings] = None ``` The main configuration object for captures. A capture must have at least one enabled channel, and should include either `analog_settings` or `capture_settings` if a capture other than the defaults below is desired. **Parameters:** - `enabled_channels`: List of analog and digital channels to capture - `analog_settings`: Optional analog capture settings (defaults to device maximum sample rate and DROP mode) - `capture_settings`: Optional capture settings (defaults to 1ms timed capture) **Methods:** You likely won't need to use these, they are used behind the scenes to persist `CaptureConfig` objects in human-readable JSON in capture directories. - `to_dict`: Converts configuration to device format that the capture binary understands ```python def to_dict(self, output_dir: Path, mso_part_number: MsoPodPartNumber) -> Dict[str, Any]: ``` - `to_dir`: Writes a full configuration to several files in a given directory ```python def to_dir(self, output_dir: Path, mso_part_number: MsoPodPartNumber) -> Path: ``` - `from_dir`: Reads a configuration from a given directory ```python @classmethod def from_dir(cls, dir_path: Path) -> "CaptureConfig": ``` #### `AnalogChannel` ```python @dataclass class AnalogChannel: channel: int name: Optional[str] = None center_voltage: float = 0.0 voltage_range: float = 10.0 probe_attenuation: ProbeAttenuation = ProbeAttenuation.PROBE_10X # Default to the 10x probe included with the MSO coupling: Coupling = Coupling.DC bandwidth_mhz: BandwidthLimit = BandwidthLimit.MHz_350 ``` Represents an analog channel configuration. **Parameters:** - `channel`: The channel number (0-based index) - `name`: Optional descriptive name for the channel (defaults to "CH_volts") - `center_voltage`: Center voltage for the channel (default: 0.0V) - `voltage_range`: Voltage range for the channel (default: 10.0V) - `probe_attenuation`: Probe attenuation setting (default: 1x) - `coupling`: Coupling type (default: DC) - `bandwidth_mhz`: Bandwidth limit (default: 350MHz) #### `DigitalChannel` ```python @dataclass class DigitalChannel(ChannelConfig): channel: int name: Optional[str] = None port: Optional[int] = None threshold_volts: Optional[float] = None minimum_pulse_width_samples: Optional[int] = None ``` Represents a digital channel configuration. **Parameters:** - `channel`: The channel number (0-based index) - `name`: Optional descriptive name for the channel (defaults to "P_CH_logic") - `port`: The port number for the digital channel (required) - `threshold_volts`: Voltage threshold for digital signal detection (default: None, uses global threshold) - `minimum_pulse_width_samples`: Minimum pulse width in samples to filter out noise (default: None) **Note:** Digital channels require a port number to be specified. All digital channels on the same port share the same voltage threshold, so setting `threshold_volts` on one channel will apply to all channels on that port. #### `AnalogSettings` ```python @dataclass class AnalogSettings: downsample_mode: DownsampleMode = DownsampleMode.AVERAGE sample_rate: float = 1_600_000_000 # Defaults to 1.6 GSPS ``` Configures analog capture settings. **Parameters:** - `downsample_mode`: How to handle downsampling (default: AVERAGE) - `sample_rate`: The sample rate in Hz (default: 1.6GS/s) #### `TimedCapture` ```python @dataclass class TimedCapture(CaptureSettings): capture_length_seconds: float ``` Configures a capture based on duration. **Parameters:** - `capture_length_seconds`: How long to capture in seconds #### `AnalogTriggered` ```python @dataclass class AnalogTriggered(CaptureSettings): trigger: EdgeTrigger pre_trigger_seconds: float = 0.0005 post_trigger_seconds: float = 0.0005 timeout_seconds: float = 1.0 ``` Configures a capture triggered by an analog signal reaching a specific threshold. See the [trigger documentation](trigger.md) for more information on configuring a trigger. **Parameters:** - `trigger`: The trigger settings - `pre_trigger_seconds`: Time before trigger to capture (default: 0.5ms) - `post_trigger_seconds`: Time after trigger to capture (default: 0.5ms) - `timeout_seconds`: Maximum time to wait for trigger before throwing a timeout error (default: 1.0s) ### Enums #### `ProbeAttenuation` If you are using the probes included with your Logic MSO, leave this attenuation setting at `Probe10x` which is the default. Those probes attenuate the signal by 10x inherently. If you are connecting a signal directly to the input of your Logic MSO with e.g. the BNC adapter and coaxial cable, then use the `Probe1x` setting to inform the MSO it will be receiving more signal per ADC count. ```python class ProbeAttenuation(Enum): PROBE_10X = "Probe10x" # default, use this when using the probes included with MSO PROBE_1X = "Probe1x" ``` #### `DownsampleMode` ```python class DownsampleMode(Enum): DROP = "Drop" AVERAGE = "Average" ``` ### Example Capture Configurations ```{code-cell} python from pathlib import Path from saleae import mso_api from saleae.mso_api.part_number import MsoPodPartNumber as PN ## Capture 100ms of data in a timed capture on 2 analog channels and 4 digital ones config1 = mso_api.CaptureConfig( enabled_channels=[ mso_api.AnalogChannel(channel=0, name="clock", center_voltage=2.0, voltage_range=5.0), mso_api.AnalogChannel(channel=1, name="data", center_voltage=2.0, voltage_range=5.0), mso_api.DigitalChannel(port=0, channel=0, name="MISO", threshold_volts=1.5), mso_api.DigitalChannel(port=0, channel=1, name="MOSI"), mso_api.DigitalChannel(port=0, channel=2, name="CLK"), mso_api.DigitalChannel(port=0, channel=3, name="CS") ], analog_settings=mso_api.AnalogSettings( downsample_mode=mso_api.DownsampleMode.AVERAGE, sample_rate=100e6 ), capture_settings=mso_api.TimedCapture(capture_length_seconds=0.1) ) ## Trigger a capture on the rising edge of an analog channel at full sample rate (default) config2 = mso_api.CaptureConfig( enabled_channels=[ # signals directly connected via coax must be set to 1x attenuation mode mso_api.AnalogChannel( channel=0, name="clock", probe_attenuation=mso_api.ProbeAttenuation.PROBE_1X ), mso_api.AnalogChannel( channel=1, name="signal", probe_attenuation=mso_api.ProbeAttenuation.PROBE_1X ), ], capture_settings=mso_api.AnalogTriggered( # specify by channel_name, or channel_index, both work trigger=mso_api.EdgeTrigger( channel_name="clock", threshold_volts=1.6, direction=mso_api.EdgeTriggerDirection.RISING ), # capture 1/2 millisecond of data on either side of the trigger pre_trigger_seconds=0.5e-3, post_trigger_seconds=0.5e-3, ) ) ## running config.to_dict() or mso.capture(config, ...) will throw ## errors if the configuration is invalid for n, config in enumerate([config1, config2], start=1): _ = config.to_dict(Path("my-capture-save-dir"), PN.MsoPod_4ch_200Mhz_12b_1000Ms) print(f"configuration #{n} is valid") ``` ## API Reference This section provides more details on each of the Saleae MSO API modules. ### Modules The API consists of the following main modules: - [MSO](mso.md): Manages interaction with the Logic MSO device - [Capture Configuration](capture_config.md): The configuration objects to control capturing from the device - [Capture](capture.md): The main object returned by a capture operation - [Trigger](trigger.md): Classes for defining trigger conditions - [Plot](plot.md): Simple debug plotting of captures - [Synthetic Trigger](synthetic_trigger.md): Tools for applying triggers to existing captures - [Binary Files](binary_files.md): Utilities for working with binary data files ## MSO The `MSO` class is the main interface for interacting with a Saleae Logic MSO device. It provides methods for capturing data and managing device settings. ### Overview The `MSO` class handles: - Device connection and initialization - Analog capture operations - Device configuration and settings ### Classes #### `MSO` ```python class MSO: def __init__(self, serial_number: Optional[str] = None) ``` **Parameters:** - `serial_number`: Optional serial number to connect to a specific device. If not provided, connects to the first available device. **Properties:** - `part_number`: The Saleae Part Number for the attached MSO. Supplies information like maximum sample rates and buffer sizes. ### Methods #### `MSO.capture()` ```python def capture(self, capture_config: mso_api.CaptureConfig, save_dir: pathlib.Path, timeout_secs: Optional[float] = None) -> mso_api.Capture ``` Captures data from the device according to the specified configuration. **Parameters:** - `capture_config`: Configuration for the capture (channels, sample rate, etc.) - `save_dir`: Directory to save capture data - `timeout_secs`: Optional timeout for the capture operation **Returns:** - A `Capture` object containing the captured data **Note:** This method will: 1. Create the save directory if it doesn't exist 2. Save the record options to `record_options.json` 3. Execute the capture command 4. Save the record result to `record_result.json` 5. Load the captured data into a `Capture` object **Example Usage:** ```{code-cell} python from pathlib import Path from saleae.mso_api import MSO, CaptureConfig, AnalogChannel, AnalogSettings, TimedCapture ## Initialize the MSO mso = MSO() ## Configure the capture config = CaptureConfig( enabled_channels=[AnalogChannel(channel=0, name="clock")], analog_settings=AnalogSettings(sample_rate=100e6), capture_settings=TimedCapture(capture_length_seconds=0.1), ) ## Perform the capture save_dir = Path("my-capture") capture = mso.capture(config, save_dir=save_dir) ## Access the captured data and inspect the actual (effective) capture settings print(f"Captured {capture.analog_data['clock'].num_samples} samples at a " f"{capture.config.analog_settings.sample_rate/1e6:.1f} MHz sample rate") ``` ### Error Handling The `MSO` class can raise the following exceptions, which are all subclasses of `MsoApiError`: - `CaptureTimeoutError` when capture operations timeout - `MsoCommandError` when device commands otherwise fail - `SmartCableNotReadyError` when a digital capture is initiated on a Smart Cable that's not plugged in - `SaleaeCommsProcessError` when something has broken in the low-level communications layer, or the device has been unplugged from USB during a capture. If either of these conditions arise, the directory you requested your capture to be saved in will contain a file called `traceback.txt` with debug information from your own program as well as the underlying capture executable. #### `CaptureTimeoutError` ```python class CaptureTimeoutError(MsoApiError): def __init__(self, timeout_seconds: Optional[float] = None): self.timeout_seconds = timeout_seconds ``` Exception raised when a capture operation times out. This can occur in two scenarios: - No trigger condition was found during a triggered capture - The actual capturing and uploading did not complete within the specified timeout period If you're running into a lot of these, try the following: - check that your trigger condition is actually occurring on the channel you've configured within the trigger timeout - consider increasing (or dropping) the overall command timeout (`timeout_secs`), as that includes upload time from the device. **Attributes:** - `timeout_seconds`: The timeout duration that was exceeded #### `MsoCommandError` ```python class MsoCommandError(MsoApiError): def __init__(self, command: str, stdout: str): self.command = command self.stdout = stdout super().__init__(f"Command {command} failed, stdout was: {stdout}") ``` Exception raised when device commands otherwise fail. **Attributes:** - `command`: The command that failed - `stdout`: The stdout output from the command #### `SmartCableNotReadyError` ```python class SmartCableNotReadyError(MsoApiError): def __init__(self, command: str, stdout: str): self.command = command self.stdout = stdout super().__init__(f"Smart cable was not connected or initialized in time. {command} failed, stdout was: {stdout}") ``` Exception raised when a smart cable is not ready or not connected. This typically occurs when: - The smart cable is not properly connected to the MSO device - The smart cable has not been initialized in time for the capture operation - There are communication issues with the smart cable hardware **Attributes:** - `command`: The command that failed - `stdout`: The stdout output from the command **Common Solutions:** - Ensure the smart cable is properly connected to the MSO device - Try unplugging and replugging the smart cable - Verify that the smart cable is being detected by the system by opening Logic2 and attempting to capture data from it #### `SaleaeCommsProcessError` ```python class SaleaeCommsProcessError(MsoApiError): def __init__(self, args: list[str], stdout: str, stderr: str, returncode: int, env: Optional[dict[str, str]] = None): self.msoexe_args = args self.stdout = stdout self.stderr = stderr self.returncode = returncode self.env = env ``` Exception raised when there are low-level communication issues with the MSO device. This typically occurs when: - The device is disconnected from USB during a capture operation - There are fundamental communication problems with the device **Attributes:** - `msoexe_args`: The command arguments that were passed to the MSO executable - `stdout`: The stdout output from the command - `stderr`: The stderr output from the command - `returncode`: The return code from the MSO executable - `env`: The environment variables used during execution **Note:** If you encounter this error outside of obvious scenarios like disconnecting the USB cable during capture, please contact Saleae support with the error details and your system information. We'd like to get the problem fixed as soon as possible. ## Plot Utility The plot utility provides a simple way to visualize analog and digital signals from Logic MSO captures. This utility is primarily intended for debugging and quick visualization purposes - it is not meant to be a fully-featured plotting solution. For advanced plotting needs, consider using matplotlib directly or other specialized plotting libraries. ### Features - Plot analog signals with voltage levels and trigger thresholds - Plot digital signals below, one per plot - Slice the time window for the plot as desired - Add vertical lines to mark things in time ### Usage ```python from pathlib import Path from saleae import mso_api from saleae.mso_api.utils.plot import plot_capture, PlotMarker ## Load a capture cap_dir = Path("path/to/capture") capture = mso_api.Capture.from_dir(cap_dir) ## Optional: Create markers for important events markers = [ PlotMarker(time=0.001), PlotMarker(time=0.002) ] ## Plot the capture plot_capture( capture=capture, save_to=Path("debug_plot.png"), start_time=0.000, # Optional: Start time in seconds stop_time=0.005, # Optional: Stop time in seconds title="My Capture", # Optional: Plot title markers=markers # Optional: List of markers ) ``` ### Parameters - `capture` (Capture): The capture object to plot - `save_to` (Path): Where to save the plot image - `start_time` (float, optional): Start time in seconds for the plot window - `stop_time` (float, optional): Stop time in seconds for the plot window - `title` (str, optional): Title for the plot - `markers` (list[PlotMarker], optional): List of markers to show on the plot ## Synthetic Trigger The `synthetic_trigger` module provides functionality to apply trigger conditions to existing captures, allowing you to split a single capture into multiple segments based on trigger conditions. ### Overview This module is particularly useful when you have a long capture and want to extract specific events based on signal characteristics. It allows you to: - Define trigger conditions after the capture has been made - Split a capture into multiple segments based on those triggers - Extract precise time windows around each trigger event - Apply holdoff periods between triggers ### Functions #### `split_capture` ```python def split_capture(capture: Capture, trigger: EdgeTrigger, pre_trigger_seconds: float, post_trigger_seconds: float) -> List[Capture] ``` Splits a capture into multiple captures based on a trigger condition. **Parameters:** - `capture`: The capture to split - `trigger`: The trigger settings (e.g., EdgeTrigger) - `pre_trigger_seconds`: Time before the trigger to include (can be negative to delay the trigger) - `post_trigger_seconds`: Time after the trigger to include (positive value) **Returns:** - A list of new captures, each spanning from a detected trigger - pre_trigger_seconds to the detected trigger + post_trigger_seconds **Note:** Captures that aren't long enough to include both pre- and post-trigger data are discarded. **Example:** ```python from saleae.mso_api.capture import Capture from saleae.mso_api.synthetic_trigger import split_capture from saleae.mso_api.trigger import EdgeTrigger, EdgeTriggerDirection from pathlib import Path ## Load a capture cap_dir = Path("path/to/capture") cap = Capture.from_dir(cap_dir) ## Define a trigger (rising edge on channel "data") trigger = EdgeTrigger( channel_name="data", threshold_volts=0, direction=EdgeTriggerDirection.RISING, holdoff_seconds=0.001 # 1ms minimum between triggers ) ## Split the capture with 6µs before the trigger and 56µs after caps = split_capture(cap, trigger, pre_trigger_seconds=-6e-6, post_trigger_seconds=56e-6) print(f"Found {len(caps)} triggered segments") ``` #### `find_trigger_indices` ```python def find_trigger_indices(capture: Capture, trigger: EdgeTrigger) -> List[int] ``` Finds the indices in the capture where the trigger conditions are met. **Parameters:** - `capture`: The capture to analyze - `trigger`: The trigger settings **Returns:** - A list of indices where the trigger conditions are met #### `slice_capture` ```python def slice_capture(capture: Capture, start_idx: int, end_idx: int) -> Capture ``` Creates a new capture containing only the data between the specified indices. **Parameters:** - `capture`: The capture to slice - `start_idx`: Starting index (inclusive) - `end_idx`: Ending index (exclusive) **Returns:** - A new Capture object containing only the specified range of data ### Common Use Cases 1. **Protocol Analysis**: Extract all instances of a specific protocol transaction 2. **Fault Detection**: Identify all occurrences of a fault condition in a long capture 3. **Signal Processing**: Pre-process data by extracting only relevant segments for further analysis ### Error Handling The module will raise appropriate errors if: - The trigger channel is not found in the capture - The capture has no channels - The pre-trigger time is after the post-trigger time - The capture segments are too short to include the requested time windows ## Trigger The `trigger` module provides classes for defining trigger conditions that can be used with both hardware captures and synthetic triggers. ### Overview This module defines the trigger settings that determine when a capture should start or how a capture should be split. It currently supports edge triggers, which detect when a signal crosses a specified voltage threshold. ### Classes #### `EdgeTriggerDirection` An enumeration that defines the direction of an edge trigger: ```python class EdgeTriggerDirection(enum.Enum): RISING = "rising" # Trigger when signal rises above threshold FALLING = "falling" # Trigger when signal falls below threshold ``` #### `EdgeTrigger` A dataclass that defines an edge trigger configuration: ```python @dataclass class EdgeTrigger: channel_index: Optional[int] = None channel_name: Optional[str] = None threshold_volts: float = 0.0 direction: EdgeTriggerDirection = EdgeTriggerDirection.RISING ``` **Parameters:** - `channel_index`: The channel number to trigger on (0-based index) - `channel_name`: The name of the channel to trigger on - `threshold_volts`: The voltage threshold that the signal must cross - `direction`: Whether to trigger on rising or falling edges - `holdoff`: Support coming soon! **Note:** Either `channel_index` or `channel_name` must be provided, but not both. If both are provided, a warning will be logged. ### Example Usage ```{code-cell} python from saleae import mso_api ## Create a trigger for rising edges on channel 1 at 0V trigger = mso_api.EdgeTrigger( channel_index=1, threshold_volts=0, direction=mso_api.EdgeTriggerDirection.RISING ) ``` ### Usage with Capture Configuration Edge triggers can also be used with the `CaptureConfig` class to configure hardware-triggered captures: ```{code-cell} python from saleae import mso_api ## Create a configuration for a triggered capture config = mso_api.CaptureConfig( enabled_channels=[mso_api.AnalogChannel(channel=0, name="data")], analog_settings=mso_api.AnalogSettings(sample_rate=100e6), capture_settings=mso_api.AnalogTriggered( trigger=mso_api.EdgeTrigger(channel_name="data", threshold_volts=0), pre_trigger_seconds=0.0005, post_trigger_seconds=0.0005, timeout_seconds=1.0 ) ) ``` ## API Reference ### saleae.mso_api Saleae MSO API package. #### `MSO` **Attributes:** | Name | Type | Description | |------|------|-------------| | `wrapper` | | | | `part_number` | `MsoPodPartNumber` | | **Methods:** ##### `__init__(serial_number: Optional[str] = None, wrapper: Optional[MsoWrapper] = None)` ##### `capture(capture_config: CaptureConfig, save_dir: Path, timeout_secs: Optional[float] = None, debug: bool = False) -> Capture` ##### `capture_analog(capture_config: CaptureConfig, save_dir: Path, timeout_secs: Optional[float] = None) -> Capture` Older alias for capture() ##### `close()` #### `AnalogData` **Attributes:** | Name | Type | Description | |------|------|-------------| | `voltages` | `NDArray[np.float32]` | | | `num_samples` | `int` | | **Methods:** ##### `voltage_at_time(time: float) -> float` Returns the voltage of the analog channel at the given time ##### `__init__(channel: int, name: str, begin_time: float, end_time: float, sample_rate: float, downsample: int, voltages: NDArray[np.float32]) -> None` #### `DiscontinuousAnalogData` **Attributes:** | Name | Type | Description | |------|------|-------------| | `channel` | `int` | | | `name` | `str` | | | `begin_time` | `float` | | | `end_time` | `float` | | | `sample_rate` | `float` | | | `downsample` | `int` | | | `traces` | `List[AnalogData]` | | | `num_samples` | `int` | | **Methods:** ##### `voltage_at_time(time: float) -> float` Returns the voltage of the analog channel at the given time ##### `__init__(channel: int, name: str, begin_time: float, end_time: float, sample_rate: float, downsample: int, traces: List[AnalogData]) -> None` #### `DigitalData` **Attributes:** | Name | Type | Description | |------|------|-------------| | `initial_state` | `bool` | | | `transition_times` | `NDArray[np.float64]` | | **Methods:** ##### `state_at_idx(idx: int) -> bool` Returns the state of the digital channel just before the given transition_time index ##### `state_at_time(time: float) -> bool` Returns the state of the digital channel at the given time ##### `__init__(port: int, channel: int, name: str, begin_time: float, end_time: float, initial_state: bool, transition_times: NDArray[np.float64]) -> None` #### `DiscontinuousDigitalData` **Attributes:** | Name | Type | Description | |------|------|-------------| | `port` | `int` | | | `channel` | `int` | | | `name` | `str` | | | `begin_time` | `float` | | | `end_time` | `float` | | | `segments` | `List[DigitalData]` | | **Methods:** ##### `state_at_time(time: float) -> bool` Returns the state of the digital channel at the given time ##### `__init__(port: int, channel: int, name: str, begin_time: float, end_time: float, segments: List[DigitalData]) -> None` #### `Capture` **Attributes:** | Name | Type | Description | |------|------|-------------| | `analog_data` | `Dict[str, BaseAnalogData]` | | | `digital_data` | `Dict[str, BaseDigitalData]` | | | `config` | `CaptureConfig` | | | `analog_start_time` | `float` | Returns the start time of the capture. | | `analog_sample_rate` | `float` | Returns the sample rate of the capture. | | `analog_channel_names` | `List[str]` | Returns a list of analog channel names in the capture. | | `analog_time_series` | `NDArray[np.float64]` | Returns the time array for analog data. | | `analog_stop_time` | `float` | Returns the end time of the capture. | | `num_analog_samples` | `int` | Returns the number of analog samples in the capture. | **Methods:** ##### @classmethod `from_dir(dir_path: Path) -> Capture` Create a Capture from a directory containing analog_.bin files. Args: dir_path: Path to directory containing analog_.bin files Returns: Capture object with analog data loaded from files ##### @classmethod `from_config(config: CaptureConfig, dir_path: Path) -> Capture` Create a Capture from a CaptureConfig by reading the files in it's save directory ##### `delete()` Delete the capture files from the disk ##### `__init__(analog_data: Dict[str, BaseAnalogData], digital_data: Dict[str, BaseDigitalData], config: CaptureConfig, _analog_start_time: Optional[float] = None, _stored_path: Optional[Path] = None) -> None` #### `Coupling` Coupling type **Attributes:** | Name | Type | Description | |------|------|-------------| | `DC` | | | | `AC` | | | #### `ProbeAttenuation` Probe attenuation type **Attributes:** | Name | Type | Description | |------|------|-------------| | `PROBE_10X` | | | | `PROBE_1X` | | | #### `BandwidthLimit` Bandwidth limit type **Attributes:** | Name | Type | Description | |------|------|-------------| | `MHz_20` | | | | `MHz_350` | | | #### `DownsampleMode` Downsample mode type **Attributes:** | Name | Type | Description | |------|------|-------------| | `DROP` | | | | `AVERAGE` | | | #### `AnalogChannel` Analog channel configuration **Attributes:** | Name | Type | Description | |------|------|-------------| | `center_voltage` | `float` | | | `voltage_range` | `float` | | | `probe_attenuation` | `ProbeAttenuation` | | | `coupling` | `Coupling` | | | `bandwidth_mhz` | `BandwidthLimit` | | **Methods:** ##### `to_dict(complete: bool = False) -> Dict[str, Any]` ##### @classmethod `from_dict(dict_version: Dict[str, Any]) -> AnalogChannel` ##### `__init__(channel: int, name: Optional[str] = None, center_voltage: float = 0.0, voltage_range: float = 10.0, probe_attenuation: ProbeAttenuation = ProbeAttenuation.PROBE_10X, coupling: Coupling = Coupling.DC, bandwidth_mhz: BandwidthLimit = BandwidthLimit.MHz_350) -> None` #### `DigitalChannel` Digital channel configuration **Attributes:** | Name | Type | Description | |------|------|-------------| | `port` | `Optional[int]` | | | `threshold_volts` | `Optional[float]` | | | `minimum_pulse_width_samples` | `Optional[int]` | | **Methods:** ##### `to_dict(complete: bool = False) -> Dict[str, Any]` ##### @classmethod `from_dict(dict_version: Dict[str, Any]) -> DigitalChannel` ##### `__init__(channel: int, name: Optional[str] = None, port: Optional[int] = None, threshold_volts: Optional[float] = None, minimum_pulse_width_samples: Optional[int] = None) -> None` #### `AnalogSettings` Analog settings **Attributes:** | Name | Type | Description | |------|------|-------------| | `downsample_mode` | `DownsampleMode` | | | `sample_rate` | `float` | | **Methods:** ##### @classmethod `default_for(part_number: MsoPodPartNumber) -> AnalogSettings` ##### `validate_and_update_sample_rate_for(part_number: MsoPodPartNumber) -> int` Validate the sample rate is supported by the given device part number, updating slightly to the nearest valid rate if necessary Return the exact downsample ratio that will be used. ##### `to_dict() -> Dict[str, Any]` ##### @classmethod `from_dict(dict_version: Dict[str, Any]) -> AnalogSettings` ##### `__init__(downsample_mode: DownsampleMode = DownsampleMode.DROP, sample_rate: float = 1600000000) -> None` #### `TimedCapture` Settings for a timed capture **Attributes:** | Name | Type | Description | |------|------|-------------| | `capture_length_seconds` | `float` | | **Methods:** ##### `to_dict() -> Dict[str, Any]` ##### @classmethod `from_dict(dict_version: Dict[str, Any]) -> TimedCapture` ##### `__init__(capture_length_seconds: float) -> None` #### `AnalogTriggered` Settings for a capture triggered by an analog signal **Attributes:** | Name | Type | Description | |------|------|-------------| | `trigger` | `EdgeTrigger` | | | `pre_trigger_seconds` | `float` | | | `post_trigger_seconds` | `float` | | | `timeout_seconds` | `float` | | **Methods:** ##### `to_dict() -> Dict[str, Any]` ##### @classmethod `from_dict(dict_version: Dict[str, Any]) -> AnalogTriggered` ##### `__init__(trigger: EdgeTrigger, pre_trigger_seconds: float = 0.0005, post_trigger_seconds: float = 0.0005, timeout_seconds: float = 1.0) -> None` #### `CaptureConfig` Capture configuration **Attributes:** | Name | Type | Description | |------|------|-------------| | `enabled_channels` | `list[ChannelConfig]` | | | `analog_settings` | `Optional[AnalogSettings]` | | | `capture_settings` | `Optional[CaptureSettings]` | | **Methods:** ##### `validate_sample_buffer_and_timeout(mso_part_number: MsoPodPartNumber)` Validate the timeout on the capture is possible given the pre_trigger_ and post_trigger_seconds, and that the maximum sample buffer depth is not exceeded. ##### `get_logic_analyzer_settings() -> Optional[json_types.LogicAnalyzerSettings]` Validate that the digital channels are configured correctly Returns a json_types.LogicAnalyzerSettings object or None if no digital channels are enabled ##### `get_analog_settings(part_number: MsoPodPartNumber) -> Optional[json_types.AnalogSettings]` Return the analog settings as a json_types.AnalogSettings object Return None if no analog channels are enabled ##### `find_trigger_channel(trigger: EdgeTrigger) -> ChannelConfig` Return the ChannelConfig from enabled_channels that matches the given trigger ##### `validate_enabled_channels() -> list[Dict[str, Any]]` Validate enabled_channels and convert them to dictionaries ##### `to_dict(output_dir: Path, mso_part_number: MsoPodPartNumber) -> Dict[str, Any]` Convert the configuration to a dictionary format expected by the MSO ##### `to_config_dict() -> dict[Any, Any]` Emit this configuration as a dictionary ##### @classmethod `from_config_dict(dict_version: dict[Any, Any]) -> CaptureConfig` Convert a dictionary version of this configuration to a CaptureConfig ##### `to_dir(output_dir: Path, mso_part_number: MsoPodPartNumber) -> Path` Save this configuration as a directory of files and return the path of the record options file ##### @classmethod `from_dir(dir_path: Path) -> CaptureConfig` Load a CaptureConfig from a directory of files ##### `__init__(enabled_channels: list[ChannelConfig], analog_settings: Optional[AnalogSettings] = None, capture_settings: Optional[CaptureSettings] = None) -> None` #### `EdgeTriggerDirection` Trigger direction **Attributes:** | Name | Type | Description | |------|------|-------------| | `RISING` | | | | `FALLING` | | | #### `EdgeTrigger` Trigger settings object **Attributes:** | Name | Type | Description | |------|------|-------------| | `channel_index` | `Optional[int]` | | | `channel_name` | `Optional[str]` | | | `threshold_volts` | `float` | | | `direction` | `EdgeTriggerDirection` | | | `holdoff_seconds` | `float` | | **Methods:** ##### `to_dict() -> Dict[str, Any]` ##### @classmethod `from_dict(dict_version: Dict[str, Any]) -> EdgeTrigger` ##### `__init__(channel_index: Optional[int] = None, channel_name: Optional[str] = None, threshold_volts: float = 0.0, direction: EdgeTriggerDirection = EdgeTriggerDirection.RISING, holdoff_seconds: float = 0.0) -> None` ### saleae.mso_api.utils Utility classes for binary file parsing and synthetic triggers. #### `SaleaeFileVersion` Enum representing Saleae file versions. **Attributes:** | Name | Type | Description | |------|------|-------------| | `FileVersion_Initial` | | | | `FileVersion_V1` | | | #### `SaleaeFiletype` Enum representing Saleae file types. **Attributes:** | Name | Type | Description | |------|------|-------------| | `Filetype_DigitalExport` | | | | `Filetype_AnalogExport` | | | | `Filetype_WaveformAdcExport` | | | | `Filetype_DigitalStore` | | | | `Filetype_AnalogStore` | | | #### `GraphTime` Represents a time value in the Saleae file format. **Attributes:** | Name | Type | Description | |------|------|-------------| | `milliseconds` | `int` | | | `fractional_milliseconds` | `float` | | **Methods:** ##### `__init__(milliseconds: int, fractional_milliseconds: float) -> None` #### `SamplingConfiguration` Configuration for sampling in Saleae files. **Attributes:** | Name | Type | Description | |------|------|-------------| | `sampling_rate` | `float` | | | `time_zero` | `GraphTime` | | **Methods:** ##### `__init__(sampling_rate: float, time_zero: GraphTime) -> None` #### `WaveformAdcExport_V0` Represents a WaveformAdcExport in Saleae binary format **Attributes:** | Name | Type | Description | |------|------|-------------| | `sample_rate` | `int` | | | `gain` | `float` | | | `offset` | `float` | | | `min_adc_code` | `int` | | | `max_adc_code` | `int` | | | `num_samples` | `int` | | | `adc_codes` | `NDArray[np.int16]` | | **Methods:** ##### `to_voltages() -> NDArray[np.float64]` Convert ADC codes to voltage values using gain and offset. ##### `__init__(sample_rate: int, gain: float, offset: float, min_adc_code: int, max_adc_code: int, num_samples: int, adc_codes: NDArray[np.int16]) -> None` #### `AnalogExport_V0` Represents an AnalogExport in Saleae binary format **Attributes:** | Name | Type | Description | |------|------|-------------| | `begin_time` | `float` | | | `sample_rate` | `float` | | | `downsample` | `int` | | | `num_samples` | `int` | | | `voltages` | `NDArray[np.float32]` | | **Methods:** ##### `__init__(begin_time: float, sample_rate: float, downsample: int, num_samples: int, voltages: NDArray[np.float32]) -> None` #### `DigitalExport_V0` Represents a DigitalExport in Saleae binary format. **Attributes:** | Name | Type | Description | |------|------|-------------| | `initial_state` | `int` | | | `begin_time` | `float` | | | `end_time` | `float` | | | `num_transitions` | `int` | | | `transition_times` | `NDArray[np.float64]` | | **Methods:** ##### `__init__(initial_state: int, begin_time: float, end_time: float, num_transitions: int, transition_times: NDArray[np.float64]) -> None` #### `DigitalChunk` Represents a chunk of digital data in v1 format. **Attributes:** | Name | Type | Description | |------|------|-------------| | `initial_state` | `int` | | | `sample_rate` | `float` | | | `begin_time` | `float` | | | `end_time` | `float` | | | `transition_times` | `NDArray[np.float64]` | | **Methods:** ##### `__init__(initial_state: int, sample_rate: float, begin_time: float, end_time: float, transition_times: NDArray[np.float64]) -> None` #### `AnalogWaveform` Represents a waveform of analog data in v1 format. **Attributes:** | Name | Type | Description | |------|------|-------------| | `begin_time` | `float` | | | `trigger_time` | `float` | | | `sample_rate` | `float` | | | `downsample` | `int` | | | `voltages` | `NDArray[np.float32]` | | **Methods:** ##### `__init__(begin_time: float, trigger_time: float, sample_rate: float, downsample: int, voltages: NDArray[np.float32]) -> None` #### `WaveformAdcWaveform` Represents a waveform of ADC data in v1 format. **Attributes:** | Name | Type | Description | |------|------|-------------| | `begin_time` | `float` | | | `trigger_time` | `float` | | | `sample_rate` | `float` | | | `downsample` | `int` | | | `gain` | `float` | | | `offset` | `float` | | | `min_adc_code` | `int` | | | `max_adc_code` | `int` | | | `adc_codes` | `NDArray[np.int16]` | | **Methods:** ##### `to_voltages() -> NDArray[np.float64]` Convert ADC codes to voltage values using gain and offset. ##### `__init__(begin_time: float, trigger_time: float, sample_rate: float, downsample: int, gain: float, offset: float, min_adc_code: int, max_adc_code: int, adc_codes: NDArray[np.int16]) -> None` #### `DigitalExport_V1` Represents a DigitalExport in Saleae binary format v1. **Attributes:** | Name | Type | Description | |------|------|-------------| | `chunks` | `list[DigitalChunk]` | | **Methods:** ##### `__init__(chunks: list[DigitalChunk]) -> None` #### `AnalogExport_V1` Represents an AnalogExport in Saleae binary format v1. **Attributes:** | Name | Type | Description | |------|------|-------------| | `waveforms` | `list[AnalogWaveform]` | | **Methods:** ##### `__init__(waveforms: list[AnalogWaveform]) -> None` #### `WaveformAdcExport_V1` Represents a WaveformAdcExport in Saleae binary format v1. **Attributes:** | Name | Type | Description | |------|------|-------------| | `waveforms` | `list[WaveformAdcWaveform]` | | **Methods:** ##### `__init__(waveforms: list[WaveformAdcWaveform]) -> None` #### `SaleaeFile` Represents a Saleae binary file with its metadata and data. **Attributes:** | Name | Type | Description | |------|------|-------------| | `token` | `str` | | | `version` | `SaleaeFileVersion` | | | `type` | `SaleaeFiletype` | | | `contents` | `Union[WaveformAdcExport_V0, WaveformAdcExport_V1, AnalogExport_V0, AnalogExport_V1, DigitalExport_V0, DigitalExport_V1, None]` | | **Methods:** ##### `__init__(token: str, version: SaleaeFileVersion, type: SaleaeFiletype, contents: Union[WaveformAdcExport_V0, WaveformAdcExport_V1, AnalogExport_V0, AnalogExport_V1, DigitalExport_V0, DigitalExport_V1, None] = None) -> None` ### saleae.mso_api.errors Error types raised by the Saleae MSO API. #### `MsoApiError` Base class for all exceptions in the Saleae MSO API #### `MsoCommandError` Exception raised when a command fails **Attributes:** | Name | Type | Description | |------|------|-------------| | `command` | | | | `stdout` | | | **Methods:** ##### `__init__(command: str, stdout: str)` #### `NoResultFileError` Exception raised when a command does not produce a result file **Methods:** ##### `__init__(command: str, stdout: str)` #### `SmartCableNotReadyError` Exception raised when a smart cable is not ready or not connected **Methods:** ##### `__init__(command: str, stdout: str)` #### `CaptureTimeoutError` Exception raised when a capture times out **Attributes:** | Name | Type | Description | |------|------|-------------| | `record_options` | `Optional[dict[Any, Any]]` | | | `timeout_seconds` | | | **Methods:** ##### `__init__(record_options: dict[Any, Any], timeout_seconds: Optional[float] = None)` #### `CaptureConfigError` Error for invalid capture configurations #### `SaleaeCommsProcessError` **Attributes:** | Name | Type | Description | |------|------|-------------| | `msoexe_args` | | | | `stdout` | | | | `stderr` | | | | `returncode` | | | | `env` | | | **Methods:** ##### `__init__(args: list[str], stdout: str, stderr: str, returncode: int, env: Optional[dict[str, str]] = None)` #### `NoJsonFound` #### `USBPermissionsError` Exception raised when a USB device is not accessible due to permissions on a Linux system **Methods:** ##### `__init__()` #### `Timeout` #### `TimeOutOfBoundsError` Exception raised when a time is not defined #### `UndefinedDataError` Exception raised when data is not defined for the given time period