Skip to content

Commit 26d4b25

Browse files
ebrahimebrahimpeterhollendergeorgevigeletteNeuromod 2duvitech-llc
authored
Add sonication control API and work towards hardware interface (#205)
This is a squash of many commits from pull request #213: * Updating * tx7332 wip * [WIP] reorganize and rename * Update test_plan.ipynb * rename profile to index where remaining * Switch from index back to profile * Update test_registers.ipynb * change print_dict to print_regs * move xdc to same folder format * Fix seg_methods loading from dictionary * [BUG] update Plan to Protocol * Add c0 parameter to DirectDelays to match json * Update test notebooks * Updates for backwards compatibility with unpatched k-wave * Improves import order * Adds json-serializability to transducer and elements * Added USTX control, Added Demo Notebook and QT5 Application * added pinmap.json that is used for the transducer pin mapping by the gui application * fixed uart read * added trigger frequency settings * update setup tools * ran test on notebook * updated readme with high voltge power supply pin connections * note on pinmap file * fixed molex connector connections * silkscreen is wrong on the PCB + and - HV are swapped * update init * for Daxsonics only use the first AFE (64 elements) * adds new json and updates indexing * Update DemoApplication.py * added axis reference for focus * Update DemoApplication.py * update notebook * added apodization mapping * added flag to auto discover control board * set control board to True * updated python notebook to async uart * added async uart to Demo application and Python Notebook * paramaterized PORT Name for uart * Add HV Controller and USTX controller placeholders * Update pulse docstrings to show that amplitude is in V, not Pa * refactor ow_ustx into openlifu * starting to fix imports * refactor dict_conversion to fix import bug * calculate profile registers * start integration * stub out some of the api * added demo mode for testing without hardware * initial changes and checkin of 3.1 branch * demo gui working * updated notebook example with straight python (to be converted to a notebook) * WIP updating tests to use TxModule * updated gui code * added pwr_if initial tests * added pwr_if all basic tests * updated dependencies * update power test procedure * [WIP] refactor for review * Rework public interface and stress testing API * transmitter demo waveform test * removing async from demoapplication for intermediate testing * demoapplication running with updated interface * added 12v on off * added 12v enable * added reset command * bug fixes * set voltage * added timeout * read voltage setting * adding capability to load TI config generated from TI TX7332 Demo output * updated test comms to slower freq * added write ti config to tx chips * added load ti config file * finished migrating demo ui to new api interface * removed delay * removing unecessary old libs * new packet type for one wire * fix apodization typo in duty cycle * remove async * removed i2c * Reorganizes ustx into LIFUTXDevice Changes the names of some classes, and starts integrating register and control functions * Remove unrelated files * Delete pinmap.json * removes ustx.py * Fix DictMixin import * Fix DictMixin import * Remove instruction to mess with PYTHONPATH * Remove qt app dependencies * Run automated safe pre-commit fixes * Run automated pre-commit UP007 fixes * Fix last remaining ruff style issues * Auto fix indentation in LIFUHVController.py * Fix remaining pylint issues * remove extraneous comments and imports * Set specific loggers to debug rather than root logger * Fix broken device interface reference * Remove tx_7332_if * Interface cleanup * style update * added dfu mode switch * update uart to handle serial exception * consolidated interface * reworking improving widget * moved to pyQT6 * added ambient temperature reading * Create test_solution.py * updates * sort import * updated error raising * fixing lint errors * demo mode work * added calls to the widget * added get ambient temperature * Shift interface to Dict * Better mocking of LIFUInterface and bugfixes * style fixes * Set specific loggers to debug rather than root logger * Add LIFUInterface test mode test * Fix sonication control mock test --------- Co-authored-by: Peter Hollender <peterhollender@gmail.com> Co-authored-by: George Vigelette <george@openwater.cc> Co-authored-by: Neuromod 2 <lab@openwater.cc> Co-authored-by: George Vigelette <gvigelet@duvitech.com>
1 parent 560e44e commit 26d4b25

32 files changed

+4607
-1094
lines changed

notebooks/LIFUTestWidget.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import asyncio
2+
import logging
3+
import sys
4+
5+
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
6+
from PyQt6.QtGui import QBrush, QColor, QPainter
7+
from PyQt6.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget
8+
from qasync import QEventLoop
9+
10+
from openlifu.io.LIFUInterface import LIFUInterface
11+
from openlifu.plan.solution import ( # Assuming Pulse is needed to create a Solution
12+
Pulse,
13+
Solution,
14+
)
15+
16+
# Configure logging
17+
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s")
18+
logger = logging.getLogger(__name__)
19+
20+
21+
class LIFUTestWidget(QWidget):
22+
# Signals now accept two arguments: descriptor and port/data.
23+
signal_connected = pyqtSignal(str, str)
24+
signal_disconnected = pyqtSignal(str, str)
25+
signal_data_received = pyqtSignal(str, str)
26+
27+
def __init__(self):
28+
super().__init__()
29+
self.interface = LIFUInterface(run_async=True)
30+
# Maintain connection status for both descriptors
31+
self.connections = {"TX": False, "HV": False}
32+
self.treatment_running = False
33+
self.init_ui()
34+
self.connect_signals()
35+
36+
def init_ui(self):
37+
"""Initialize the UI components."""
38+
self.setWindowTitle("Open LIFU")
39+
self.setGeometry(100, 100, 300, 350)
40+
41+
# Status label shows connection status for both devices
42+
self.status_label = QLabel("TX: Disconnected, HV: Disconnected", self)
43+
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
44+
45+
# Existing buttons
46+
self.send_ping_button = QPushButton("Send Ping", self)
47+
self.send_ping_button.setEnabled(False)
48+
self.send_ping_button.clicked.connect(self.send_ping_command)
49+
50+
self.treatment_button = QPushButton("Run Treatment (Off)", self)
51+
self.treatment_button.setEnabled(False)
52+
self.treatment_button.clicked.connect(self.toggle_treatment_run)
53+
54+
# New buttons to call interface methods:
55+
self.load_solution_button = QPushButton("Load Solution", self)
56+
self.load_solution_button.clicked.connect(self.load_solution)
57+
58+
self.start_sonication_button = QPushButton("Start Sonication", self)
59+
self.start_sonication_button.clicked.connect(self.start_sonication)
60+
61+
self.stop_sonication_button = QPushButton("Stop Sonication", self)
62+
self.stop_sonication_button.clicked.connect(self.stop_sonication)
63+
64+
self.get_status_button = QPushButton("Get Status", self)
65+
self.get_status_button.clicked.connect(self.get_status)
66+
67+
# Layout
68+
layout = QVBoxLayout()
69+
layout.addWidget(self.status_label)
70+
layout.addWidget(self.send_ping_button)
71+
layout.addWidget(self.treatment_button)
72+
layout.addWidget(self.load_solution_button)
73+
layout.addWidget(self.start_sonication_button)
74+
layout.addWidget(self.stop_sonication_button)
75+
layout.addWidget(self.get_status_button)
76+
self.setLayout(layout)
77+
78+
def connect_signals(self):
79+
"""Connect the signals from the LIFU interface to the UI."""
80+
# Connect TX signals
81+
if hasattr(self.interface.txdevice, 'uart'):
82+
self.interface.txdevice.uart.signal_connect.connect(self.signal_connected.emit)
83+
self.interface.txdevice.uart.signal_disconnect.connect(self.signal_disconnected.emit)
84+
self.interface.txdevice.uart.signal_data_received.connect(self.signal_data_received.emit)
85+
else:
86+
logger.warning("TX UART interface not found in LIFUInterface.")
87+
88+
# Connect HV signals
89+
if hasattr(self.interface.hvcontroller, 'uart'):
90+
self.interface.hvcontroller.uart.signal_connect.connect(self.signal_connected.emit)
91+
self.interface.hvcontroller.uart.signal_disconnect.connect(self.signal_disconnected.emit)
92+
self.interface.hvcontroller.uart.signal_data_received.connect(self.signal_data_received.emit)
93+
else:
94+
logger.warning("HV UART interface not found in LIFUInterface.")
95+
96+
# Connect our widget signals to slots
97+
self.signal_connected.connect(self.on_connected)
98+
self.signal_disconnected.connect(self.on_disconnected)
99+
self.signal_data_received.connect(self.on_data_received)
100+
101+
async def start_monitoring(self):
102+
"""Start monitoring for USB device connections."""
103+
await self.interface.start_monitoring()
104+
105+
@pyqtSlot(str, str)
106+
def on_connected(self, descriptor, port):
107+
"""Handle the connected signal."""
108+
self.connections[descriptor] = True
109+
status_text = (
110+
f"TX: {'Connected' if self.connections['TX'] else 'Disconnected'}, "
111+
f"HV: {'Connected' if self.connections['HV'] else 'Disconnected'}"
112+
)
113+
self.status_label.setText(status_text)
114+
# Enable buttons if TX is connected (assuming TX is needed for ping/treatment)
115+
if self.connections["TX"]:
116+
self.send_ping_button.setEnabled(True)
117+
self.treatment_button.setEnabled(True)
118+
self.update()
119+
120+
@pyqtSlot(str, str)
121+
def on_disconnected(self, descriptor, port):
122+
"""Handle the disconnected signal."""
123+
self.connections[descriptor] = False
124+
status_text = (
125+
f"TX: {'Connected' if self.connections['TX'] else 'Disconnected'}, "
126+
f"HV: {'Connected' if self.connections['HV'] else 'Disconnected'}"
127+
)
128+
self.status_label.setText(status_text)
129+
# Disable TX buttons if TX is disconnected
130+
if not self.connections["TX"]:
131+
self.send_ping_button.setEnabled(False)
132+
self.treatment_button.setEnabled(False)
133+
self.update()
134+
135+
@pyqtSlot(str, str)
136+
def on_data_received(self, descriptor, data):
137+
"""Handle the data received signal."""
138+
self.status_label.setText(f"{descriptor} Received: {data}")
139+
self.update()
140+
141+
def send_ping_command(self):
142+
"""Send a ping command on the TX device."""
143+
if hasattr(self.interface.txdevice, 'ping'):
144+
self.interface.txdevice.ping()
145+
else:
146+
logger.warning("TX device does not support ping.")
147+
148+
def toggle_treatment_run(self):
149+
"""Toggle the treatment run state."""
150+
self.interface.toggle_treatment_run(self.treatment_running)
151+
self.treatment_running = not self.treatment_running
152+
self.treatment_button.setText(
153+
"Run Treatment (On)" if self.treatment_running else "Stop Treatment (Off)"
154+
)
155+
156+
def load_solution(self):
157+
"""Call the interface's set_solution method using a dummy solution."""
158+
try:
159+
# Create a fake solution for testing
160+
fake_solution = Solution(name="Test Solution", pulse=Pulse(amplitude=5))
161+
result = self.interface.set_solution(fake_solution)
162+
if result:
163+
self.status_label.setText("Solution loaded successfully.")
164+
else:
165+
self.status_label.setText("Failed to load solution.")
166+
except Exception as e:
167+
self.status_label.setText(f"Error loading solution: {e}")
168+
logger.error("Error loading solution: %s", e)
169+
170+
def start_sonication(self):
171+
"""Call the interface's start_sonication method."""
172+
try:
173+
result = self.interface.start_sonication()
174+
if result:
175+
self.status_label.setText("Sonication started.")
176+
else:
177+
self.status_label.setText("Failed to start sonication.")
178+
except Exception as e:
179+
self.status_label.setText(f"Error starting sonication: {e}")
180+
logger.error("Error starting sonication: %s", e)
181+
182+
def stop_sonication(self):
183+
"""Call the interface's stop_sonication method."""
184+
try:
185+
result = self.interface.stop_sonication()
186+
if result:
187+
self.status_label.setText("Sonication stopped.")
188+
else:
189+
self.status_label.setText("Failed to stop sonication.")
190+
except Exception as e:
191+
self.status_label.setText(f"Error stopping sonication: {e}")
192+
logger.error("Error stopping sonication: %s", e)
193+
194+
def get_status(self):
195+
"""Call the interface's get_status method and display the status."""
196+
try:
197+
status = self.interface.get_status()
198+
self.status_label.setText(f"Status: {status}")
199+
except Exception as e:
200+
self.status_label.setText(f"Error getting status: {e}")
201+
logger.error("Error getting status: %s", e)
202+
203+
def paintEvent(self, event):
204+
"""Draw the connection status indicator."""
205+
painter = QPainter(self)
206+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
207+
dot_radius = 20
208+
dot_color = QColor("green") if any(self.connections.values()) else QColor("red")
209+
brush = QBrush(dot_color)
210+
painter.setBrush(brush)
211+
rect = self.rect()
212+
painter.drawEllipse(
213+
rect.center().x() - dot_radius // 2,
214+
rect.top() + 20,
215+
dot_radius,
216+
dot_radius
217+
)
218+
219+
def closeEvent(self, event):
220+
"""Handle application closure."""
221+
self.cleanup_task = asyncio.create_task(self.cleanup_tasks())
222+
super().closeEvent(event)
223+
224+
async def cleanup_tasks(self):
225+
"""Stop monitoring and cancel running tasks."""
226+
self.interface.stop_monitoring()
227+
loop = asyncio.get_running_loop()
228+
tasks = [t for t in asyncio.all_tasks(loop) if t is not asyncio.current_task()]
229+
for task in tasks:
230+
task.cancel()
231+
await asyncio.gather(*tasks, return_exceptions=True)
232+
233+
234+
if __name__ == "__main__":
235+
app = QApplication(sys.argv)
236+
loop = QEventLoop(app)
237+
asyncio.set_event_loop(loop)
238+
239+
widget = LIFUTestWidget()
240+
widget.show()
241+
242+
async def main():
243+
await widget.start_monitoring()
244+
245+
with loop:
246+
widget._monitor_task = asyncio.ensure_future(main())
247+
loop.run_forever()

notebooks/foo.nii.gz

-8.26 MB
Binary file not shown.

notebooks/intensity.nii.gz

-1.94 MB
Binary file not shown.

notebooks/run_self_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from openlifu.io.LIFUInterface import LIFUInterface
2+
3+
# set PYTHONPATH=%cd%\src;%PYTHONPATH%
4+
# python notebooks/run_self_test.py
5+
"""
6+
Test script to automate:
7+
1. Connect to the device.
8+
2. Test HVController: Turn HV on/off and check voltage.
9+
3. Test Device functionality.
10+
"""
11+
print("Starting LIFU Test Script...")
12+
interface = LIFUInterface(test_mode=False)
13+
tx_connected, hv_connected = interface.is_device_connected()
14+
if tx_connected and hv_connected:
15+
print("LIFU Device Fully connected.")
16+
else:
17+
print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}')
18+
19+
print("Ping the device")
20+
interface.txdevice.ping()
21+
22+
print("Run Self OneWire Test")
23+
interface.txdevice.run_test()
24+
25+
print("Tests Finished")

notebooks/stress_test.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import random
2+
3+
from openlifu.io.LIFUInterface import LIFUInterface
4+
5+
6+
def run_test(interface, iterations):
7+
"""
8+
Run the LIFU test loop with random trigger settings.
9+
10+
Args:
11+
interface (LIFUInterface): The LIFUInterface instance.
12+
iterations (int): Number of iterations to run.
13+
"""
14+
for i in range(iterations):
15+
print(f"Starting Test Iteration {i + 1}/{iterations}...")
16+
17+
try:
18+
tx_connected, hv_connected = interface.is_device_connected()
19+
if not tx_connected: # or not hv_connected:
20+
raise ConnectionError(f"LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}")
21+
22+
print("Ping the device")
23+
interface.txdevice.ping()
24+
25+
print("Toggle LED")
26+
interface.txdevice.toggle_led()
27+
28+
print("Get Version")
29+
version = interface.txdevice.get_version()
30+
print(f"Version: {version}")
31+
32+
print("Echo Data")
33+
echo, length = interface.txdevice.echo(echo_data=b'Hello LIFU!')
34+
if length > 0:
35+
print(f"Echo: {echo.decode('utf-8')}")
36+
else:
37+
raise ValueError("Echo failed.")
38+
39+
print("Get HW ID")
40+
hw_id = interface.txdevice.get_hardware_id()
41+
print(f"HWID: {hw_id}")
42+
43+
print("Get Temperature")
44+
temperature = interface.txdevice.get_temperature()
45+
print(f"Temperature: {temperature} °C")
46+
47+
print("Get Trigger")
48+
trigger_setting = interface.txdevice.get_trigger()
49+
if trigger_setting:
50+
print(f"Trigger Setting: {trigger_setting}")
51+
else:
52+
raise ValueError("Failed to get trigger setting.")
53+
54+
print("Set Trigger with Random Parameters")
55+
# Generate random trigger frequency and pulse width
56+
trigger_frequency = random.randint(5, 25) # Random frequency between 5 and 25 Hz
57+
trigger_pulse_width = random.randint(10, 30) * 1000 # Random pulse width between 10 and 30 ms (convert to µs)
58+
59+
json_trigger_data = {
60+
"TriggerFrequencyHz": trigger_frequency,
61+
"TriggerMode": 1,
62+
"TriggerPulseCount": 0,
63+
"TriggerPulseWidthUsec": trigger_pulse_width
64+
}
65+
trigger_setting = interface.txdevice.set_trigger(data=json_trigger_data)
66+
if trigger_setting:
67+
print(f"Trigger Setting Applied: Frequency = {trigger_frequency} Hz, Pulse Width = {trigger_pulse_width // 1000} ms")
68+
if trigger_setting["TriggerFrequencyHz"] != trigger_frequency or trigger_setting["TriggerPulseWidthUsec"] != trigger_pulse_width:
69+
raise ValueError("Failed to set trigger setting.")
70+
else:
71+
raise ValueError("Failed to set trigger setting.")
72+
73+
print(f"Iteration {i + 1} passed.\n")
74+
75+
except Exception as e:
76+
print(f"Test failed on iteration {i + 1}: {e}")
77+
break
78+
79+
if __name__ == "__main__":
80+
print("Starting LIFU Test Script...")
81+
interface = LIFUInterface(test_mode=False)
82+
83+
# Number of iterations to run
84+
test_iterations = 1000 # Change this to the desired number of iterations
85+
86+
run_test(interface, test_iterations)

0 commit comments

Comments
 (0)