[ale] Anyone familiar with Python?

Alex Carver agcarver+ale at acarver.net
Tue Mar 8 22:34:47 EST 2016


Thank's Billy,

The problem with this approach is that not every module works with every
function.  The voltage input module, for example, expects certain
commands to read the inputs.  However, the relay module doesn't
understand that command, it has a different command for setting one of
the relays.

The idea for the class was to ensure that only functions related to a
particular type of module were exposed to the final program because, in
the future, I may not be the only one using this code.  So I'm trying to
make it reasonably easy to use once loaded as a module/library.

Using the instantation examples for my original class:

gauges = dcf.voltage_input(module_address=1)
valves = dcf.relay_output(module_address=2)

This would ensure that a user of the library couldn't do something dumb
like say valves.get_voltage() because it makes no sense.  The module
might ignore the command (or maybe answer with an error message) but
it's entirely possible that the command is reused in another module and
does something different. [1]


The other problem is the nested defs like your voltage_input example.
I don't necessarily want to execute all the commands in a module every
single time because that would tie up the bus and prevent me from
reaching other modules while I wait.  I'd only want to poll a value from
the module on a regular basis and ask for other things once or twice in
a session.

For example, when everything starts up I'll request the configuration of
the module so I know things like input voltage range (this is
adjustable), data format (ASCII text or hex encoded), module name, etc.
 I only need those once.  Then after that I would poll for the voltages
in a loop (along with other polling of other modules).  Using your
example I could break that out as an independent function but that hits
the first problem above.  If it's a nested function then I have to run a
control structure to decide what I want to do and how to route the code
to the right location.  I end up repeating a lot of code.  I did this
for the first brute force code to make sure the modules were functional
but it's very hard to maintain.


The way you wrote your code shows that my choice of calling it
"data_collector" is probably not the best because it's not stand-alone
code that runs in the background.  It's really a driver library so that
a custom test bench program can get data or twiddle outputs as needed
for a particular experimental setup.  It doesn't really run in a loop on
its own.  One day the test bench might do:

while True:
	v1 = get_voltage(module=1, channel=2)
	set_relay(module=3, channel=4, True)
	v2 = get_voltage(module=2, channel=5)
	set_relay(module=3, channel=4, False)	

And another day it might be:

for i in range(0,20):
	v1 = get_voltage(module=1, channel=6)
	if v1 > 5:
		set_relay(module=7, channel=2, True)
	else:
		set_relay(module=4, channel=4, True)


It has to be easy for someone to write up a test module that reads and
writes what it is supposed to do without them having to think "Am I sure
I'm not trying to read a voltage from a relay module?"

I rewrote my class by breaking out the serial portion as its own top
level class and leave the rest as classes and subclasses of a generic
module within the module file and that seems to work well enough.

Now I run this:

import my_class_file

serial_bus = my_class_file.setup_bus(tty="/dev/ttyS7")

valve_relay_module = my_class_file.relay_module_typeA(serial_bus,
module_address=5)

gauge_voltage_module =
my_class_file.voltage_input_module_typeB(serial_bus, module_address=3)

It's close.  I'd eventually like to be able to ditch passing the
serial_bus parameter so I still do the setup_bus function but then the
rest of the classes know about it automatically.  For now it's enough to
make documenting a test module a little easier.  I just have to say
"remember to do this and be sure serial_bus is the first parameter when
setting up the modules".


[1] The reasoning here is that the base command structure is:

$AACDDDD....

Where:
AA is the hex address (transmitted as two ASCII characters) of the
module (so 01, 01, 0E, 0F, etc.),
C is the single command as an ASCII character 0-9 or A-Z, and
DDDD.... is the variable length data payload for a command (could be
zero or more data bytes).

The single command byte only allows for 36 characters.  The company
makes over 20 different types of modules so command reuse is very
likely.  Some commands are the same across all modules.  For example
$01M asks for the name of the module (command M) with address 01.
Others won't have the same data expectations, especially for write
commands versus read queries.


On 2016-03-08 18:35, Billy wrote:
> Hey Ales,
> 
> I'm no expert in Python by any means, so if anyone else here sees a
> problem with my approach, please speak up. But it seems to me that what
> you would want to be doing is creating a single class, and then running
> the different functions within it.
> 
> In code:
> <file is my_data_collector_file>
> class data_collector(object):
>                                                                           
>     serial_port = None
>                                                                           
>     def open_port(self, tty=''):
>         self.serial_port = serial.open(tty)
>                                                                           
>     def write_port(self):
>         pass
>                                                                           
>     def read_port(self):
>         pass
>                                                                           
>     def get_module_name(self):
>         self.write_port()
>         self.read_port()
>                                                                           
>     def voltage_input(self, channel):
>         # This would perform whatever tasks you planned on running for
>         # get_voltage. You could even define whatever functions you
>         # needed in here, and then call them and return their values
>         def get_voltage(channel):
>             # Do what you need to here
>             return self.write_port(channel)
> 
>         def other_function(channel)
>             # Do what you need to here
>             return self.read_port(channel)
> 
>         # Used like this, you get back what you return from your
>         # functions in an array
>         return [get_voltage(channel), other_function(channel)]
>                                                                           
>         # Any variables you may need can be set from here as well, but
>         # it's only necessary to do so if you want to set default values
>         my_variable = 0
> 
>     def relay_output(self, channel):
>         # Other stuff here
> 
> So then when you go to import your module, you would do something like
> this:
> 
> import my_data_collector_file as my_dcf
> 
> dc = my_dcf.data_collector()
> 
> 
> The problem with this bit of code:
> 
> gauges = dcf.voltage_input(module_address=1)
> valves = dcf.relay_output(module_address=2)
> 
> is that you're creating two different
> objects, so any variables stored within one of those objects aren't shared
> with the other.
> 
> By using a single object with those functions within it, you would use
> that object to perform the actions you needed, like so:
> 
> dc.voltage_input(1)
> 
> The variable you set in the class can be accessed here as well as
> modified, so you can do things like 
> 
> dc.voltage_input(dc.my_variable) # my_variable was set to 0 in the class
> 
> dc.my_variable = 2
> 
> dc.voltage_input(dc.my_variable) # my_variable is now 2
> dc.relay_output(dc.my_variable) # my_variable is still 2
> 
> If you did something like having voltage_input return values, you can
> access those by storing them in a variable either in or outside of the
> scope of the object
> 
> my_array = dc.voltage_input(123)
> 
> dc.new_array = dc.voltage_input
> 
> Like I said, I'm no expert, so if anyone sees any errors with what I've
> written, please correct them.
> 
> I hope this helps though!
> 
> On Tue, Mar 08, 2016 at 02:31:36PM -0800, Alex Carver wrote:
>> No, there's no module for these devices.  The company supports their own
>> programming environment (very similar to LabView) and provides the
>> protocol reference so that the devices can be used elsewhere but no code
>> is offered for anything outside their program.
>>
>> This is Python 2.7.  I can't use 3.0 or above because I'm having to
>> integrate this hardware into an existing code base that communicates
>> with even more specialized hardware.  I know there's a few things in the
>> existing code base that won't migrate easily and would require a lot of
>> work to rewrite.
>>
>> On 2016-03-08 14:23, Jay Lozier wrote:
>>> Just curious, but is it possible that there might be Python module
>>> available to do what you want?
>>>
>>> Also, which flavor of Python are you using?
>>>
>>> Jay
>>>
>>>
>>> On 03/08/2016 04:12 PM, Alex Carver wrote:
>>>> Ok, so this should be fun since I've spent the past hour and a half on
>>>> Google and haven't really gotten anywhere.
>>>>
>>>> I'm writing some code to control data collection modules.  Physically,
>>>> all the modules are hanging off an RS485 bus with a single wire to the
>>>> computer.  Each of the modules has a set of very core commands
>>>> (specifically for things like fetching the module name, firmware number,
>>>> and base configuration) and then it has module specific commands that
>>>> vary depending on the type of module (read voltage inputs, set relays on
>>>> outputs, etc.)  All of the modules share the same protocol for comms.
>>>>
>>>> So my original thought was to create a class which contained the very
>>>> bottom stuff, serial I/O/ that would handle the basic serial functions
>>>> and the protocol.  Let's call this data_collectors.
>>>>
>>>> I then thought to make a subclass of data_collectors that contains the
>>>> more specific functions mapped to the protocol commands (e.g. I'd have a
>>>> get_voltage() function for the specific voltage input module and a
>>>> set_relay() function for a relay output module).  The modules each have
>>>> their own address.
>>>>
>>>> So in code form (some pseudo code in here, too for brevity):
>>>>
>>>> <file is my_data_collector_file>
>>>> class data_collector(object):
>>>>
>>>>     serial_port = None
>>>>
>>>>     def open_port(self, tty=''):
>>>>         self.serial_port = serial.open(tty)
>>>>
>>>>     def write_port(...):
>>>>
>>>>     def read_port(...):
>>>>
>>>>     def get_module_name(...):
>>>>         write_port()
>>>>         read_port()
>>>>
>>>> class voltage_input(data_collector):
>>>>     __init__(self, module_address):
>>>>         self.module_address  = module_address
>>>>
>>>>     get_voltage(self, channel):
>>>>         <stuff here including write_port() read_port()>
>>>>
>>>> class relay_output(data_collector):
>>>>     __init__(self, module_address):
>>>>         self.module_address = module_address
>>>>
>>>>
>>>>     set_relay(self, channel, value):
>>>>         <stuff here including value validation, write_port(), read_port()
>>>>
>>>>
>>>> The goal was to be able to import this block of code and then do the
>>>> following (somehow):
>>>>
>>>> import my_data_collector_file as my_dcf
>>>>
>>>> dcf.open_port(tty=...0)  #somehow do this, not sure how
>>>> gauges = dcf.voltage_input(module_address=1)
>>>> valves = dcf.relay_output(module_address=2)
>>>>
>>>>
>>>> Right now, with the same structure I can execute the gauges = and valves
>>>> = lines but they get independent copies of the base class.  I want to
>>>> share the base class (so the port opens and stays open) among all of the
>>>> subclasses but have the subclasses behave individually (since they each
>>>> have their own set of commands that don't overlap).
>>>>
>>>> I haven't figured out if there's a way to accomplish this even if
>>>> there's a third call (as above with the dcf.open_port) to just get
>>>> everything started.




More information about the Ale mailing list