# 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