Reading an entire groov module at once with Python

Using the pre-built Python OptoMMP package it’s straightforward to grab individual channels as-needed, even in a loop to grab each channel in a module – but what about reading an entire module at once?

Using the right MMP addresses it’s totally possible.

This post will cover some code I’ve put together to read an entire digital or analog module with just one read command. The tricky part is exactly how to unpack the raw block of data that comes back.

Digital module blocks come back as a 4-byte bitmask (a list of thirty two 1’s and 0’s for “on” or “off” for each channel). On the other hand analog module blocks come back as one byte per channel, where each byte needs to be converted to a floating point value.

Before running the code you will need an internet connection to sudo pip install optommp in order to use the ReadBlock function. You could work around this by opening a socket, creating the hex for the request, then unpacking the response. It’s a bit more involved, but if you want to give it a go, that is described on the developer site guide here: Getting Started with OptoMMP for Python | Opto 22 Developer

Note for RIO EMU: All 64 channels are floating point values, so reading the analog module 0 address will work, but due to a limitation of one of the functions I use it is only possible to read 63 channels at once – if you need 64 just do two separate chunks.


readDigitalModule.py mod num

I’ll explain how to run it, then go through it piece by piece, and will attach a file download (with comments) to this post; but first, here’s the code:

import optommp
import sys
mod = int(sys.argv[1])
num = int(sys.argv[2])
groov = optommp.O22MMP()
address = int("0xF100180" + hex(mod*4)[2:], 0)
raw = groov.ReadBlock(address, 4)[16:]
sortedData = []
for x in raw:
sortedData.insert(0, format(ord(x), '#010b')[:1:-1])
output = ''.join(str(i) for i in sortedData)
for i in range(num):
print(str(i) + " : " + output[i])
groov.close()

How to use it (short version):
To use this program just download the attached file, put it into your groov storage somewhere, navigate to that folder with SSH and type in python readDigitalModule.py #mod #num
Where #mod is the module index and #num is the number of channels to read starting at channel 0, with a maximum of 32 channels. So if I have a 12-channel digital module installed in the second rack slot at index 1, I would call python readDigitalModule.py 1 12
and the script would output a list of channel numbers and their respective bit value, 0 for off/false or 1 for on/true.

How it works (long version):
First, import the functions you’ll need: optommp to connect and read the data from the MMP address and sys to read in the module index and number of channels and output the results. Then set those inputs to variables, both saved as integers.
Once that is set, create a connection to the groov device (defaults to localhost). This handles opening the socket that you’ll be communicating over. Then calculate the address based on the module number; since modules 1, 2, and so on are at offset MMP addresses from the base address (0xF1001800 for module 0).

The next line grabs all the raw data at that block, reading 4 bytes (32 bits) from that address, along with a response header. Simply drop off the first 16 bytes to remove the header since you don’t need it.
After that, re-sort everything since the data comes back in reverse order: the first bit is the 32nd channel, the second is the 31st, and so on. Let’s break that down more specifically:
I do this by looping through all the raw data and grab each byte in the response.
The first thing I need to do is convert it to a number, so I start with the integer representation of the byte with ord(x)
Then, I need to get the individual 1’s and 0’s, so I reformat this integer to binary but make sure I get all 8 bits, even if some of them are leading zeros – for this I use format(.., '#010b')
Once I have the bits, I need to reverse their order, so that instead of “7 6 5 4 3 2 1 0” I have “0 1 2 3 4 5 6 7” in the correct consecutive order. That’s basically a reordering of an array so I can use ..[:1:-1]
Finally I need to push that to the front of my sortedData array, so that instead of “8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7” if I added to the back end, I do have the channels in the correct consecutive order between bytes. To do that I insert each set of bits to the start or “0” position of the array with sortedData.insert(0, ..)

Now I have an array of 4 bytes converted to their full binary representations! To make it easy to grab the channels I need, I can join these bytes together so I have a single string of 32 bits:
output = ''.join(str(i) for i in sortedData)

The last step is to output it; but if I only have a 12-channel module, I only need the first 12 bits in that 32-long array, so I grab that num variable I saved at the start of the script and loop through the output to print that many values.
I chose to list each channel number using str(i) in front of each channel value, which is held in output[i] – so if you just need the results, just refer to the output[i] where i is the channel number.


readAnalogModule.py #mod #num

Like above, below is the code followed by usage instructions and then an in-depth description:

import optommp
import struct
import sys
mod = str(sys.argv[1])
num = int(sys.argv[2])
groov = optommp.O22MMP()
address = int("0xF1008" + mod + "00", 0)
raw = groov.ReadBlock(address, 4*num)[16:]
output = []
i = 0
while(i < 4*num):
arr = raw[i:i+4]
val = struct.unpack_from('>f', bytearray(arr))
output.append(val)
i += 4
for i in range(len(output)):
print(str(i) + " : " + str(output[i])[1:-2])
groov.close()

How to use it (short version):
Similar to readDigitalModule.py, just load it up, navigate to where you have it saved on your groov device, then via SSH enter python readAnalogModule.py #mod #num where #mod is the module index and #num is the number of channels to read starting at channel 0 with a maximum of 63 channels. So if I have a 24-channel analog module installed in the third rack slot at index 2, I would call python readAnalogModule.py 2 24
and the script would output a list of channel numbers and their respective bit value as a floating point number.

How it works (long version):
Again, as above the first thing is to import the required packages with the addition of struct, which is used to unpack the hex bytes into 32-bit floating point values.
Next, the module index and number of channels to read are saved as local variables, the socket connection is opened with optommp.O22MMP(), then the MMP address is calculated based on the module number.

Then we read the block of data, this time getting 4 * num bytes of data, since each float channel is 4 bytes long. Again, we chop off 16 bytes to remove the unused response header.

After that I need to set an output array of values, and start looping through the groups of 4 bytes / 32-bit floats. I just grab each group of bytes, and hand them into the struct.unpack_from function to unpack them as big-endian stored floats with >f as the datatype, and convert the array of hex values to a bytearray so the bytes themselves are handed in, rather than the string of numbers and letters. Finally I append that to the list of output values. Unlike digital values which are stored backwards in their bitmask, these analog values are already stored in consecutive order.

Finally I loop through my output values and print them to the screen, but they are in the form (12.3456789,) so I need to trim of the first and last two characters, which I can do by converting it to a string and using [1:-2] to specify the range of the actual data.
If you only need the numbers, or only specific channels, just do that trimming before you append it to the output and then reference the channels with output[i]


In the attached files I have short comments describing what each pieces does; you can download those *.py scripts with the links below.
If you have any questions or comments, or use these scripts in your own application please leave a post in the thread below,
and as always, happy coding!

python scripts.zip (1.6 KB)

1 Like

Great post! I recently had an implementation that needed this and modified the optommp library to fit my needs. It looks a little different from yours so I thought I would share.

If you want to read different modules and channels all in one request, check out the “Custom Configuration Area” page 144, Form 1465 (the OptoMMP Protcol Guide). You should also have a pretty good understanding of building, sending, and receiving block requests (Form 1465, page 84-87)

The following is the code I used to get 168 data points across 8 modules

Main Script:

import socket
import struct
if __name__ == "__main__":
    CUSTOMREADMEMORYADDRESS = 0xF0D60000            # Custom Memory Address Form 1465 Page 144
    DATAPOINTS = 168
    BYTESPERDATA = 4
    TOTALBYTES = (DATAPOINTS * BYTESPERDATA)
    hostURL = 169.254.192.2

    socketDevice = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socketDevice.connect((hostURL, 2001))

    data = read_block(CUSTOMREADMEMORYADDRESS, TOTALBYTES)
    unpackedData = unpack_read_response(data, "ALL")
    print(unpackedData)

Function: read_block(address, size)

def read_block(address, size):
    tcode = 5              # Read Block Request Code Form 1465 Page 84
    block = [
        0, 0, (0 << 2), (tcode << 4), 0, 0, 255, 255,
        int(str(hex(address))[2:4], 16), int(str(hex(address))[4:6], 16),
        int(str(hex(address))[6:8], 16), int(str(hex(address))[8:10], 16),
        (size >> 8), (size & 0xFF), 0,0]           # Split the size to 2 Bytes (not done in the optommp library)
        
    sendBlock = bytearray(block)
    socketDevice.send(sendBlock )
    return socketDevice.recv(16 + size)      #16 Bytes is the header for the block response

Function: unpack_read_respnse(data, data_type)

def unpack_read_response(data, data_type):
    UNPACKALL = ">iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii\
                 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
    data_block = data[16:]          #16 Bytes is the header for the block response, we don't want it
    if data_type == 'NONE':         # Raw Data
        output = data_block
    elif data_type == 'ALL':        # "All" was the arbitary name I chose
        output = struct.unpack(UNPACKALL, data_block)
    else:            # C-type data_type for struct.unpack 
        raw = struct.unpack_from('>' + data_type, bytearray(data_block))
        output = str(raw)[1:-2]
    return output

There was a lot of information that I had to digest before getting to this point, but hope this is able to help someone.

2 Likes