How to Debug a Single Nallely VirtualDevice Instance with pdb Without Affecting Other Devices
You can debug a single Nallely VirtualDevice instance using pdb by inserting a breakpoint inside that device's thread-specific code, since each VirtualDevice runs in its own Python threading.Thread.
The nallely-midi framework isolates every virtual device in its own execution thread, making it possible to pause and inspect one device while the rest of your MIDI patch continues running. This architecture allows you to use standard Python debugging tools like pdb.set_trace() or breakpoint() to troubleshoot individual device behavior without freezing your entire application.
Understanding Thread Isolation in Nallely
Each VirtualDevice (and subclasses like Sequencer or Clock) executes inside an independent threading.Thread managed by the framework. In nallely/core/virtual_device.py, the VirtualDevice.run() method at line 357 contains the core processing loop that invokes your device's main() method. Because Python's pdb operates at the thread level, inserting a breakpoint anywhere inside this loop suspends only that specific thread, leaving all other device threads unaffected.
The framework provides built-in mechanisms to control this isolation:
VirtualDevice.pause()(line 615): Clears the device's internalpause_event, empties input queues, and stops the run-loop untilresume()is calledself.debug(line 205): A boolean flag initialized toFalsethat enables conditional breakpoints for specific instancesset_pauseproperty: Allows external threads to pause and resume a device programmatically without modifying the device's source code
Using the Debug Flag for Conditional Breakpoints
The VirtualDevice base class includes a debug attribute that you can toggle on a per-instance basis. When device.debug is True, you can guard your breakpoints to ensure they only trigger for the device you are actively investigating.
Place your breakpoint at the start of your device's main() method or within specific event handlers:
from nallely import VirtualDevice, on
from nallely.core.world import ThreadContext
class MySynth(VirtualDevice):
def main(self, ctx: ThreadContext):
if self.debug: # Only breaks when enabled for this instance
import pdb; pdb.set_trace()
# Continue with regular processing...
pass
This pattern ensures that even if you create multiple instances of MySynth, only the one with debug=True will enter the debugger.
Step-by-Step Debugging Strategy
Follow this workflow to isolate and debug a single device in your Nallely patch:
-
Create the device instance and enable debugging:
my_device = MySynth(debug=True) -
Insert conditional breakpoints in your device's
main()method or event handlers usingif self.debug: pdb.set_trace() -
Start the device using
my_device.start()or let the framework auto-start it -
Interact with pdb when the breakpoint triggers—use
n(next) to step through code orc(continue) to resume execution -
Resume normal operation by continuing in pdb; only this device's thread pauses while others process normally
If you need to pause the device from a test script or another thread without modifying the device code, use the set_pause property:
my_device.set_pause = 1 # Pauses only this device
# ... inspect state or modify inputs ...
my_device.set_pause = 0 # Resumes execution
Complete Code Examples
Minimal Example with Conditional Breakpoint
This example demonstrates debugging a simple counter device without affecting the main execution flow:
# examples/debug_one_device.py
from nallely import VirtualDevice
from nallely.core.world import ThreadContext
import time
class Counter(VirtualDevice):
output_cv = VirtualDevice.output_cv
def main(self, ctx: ThreadContext):
if self.debug:
import pdb; pdb.set_trace() # Stops only this thread
self.output = (self.output or 0) + 1
return self.output
if __name__ == "__main__":
counter = Counter(debug=True) # Enable debug for this instance only
counter.start()
time.sleep(2) # Let it run (will hit breakpoint immediately)
counter.stop()
Running this script drops you into pdb after the first cycle, allowing you to inspect self.output while the Python interpreter continues running other threads normally.
Debugging a Device in a Larger Patch
When working with linked devices, you can debug one component while the clock and other devices keep running:
from nallely import Clock, VirtualDevice, on
from nallely.core.world import ThreadContext
import time
class SimpleSynth(VirtualDevice):
note_cv = VirtualDevice.output_cv
@on(Clock.output_cv, edge="rising")
def on_clock(self, value, ctx):
if self.debug:
import pdb; pdb.set_trace()
self.note_cv = int(value * 12) % 128
return self.note_cv
# Create patch
clock = Clock(speed=1.0, autoconnect=True)
synth = SimpleSynth(debug=True) # Debug only the synth
synth.bind(clock)
clock.start()
synth.start()
time.sleep(5) # Clock runs continuously; synth pauses at breakpoint
clock.stop()
synth.stop()
Here the Clock continues ticking while you step through the SimpleSynth logic in pdb.
Pausing from a Test Script
You can pause devices programmatically without adding breakpoint code:
import time
from nallely import Clock, VirtualDevice
class Dummy(VirtualDevice):
def main(self, ctx: ThreadContext):
return None
clock = Clock(speed=2.0, autoconnect=True)
dummy = Dummy()
clock.bind(dummy)
clock.start()
dummy.start()
# Pause only the dummy device externally
dummy.set_pause = 1
time.sleep(1) # Clock runs, dummy is frozen
dummy.set_pause = 0 # Resume dummy
time.sleep(2)
clock.stop()
dummy.stop()
The set_pause property calls pause() and resume() internally, manipulating only that instance's pause_event without touching global interpreter state.
Summary
- Thread isolation in
nallely-midiplaces eachVirtualDevicein its ownthreading.Thread, making single-device debugging possible - Conditional breakpoints using
if self.debug:allow you to target specific instances without affecting others - The
debugflag (defined at line 205 invirtual_device.py) controls which instances trigger pdb breakpoints - Programmatic pausing via
set_pauseorpause()(line 615) stops individual devices from external threads - The
run()method (line 357) serves as the primary insertion point for breakpoints within the device's execution loop
Frequently Asked Questions
Will pdb stop all devices in a Nallely patch?
No. Because each VirtualDevice executes in its own Python thread, pdb.set_trace() suspends only the thread where it is called. Other devices in your patch continue processing MIDI events and updating their outputs normally while you debug the paused instance.
Where should I place breakpoints in a VirtualDevice subclass?
Insert breakpoints at the beginning of your main() method or inside event handlers decorated with @on(). These locations in nallely/core/virtual_device.py run within the device's dedicated thread. Avoid placing breakpoints in __init__ or module-level code, as those run in the main thread and would pause your entire application.
Can I pause a device without modifying its source code?
Yes. Use the set_pause property available on all VirtualDevice instances. Setting device.set_pause = 1 calls the pause() method defined at line 615 of virtual_device.py, which clears the device's internal event loop and stops processing without requiring any changes to the device's implementation.
Does the debug flag affect performance when not in use?
No. The debug attribute defaults to False and checking if self.debug: adds negligible overhead. Since the breakpoint code never executes when the flag is disabled, you can safely leave conditional checks in production code without impacting the real-time performance of your MIDI processing loop.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →