SPI Controller Box
To synchronously control multiple NV200 devices, the SPI Box can be used as a master device. It can send positional data to up to 3 connected NV200 devices via SPI communication.
Due to the NV200’s SPI limitations, the SPI Box can only send position commands to the connected devices. Therefore configuration of the connected NV200 devices (e.g., PID parameters, operation mode, etc.) must be done separately.
The SPI Box provides the following functionality: - Sending single position commands to up to 3 connected NV200 devices - Replaying a custom waveform with up to 20000 samples - Reading back the SPI data received from the connected NV200 devices - Ethernet, USB and RS-232 connection support
The following example demonstrates how to use these features of the SPI Controller Box.
Step by step guide to using the SPI Controller Box with the NV200 library
This guide walks through the typical steps required to control the SPI Controller Box
(SpiBoxDevice) using the NV200 Python library and shows how to discover/connect to the device, set simple
setpoints, upload and run waveforms, and read back the SPI response data.
Step by step guide to using the SPI Controller Box with the NV200 library
Step 1: Import the required modules
Start by importing the required modules seen in the example above.
import asyncio
import matplotlib.pyplot as plt
import matplotlib_helpers
from nv200.connection_utils import connect_to_detected_device
from nv200.device_discovery import discover_devices
from nv200.shared_types import DiscoverFlags
from nv200.spibox_device import SpiBoxDevice
from nv200.waveform_generator import WaveformGenerator, WaveformType
Step 2: Discover available SPI Box devices
Discover devices connected via the interfaces you want to use. In this example, we search for SPI Controller Boxes connected via USB using:
detected_devices = await discover_devices(
DiscoverFlags.DETECT_SERIAL | DiscoverFlags.READ_DEVICE_INFO,
device_class=SpiBoxDevice
)
Step 3: Connect to a detected device
After discovery, connect to a device using the helper from connection_utils. In this case we connect to the first detected device:
device : SpiBoxDevice = await connect_to_detected_device(detected_devices[0])
Step 4: Simple setpoint commands (move channels)
The SPI Box can only send position commands to connected NV200 devices. Use the SpiBoxDevice’s high-level set/get
helpers to move channels and read back positions. The example moves all channels to lowest and highest setpoints
and reads them back using:
await device.set_setpoints_percent(0.0, 0.0, 0.0)
await asyncio.sleep(1.0)
await device.set_setpoints_percent(100.0, 100.0, 100.0)
await asyncio.sleep(1.0)
positions = await device.get_setpoints_percent()
Note: setpoints are provided as percentages (0.0–100.0). The SpiBoxDevice converts these to the NV200 16-bit
hex representation internally.
Step 5: Create waveforms (generator usage)
To replay synchronized motion patterns, create a WaveformGenerator instance. The example creates the generator with
None as the device because the SPI Box is not a standard NV200Device and the generator is only used to create
sample arrays. We then generate three sine waveforms (one per channel) in a loop with generate_waveform(...).
waveform_generator = WaveformGenerator(None)
waveforms = []
# Generate a sine waveforms for all channels
for ch in range(3):
waveform = waveform_generator.generate_waveform(
waveform_type=WaveformType.SINE,
freq_hz=20,
low_level=0.0,
high_level=100.0,
phase_shift_rad= ch * (3.14159 / 2.0) # Phase shift each channel by 90 degrees
)
waveforms.append(waveform)
Step 6: Transfer waveforms to the SPI Box
Before uploading samples, configure how often the waveforms should repeat and at what sampling rate they should be played back:
await device.set_waveform_cycles(3, 3, 3)
await device.set_waveform_sample_factors(
waveforms[0].sample_factor,
waveforms[1].sample_factor,
waveforms[2].sample_factor
)
Upload the prepared sample arrays with:
await device.upload_waveform_samples(
waveforms[0].values,
waveforms[1].values,
waveforms[2].values,
lambda current, total: print(f"Upload progress: {current}/{total} samples")
)
Step 7: Start and wait for waveform playback
Start waveform playback on the device with start_waveforms() and wait for completion with
await_waveform_completion() as shown in the example:
await device.start_waveforms()
await device.await_waveform_completion()
Call await device.stop_waveforms() if you need to stop playback early.
Step 8: Read back SPI response data (recorded samples)
Read the SPI response buffer using get_waveform_response. The example reads every 3rd sample (step size 3) and
limits to 1000 max samples while reporting progress:
response = await device.get_waveform_response(
3,
1000,
lambda current, total: print(f"Readback progress: {current}/{total} samples")
)
The returned items are WaveformGenerator.WaveformData objects which contain values and sample_time_ms.
Step 9: Plotting and visualization
Use the included matplotlib_helpers and matplotlib to plot response channels as in the example:
matplotlib_helpers.prepare_plot_style()
plt.plot(response[0].sample_times_ms, response[0].values, linestyle='-', color='orange', label='Channel 1')
plt.plot(response[1].sample_times_ms, response[1].values, linestyle='-', color='green', label='Channel 2')
plt.plot(response[2].sample_times_ms, response[2].values, linestyle='-', color='blue', label='Channel 3')
matplotlib_helpers.show_plot()
Step 10: Close the device
When finished, close the device transport to release resources:
await device.close()
Extra notes and implementation details
The
SpiBoxDeviceexposes convenience methods that map to low-level SPI Box commands: -set_waveform_sample_factors(...)/get_waveform_sample_factors()-set_waveform_cycles(...)/get_waveform_cycles()-upload_waveform_samples(...), which sets sample counts and issueswfsetentries for every sample -start_waveforms()/stop_waveforms()which controlwfrun-get_waveform_response(...),get_response_samples_count()andget_response_sample(index)for readbackKeep in mind the SPI Box can only send position commands; device configuration (PID, mode, etc.) must be applied to the NV200 devices separately using their own configuration flows.
API Reference
- class SpiBoxDevice[source]
Bases:
PiezoDeviceBaseA high-level asynchronous client for communicating with NV200 piezo controllers. This class extends the
PiezoDeviceBasebase class and provides high-level methods for setting and getting various device parameters, such as PID mode, setpoint,- async connect(auto_adjust_comm_params: bool = True)[source]
Establishes a connection using the transport layer.
- async get_response_sample(index: int) List[float][source]
Get a single response sample at the specified index.
- Parameters:
index (int) – The index of the sample to retrieve.
- Returns:
A list containing the waveform data for each channel at the specified index.
- Return type:
List[float]
- async get_response_samples_count() int[source]
Get the number of available response samples from the device.
- Returns:
The number of available response samples.
- Return type:
int
- async get_setpoints_percent() List[float][source]
Get device setpoints as percentages (0.0 to 100.0) for 3 channels.
- Returns:
A list containing the setpoints for each channel as percentages.
- Return type:
List[float]
- async get_waveform_cycles() List[int][source]
Get the number of waveform cycles for each channel.
- Returns:
A list containing the number of cycles for each channel.
- Return type:
List[int]
- async get_waveform_response(step_size: int = 1, max_samples: int | None = None, on_progress: callable | None = None) List[WaveformData] | None[source]
Get the current waveform response from the device.
- Parameters:
step_size (int) – The step size for sampling the waveform response.
max_samples (int, optional) – The maximum number of samples to retrieve. If None, retrieves all available samples.
on_progress (callable, optional) – A callback function that receives progress updates.
- Returns:
A list containing three numpy arrays, one for each channel, with the waveform data. If no samples are available, returns None.
- async get_waveform_sample_factors() List[int][source]
Get the waveform sample factors for each channel.
- Returns:
A list containing the sample factors for each channel.
- Return type:
List[int]
- async set_setpoints_percent(ch1: float = 0, ch2: float = 0, ch3: float = 0) List[float][source]
Set device setpoints as percentages (0.0 to 100.0) for 3 channels.
Converts percent values to 16-bit hex strings and sends them as a formatted command.
- Parameters:
ch1 (float) – Setpoint for channel 1 (0.0 to 100.0).
ch2 (float) – Setpoint for channel 2 (0.0 to 100.0).
ch3 (float) – Setpoint for channel 3 (0.0 to 100.0).
- async set_waveform_cycles(ch1_cycles: int, ch2_cycles: int, ch3_cycles: int)[source]
Set the number of waveform cycles for each channel.
- Parameters:
ch1_cycles (int) – Number of cycles for channel 1.
ch2_cycles (int) – Number of cycles for channel 2.
ch3_cycles (int) – Number of cycles for channel 3.
- async set_waveform_sample_factors(ch1_factor: int, ch2_factor: int, ch3_factor: int)[source]
Set the waveform sample factors for each channel.
- Parameters:
ch1_factor (int) – Sample factor for channel 1.
ch2_factor (int) – Sample factor for channel 2.
ch3_factor (int) – Sample factor for channel 3.
- async set_waveform_samples(ch1: ndarray, ch2: ndarray, ch3: ndarray, on_progress: callable | None = None)[source]
Add waveform samples for each channel.
- async upload_waveform_samples(ch1: ndarray, ch2: ndarray, ch3: ndarray, on_progress: callable | None = None)[source]
Upload waveform samples for each channel.
Uploads the samples to the device after setting the sample counts.
- Parameters:
ch1 (np.ndarray) – Waveform samples for channel 1.
ch2 (np.ndarray) – Waveform samples for channel 2.
ch3 (np.ndarray) – Waveform samples for channel 3.
on_progress (callable, optional) – A callback function that receives progress updates.
- parse_hex_to_floats_percent(data: str) List[float][source]
Parses a string of 4-character hexadecimal values into a float.
The hex values are interpreted as unsigned 16-bit integers and converted to floats.
- Parameters:
data (str) – A string of comma-separated 4-character hex values (e.g., “0000,FFFD,FFFD”).
- Returns:
A list of float representations of the parsed unsigned integers.
- Return type:
List[float]