I am writing a device driver in python for a pulse generator. This device is available with either 4 or 8 channels. Without any prior device knowledge, the channel count can be obtained through a command/response transaction.
Each transaction has been implemented as a descriptor. There are two types of transactions: one that interacts with the device without regard to channel and one that only applies to each channel. My first attempts included making a class for channel-based transactions and making instances for each channel. This didn't work since the channel class is already defined and therefore the descriptors it contains are already coded the messages needed. I also looked at factory methods and simply making a dictionary or list with the various channel instances. In every case, I've failed with not being able to define the channel instances with their specific channel numbers before the class or instances are defined. In the end, I've just copied and pasted the channel-based descriptors but this seems to stink pretty bad.
Can anyone suggest a approach to refactor this in a better form? For instance, I would really like to be able to write
for ch in len(channels):
b.channels[ch].enable = 1
rather than
b.channel1_enable = 1
b.channel2_enable = 1
b.channel3_enable = 1
...
My current working code looks like this. Note the copy and paste chunks for each channel near the end. I'm hoping there's a better means of encapsulating the channel-based code.
class Transaction():
def __init__(self, command, name=None):
self.command = command
self.name = name
def __set_name__(self, obj, name):
if not self.name:
self.name = name
def __get__(self, obj, cls):
raise NotImplementedError
def __set__(self, obj, value):
raise NotImplementedError
def __delete__(self, obj):
raise NotImplementedError
class Read(Transaction):
def __get__(self, obj, cls):
return obj.transact(f'{self.command}?')
class ReadWrite(Read):
def __set__(self, obj, value):
return obj.transact(f'{self.command} {value}')
...
number_of_channels = 8
class Pulser:
def __init__(self, interface='COM1', **kwargs):
self.interface = serial.Serial(interface, baudrate=9600)
self.eol = bytes('\r\n', 'utf-8')
def __set_name__(self, obj, name):
self.name = name
def transact(self, command):
self.interface.write(bytes(command, 'utf-8'))
self.interface.write(self.eol)
response = self.interface.read_until(self.eol).decode('utf-8').strip()
return response
@property
def identity(self):
_ = self.idn
idn = _.split(',')
self.manufacturer = idn[0]
self.model = idn[1]
self.number_channels = int(self.model[-1])
self.serial_number = idn[3]
self.firmware_version = idn[3]
# b.channels[1].channel_output_enable = True
return _
idn = Read('*IDN')
settings_label = ReadWrite('*LBL')
list_channel_names = Read(':INST:CAT')
list_channel_numbers = Read(':INST:FULL')
list_commands = Read(':INST:COMM')
display_update = Read(':DISP:UPD')
system_state = Read(':SYST:STAT')
...
channel1_enable = ReadWrite(':PULS1:STAT')
channel1_pulse_width_seconds = ReadWrite(':PULS1:WIDT')
channel1_delay_seconds = ReadWrite(':PULS1:DEL')
channel1_sync_source = ReadWrite(':PULS1:SYNC')
channel2_enable = ReadWrite(':PULS2:STAT')
channel2_pulse_width_seconds = ReadWrite(':PULS2:WIDT')
channel2_delay_seconds = ReadWrite(':PULS2:DEL')
channel2_sync_source = ReadWrite(':PULS2:SYNC')
channel3_enable = ReadWrite(':PULS3:STAT')
channel3_pulse_width_seconds = ReadWrite(':PULS3:WIDT')
channel3_delay_seconds = ReadWrite(':PULS3:DEL')
channel3_sync_source = ReadWrite(':PULS3:SYNC')
...
if number_of_channels > 4:
# more repeats here for channels 5-8
There's no good way to condition the existence of a descriptor on a value of an isntance of your class. That's because the descriptors are set up at class creation time, long before the instance details can be known.
A better approach would be to unconditionally create the decorators, but add logic to (some of) them that checks if they are enabled or not. A callback function is an easy way to do that:
You'd set it up with something like this:
As for the more general part of question about avoiding repeating yourself to set this code up, there's not really a good way to do that with descriptors. They need to be defined in the class body, and it's very awkward to try to write code that manipulates them dynamically. But you could consider abandoning the descriptor approach (at least, at the level you're currently using it) and create a
Channel
class that encapsulates the relevant things you need. Then you can create a list ofChannel
s in the__init__
method of yourPulser
class, and at that point you'd be able to query the device to see how many channels were needed.