EPIC data using OptoMMP with Python

As well as using the groov Manage REST API, you can also access groov EPIC data through Opto’s Memory-Mapped Protocol (OptoMMP). OptoMMP can access both system information and physical I/O points with packages of bytes that follow the IEEE 1394 standard format, which essentially means they follow a strict but consistent structure.

Beyond being able to open a socket to port 2001 on the controller, there aren’t many system or software requirements to use OptoMMP, so even Python scripts can make powerful control commands with the right packages – the hard part is building the package. Once the package is built, it is sent through the socket, a response is received, unpacked, and printed to the console to give read result or write success/failure.

To figure out which package to use and what it looks like, refer to the OptoMMP Protocol Manual. This document goes deep into the details of the protocol and how to use it. The Overview of Custom Application Programming section should be especially handy, that’s where you’ll find the binary breakdown of every package type.
Using the manual and taking some inspiration from the C++ SDK for OptoMMP I built three Python example scripts that can check a controller up-time, toggle a digital output, and read a digital output, all using OptoMMP.

Basic instructions are commented at the top of each script, but for more details on how these scripts were written or how the memory map packages are built, check out this developer tutorial.

Happy coding!

Scripts:

getUptime.py

# >>python getUptime.py OR >>python getUptime.py 127.0.0.1
import sys
import socket
import struct
if len(sys.argv) < 2:   # if an arguement isn't included
    host = 'localhost'  # default to localhost
else:
    host = sys.argv[1]  # otherwise use the first argument

port = 2001 # default OptoMMP port number
# create socket with IPv4 family, and TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# use that socket connect to host:port tuple
s.connect((host, port))
# build uptime block request:                 F0  30  01  0C    <- uptime location in hex
myBytes = [0, 0, 4, 80,  0,  0,  255, 255,   240, 48,  1, 12,     0, 4, 0,0]

# send request and save the response
nSent = s.send(bytearray(myBytes)) # want nSent to be exactly 16 bytes
data = s.recv(24) # read response block is 24 bytes
data_block = data[16:20] # data_block is in bytes 16-19 for Read Response, stop at 20.

# decode bytearray in big-endian order (>) for integer value (i)
output = str(struct.unpack_from('>i', bytearray(data_block)))
# clip out first `(` and last two characters `,)` before printing
print 'uptime: ' + output[1:-2] + 'ms'
# close the socket:
s.close()

readModCh.py

#    readModCh.py >>python readModCh.py <module #> <channel #>
import sys
import socket
import struct

host = '127.0.0.1' # groov EPIC IP

if(len(sys.argv) != 3): # If the module and/or channel are not provided.
    print 'Please provide module # and channel #.'
    print 'Exiting script . . .'
    exit() # Inform the user and exit the script.

port = 2001 # default OptoMMP port number
tcode = 5   # read block request
modN = int(sys.argv[1]) # first argument
chN = int(sys.argv[2])  # second argument

# create socket with IPv4 family, and TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# use that socket connect to host:port tuple
s.connect((host, port))
# Calculate the destination offset:
# EPIC digital read start address = 0xF01E0000
dest = 0xF01E0000 + (modN * 0x1000) + (chN * 0x40)
# build the read block request:
myBytes = [0, 0, (1 << 2), (tcode << 4), 0, 0, 255, 255, int(str(hex(dest))[2:4],16), int(str(hex(dest))[4:6],16), int(str(hex(dest))[6
:8],16), int(str(hex(dest))[8:10],16), 0, 4, 0, 0];
# send the read block request and save the response:
nSent = s.send(bytearray(myBytes)) # want nSent to be exactly 16 bytes
data = s.recv(20) # read block response is 16 + 4 bytes
data_block = data[16:20] # data_block is in bytes 16-19 for Read Response, stop at 20.

# decode bytearray in big-endian order (>) for integer value (i)
output = str(struct.unpack_from('>i', bytearray(data_block)))
# clip out first `(` and last two characters `,)` before printing
print 'module ' + str(modN) + ', point ' + str(chN) + ' = ' + output[1:-2]
#close the socket:
s.close()

writeModChVal.py

#    writeModChVal.py >>python writeModChVal.py <module #> <channel #> <1|0>
import sys
import socket
import struct

host = '127.0.0.1' # groov EPIC IP

if(len(sys.argv) != 4): # If the module, channel, and/or value are not provided.
        print 'Please provide module #, channel #, and value [1|0].'
        print 'Exiting script . . .'
        exit() # Inform the user and exit the script.

port = 2001 # default OptoMMP port number
tcode = 1   # write block request
modN = int(sys.argv[1]) # first argument
chN = int(sys.argv[2])  # second argument
val = int(sys.argv[3])  # third argument

# create socket with IPv4 family, and TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# use that socket connect to host:port tuple
s.connect((host, port))
# Calculate the destination offset:
# EPIC digital write start address = 0xF0220000
dest = 0xF0220000 + (modN * 0x1000) + (chN * 0x40)
# build the write block request: 
myBytes = [0, 0, (1 << 2), (tcode << 4), 0, 0, 255, 255, int(str(hex(dest))[2:4],16), int(str(hex(dest))[4:6],16), int(str(hex(dest))[6:8],16), int(str(hex(dest))[8:10],16), 0,16,  0,0,  0,0,0,val] + [0]*12;
# + [0]*12 to fill the write block and make it the correct size.

# send the write block request and save the response:
nSent = s.send(bytearray(myBytes)) # want nSent to be exactly 32 bytes
data = s.recv(12) # write block response is 12 bytes
data_block = data[4:8] # data_block is in bytes 4-7 for Write Response, stop at 8.

# decode bytearray in big-endian order(>) for integer value (i)
output = str(struct.unpack_from('>i', bytearray(data_block)))
# clip out first `(` and last two characters `,)` to get the status code number
status = int(output[1:-2])
if (status == 0):
    print 'Write success ' + str(status)
else:
    print 'Write failure ' + str(status)
#close the socket:
s.close()

Happy coding!

Any CLI code or Python code that writes data to a variable/memory of Groov EPIC that can be retrieve by Strategy?

Yes.
Lots of examples here; Getting Started with Python for groov EPIC | Opto 22 Developer

1 Like

Um, I am going circles clicking on those links on developer page.

I am following demo.py
I get error, during import:
image

But looks like there exist O22SIOUT.py

also on the following line, how do you tell python which remote EPIC to access?
grvEpic = optommp.O22MMP(<???>)

Hi Eugene, since you’re using the Python library you’ll want to import optommp, not O22SIOUT – that’s just one of the package files.
The full instructions can be found here: Downloading and Using the Pre-Built Python Package | Opto 22 Developer
I’ll go back through the developer pages and try to make that link easier to find – thanks for your feedback.

Regarding the command usage, you can find those details under the “included functions” on the developer site, or in the Github readme here: GitHub - optodeveloper/optommp: Python toolkit for Opto 22 memory-mapped devices.

1 Like

Thank you guys.

I am using windows.
The code below got it working for me, but I need to edit optommp.py module to
“import O22SIOUT” to “from . import O22SIOUT”
otherwise it will return Module not found error.

import optommp

print("start")

grvEpic = optommp.O22MMP('10.200.14.3')

grvEpic.SetScratchPadIntegerArea(10, 72)

grvEpic.close()

print("end")
2 Likes

Hi Torchard
I have a problem with OptoMMP, all the commands work for me except GetScratchPadStringArea, I think there is something to correct in the library, has someone solved this?

Hi @nextcontrol, what error are you getting when you run it? and what string content / length are you seeing the issue with? I haven’t had any issues with the scratchpad on my end, but I might need to reproduce the same conditions as you before I can see it.

If there is anything to correct I’ll dig it up and fix it as soon as possible, I just want to nail down what the problem is, I’d appreciate any extra info you have on your setup.

Hi Torchard,
I get the following error when trying GetScratchPadStringArea(0)
The other commands (ScratchPad) work fine.

Hi @nextcontrol,

I had this same problem with GetScratchPadStringArea(). The problem appears to be this line in init.py:

size = ord(rawSize) if len(rawSize)==1 else int('0'+rawSize[1:], 16)

The problem seems to be converting the string from a bytes object to an int. I solved this by taking out the int conversion and instead taking the length of the object. My solution is not necessarily perfect because the length of the bytes object is not the same size as the length of the string so I had to add some padding to the length to make sure the whole string gets through.

Here is my solution, ( I also spaced it out on multiple lines for readability):

# rawSize is char `c` *or* a hex `/x00` number. Convert to int:
        if len(rawSize)==1: 
            size = ord(rawSize) 
        else: 
            size = len('0'+rawSize[1:]) + 50 
        # Note: string size is at offset + 1, string data is at offset + 2

I hope this helps and definitely welcome a more elegant and full solution to this problem.

Cheers!

1 Like

Thankyou for sharing your solution, I’ll go back and review the package to find the best place to fix this – your code does look like a great way to handle it.