Go back to Getting Started with OptoMMP for Python

About OptoMMP Data Packages

OptoMMP is a binary-based protocol that requires precision in every package it deals with. As a result, you should know the format of the package you want before you begin, and that starts with the transaction code.

The transaction code is determined by whether you are reading or writing to the memory location, and also whether you want to work with 4-byte ‘quadlets’ or N-byte ‘blocks’.
Use these attributes to choose one of the following seven package types (note that there is no code 3):

Package Type Code Binary Package Size
Write Quadlet Request 0 0000 16 bytes
Write Block Request 1 0001 24 bytes
Write Quadlet or Block Response 2 0010 12 bytes
Read Quadlet Request 4 0100 12 bytes
Read Block Request 5 0101 16 bytes
Read Quadlet Response 6 0110 16 bytes
Read Block Response 7 0111 24 bytes

Examining a Read Block Request

Once the transaction code is determined, follow the specific syntax for that package type as described in the section on “Overview of Custom Application Programming” in the OptoMMP Protocol Guide (form 1465). Here is a snippet of that guide showing one of the bit-by-bit breakdowns, specifically the Read Block Request package structure:

The greyed out areas represent unused bits: destination_ID, source_ID, and extended_tcode are entirely unused bytes that can have all eight bits set to zero. However, the retry code rt takes up two unused bits that share the rest of their byte with transaction label tl, for the requester to track transactions between applications. Similarly priority pri takes up four unused bits of the transaction code tcode byte, which determines the package type as described above. To handle these unused areas, just set the unused bytes to zero, and bitshift both the transaction code and transaction label to make up for the unused bits.

  • Transaction label should be bitshifted by two (Python operation << 2)
    So that a label of 1 = 0000 0001 becomes 4 = 0000 0100

  • Transaction code should be bitshifted by four (Python operation << 4)
    So that a code of 5 = 0000 0101 becomes 80 = 0101 0000


Destination Offset

The instructional content of the package is wrapped in the destination offset (and, if making a write request, in the data block). This offset is the specific address location that you want to access, where some addresses are fixed; for example, system uptime (ms) at 0xF030 010C. You can use PAC Manager to see all fixed addresses, most of which are system values like uptime or firmware details. However, the addresses of physical digital and analog points aren’t fixed; their addresses depend on the I/O module you’re reading.
You can use this formula to calculate the addresses of the physical points on the installed I/O modules:

offset = start_address + (module_no * mod_length) + (channel_no * ch_length)

… where start_address and each length are constants, based on the type of area being accessed. For example, the EPIC digital channel read area starts at 0xF01E 0000, has a module length of 0x1000, and a channel length of 0x40. For example the address to read the digital point at channel 5 of module 1 on an EPIC chassis is:

offset = 0xF01E0000 + (1 * 0x00001000) + (5 * 0x00000040)

… giving a result of 0xF01E1140. You will need to find the specific constant values for starting addresses and lengths in the reference material linked at the end of this tutorial.

Now that this memory address has been found or built, it must be stored across the four bytes 8–11 of the read block request as separate bytes: F0, 1E, 11, and 40. Let’s look at that process next.


Byte Array

Python is a high-level language that was not designed for memory-level operations, so it does lack some of the bit- and byte-level manipulation tools and types that would be helpful when working with OptoMMP. To get around this, we’ll manage the exact bytes of the package using standard character arrays, string slicing, and some back-and-forth type conversion.

  1. Python treats the above “offset” variable like a decimal number, so the first step is to get the hexadecimal representation of that number back:

    hex(offset) # == 0xF01E1140

  2. Next, we need to be able to reference each byte of that number. We can do that by converting this hexadecimal representation into a string and slicing it after 0x:

    str( hex(offset) )[2:4] # == "F0"

  3. Finally, convert from a string of characters back into an actual number for Python to store in the array package to be sent, using base 16 (hexadecimal) encoding:

    int( str(hex(offset))[2:4], 16 ) # == 240 (decimal value of F0)

  4. Repeat this convert-slice-convert process for all four bytes and put them in the array:

    [... int(str(hex(offset))[2:4],16), int(str(hex(offset))[4:6],16), ..., int(str(hex(offset))[8:10]) ...]

… after which bytes 8-11 of the array will hold: [… 240, 30, 17, 64 …], or in hex: [… F0, 1E, 11, 40 …], which is precisely what we should send for this specific offset.

Using this method, offset can be determined dynamically through argument values and then systematically broken down into bytes to be packaged up and sent to the controller. Even the constants for the memory starting point and the module and channel lengths can be determined by run-time parameters rather than by hard-coded hex values.


Data Package

Now all of these pieces go together into that larger data package pictured above, I'll be storing the pieces in a standard array that will need exactly 16 entries because a Read Block Request package is 16 bytes. To help see what goes where, we can build a rough package map (specifically for this Read Block Request) to see the property associated with each entry.

Before you examine the array, take note of a few general characteristics:

  • No entry is larger than 1111 1111 = 0xFF = 255 (the maximum value of one byte).
  • The first two bytes of the destination offset are both 0xFF, it’s the last four bytes vary – they are the offset.
  • The size of the array should match the size of the request package, for example Write Block Requests are 20 bytes and Read Quadlet Requests are 12 bytes, so their arrays should have 20 and 12 entries respectively.
  • If you are sending a write request, you should fill the entire block using zeros for any unused space. For example, append + [0] * 12 if only writing four bits (one byte).
    #[destination_ID        shifted transaction details     source_id   destination
    [          0, 0,       (t_label << 2), (t_code << 4),      0, 0,       255,255,

    #    destination offset byte-by-byte, use int(str(hex(offset))[a:b],16) as above
        int(str(..)[2:4]),  int(str(..)[4:6]),  int(str(..)[6:8]),  int(str(..)[8:10]),

    #    data length		extended t_code                 = property
            0,16,                   0,0]                        = array value

Here’s a simplified example without the property notation:

myBytes = [ 0, 0, (t_label << 2), (t_code << 4), 0, 0, 255, 255,            # bytes 0-7
    offset[2:4], offset[4:6], offset[6:8], offset[8:10], 0, 16, 0, 0];      # bytes 8-15


Next Step

Continue with Sending and decoding OptoMMP data

Or go back to Getting Started with OptoMMP for Python

Or check out the prebuilt Python package for groov EPIC.