0

I use a Raspberry Pi to collect sensor data and set digital outputs, to make it easy for other applications to set and get values I'm using a socket server. But I am having some problems finding an elegant way of making all the data available on the socket server without having to write a function for each data type.

Some examples of values and methods I have that I would like to make available on the socket server:

do[2].set_low()    # set digital output 2 low
do[2].value=0      # set digital output 2 low
do[2].toggle()     # toggle digital output 2
di[0].value        # read value for digital input 0
ai[0].value        # read value for analog input 0
ai[0].average      # get the average calculated value for analog input 0
ao[4].value=255    # set analog output 4 to byte value 255
ao[4].percent=100  # set analog output 4 to 100%

I've tried eval() and exec():

self.request.sendall(str.encode(str(eval('item.' + recv_string)) + '\n'))

eval() works unless I am using equal sign (=), but I'm not to happy about the solution because of dangers involved. exec() does the work but does not return any value, also dangerous.

I've also tried getattr():

recv_string = bytes.decode(self.data).lower().split(';')
values = getattr(item, recv_string[0])
self.request.sendall(str.encode(str(values[int(recv_string[1])].value) + '\n'))
                                                                ^^^^^

This works for getting my attributes, and the above example works for getting the value of the attribute I am getting with getattr(). But I can not figure out how to use getattr() on the value attribute as well.

The semi-colon (;) is used to split the incoming command, I've experimented with multiple ways of formatting the commands:

# unit means that I want to talk to a I/O interface module,
# and the name specified which one

unit;unit_name;get;do;1
unit;unit_name;get;do[1]
unit;unit_name;do[1].value

I am free to choose the format since I am also writing the software that uses these commands. I have not yet found a good format which covers all my needs.

Any suggestions how I can write an elegant way of accessing and returning the data above? Preferably with having to add new methods to the socket server every time a new value type or method is added to my I/O ports.

Edit: This is not public, it's only available on my LAN.

Thomas Jensen
  • 2,138
  • 2
  • 25
  • 48
  • If this is anything public, prepare for a security nightmare. – orlp Jan 08 '15 at 00:03
  • Yeah, but it's not. It's only used for my automation system on the local network. – Thomas Jensen Jan 08 '15 at 00:05
  • Where do the `;` come from? What is the structure of `self.data` or `recv_string`? Does it look something like your commands, e.g. `do[2].value=0` – Caleb Hattingh Jan 08 '15 at 00:23
  • Also, if you make all your operations on IO ports *methods*, then you could use eval everywhere. Specifically, instead of assignments `do[2].value=0`, rather make your API like `do[2].setvalue(0)`. You could make such methods return bools, if there was a chance of an assignment not succeeding for example. But I don't have enough details about the specific use case. – Caleb Hattingh Jan 08 '15 at 00:26
  • Another idea: just make them all simple methods of the form `def value(newValue=None)` which always returns the current value, and will change the value if `newValue` is not `None`. This will make it simpler to translate incoming messages; provide the new value if given, otherwise leave it out. – Caleb Hattingh Jan 08 '15 at 00:29
  • I have updated my question with information regarding the semi-colon. Every change to an I/O port will return the new value of the port. Thanks for your input, I've been thinking along those lines myself. I've read some bad things about the performance of `eval()`, but might not be a problem for me. My I/O units use around 10ms to respond to a request, so it's not lightning fast anyways. – Thomas Jensen Jan 08 '15 at 00:36
  • Answer submitted. I can't imagine the speed of eval will be an issue for you because you already have the cost of network IO. – Caleb Hattingh Jan 08 '15 at 00:51

1 Answers1

1

Suggestions

Make your API all methods so that eval can always be used:

def value_m(self, newValue=None):
    if newValue is not None:
        self.value = newValue
    return self.value

Then you can always do

result = str(eval(message))
self.request.sendall(str.encode(result + '\n'))

For your message, I would suggest that your messages are formatted to include the exact syntax of the command exactly so that it can be evaled as-is, e.g.

message = 'do[1].value_m()'  # read a value, alternatively...
message = 'do[1].value_m(None)' 

or to write

message = 'do[1].value_m(0)'  # write a value

This will make it easy to keep your messages up-to-date with your API, because they must match exactly, you won't have a second DSL to deal with. You really don't want to have to maintain a second API, on top of your IO one.

This is a very simple scheme, suitable for a home project. I would suggest some error handling in evaluation, like so:

import traceback

try:
    result = str(eval(message))
except Exception:
    result = traceback.format_exc()
self.request.sendall(str.encode(result + '\n'))

This way your caller will receive a printout of the exception traceback in the returned message. This will make it much, much easier to debug bad calls.

NOTE If this is public-facing, you cannot do this. All input must be sanitised. You will have to parse each instruction and compare it to the list of available (and desirable) commands, and verify input validity and validity ranges for everything. For such a scenario you are better off simply using one of the input validation systems used for web services, where this problem receives a great deal of attention.

Caleb Hattingh
  • 9,005
  • 2
  • 31
  • 44