Specific OptoMMP Addresses with C++ (serial number, MAC address, and analog scan time)

If you ever need some values that are not connected to the rest API, you may want to turn to the Opto Memory Mapped Protocol, OptoMMP, and look at the values exactly where they’re stored.
(All supported REST endpoints are listed in the references on the developer site)

In this post I’ll look at three memory mapped addresses that cover three very different pieces of data. I’ll show how to read them in, which functions I used, and how I chose to consume and process this data:

0xF030014C = device serial number (standard 32-bit integer)
0xF0300140 = analog scan time (float / decimal number)
0xF030002E = MAC address (64-bit unsigned integer to be converted to a string)

If you want to know where I found these addresses, the data types, or more information about how they’re decoded please check out the OptoMMP Protocol Guide, and if you’re well-versed C++ you may benifit from checking out the C++ toolkit source code. The entirety of this post is thanks to those two downloads (and a LOT of Googling).

Disclaimer: I am not an expert when it comes to coding in C++, but I’m always eager to learn more and I’d love to improve these sample programs. If you know of any ways to improve the code I use below, please post it in this thread!


Getting a groov Serial Number

I’ll start with the easier of the three and have a look at getting the serial number at 0xF030014C. Since this is a standard integer value it’s pretty straightforward to unpack – there’s even a built-in function with the OptoMMP toolkit to do it:

int ReadBlockAsIntegers(uint32_t dwDestOffset, uint16_t wDataLength, int * pnData)

I’ll quickly go through this first line so it’s clear what we’re working with.
This function “ReadBlockAsIntegers” is a function that returns an integer (shown by the first int) with a status value, and it requires the OptoMMP address (dwDestOffset), the amount of data to fetch (wDataLength), and a variable to put it in (* pnData). It’s also important to match datatypes, as you will see in the sample code below.
The source code for this function can be found in the OptoMMP C++ toolkit: https://www.opto22.com/products/pac-dev-optommp-cplus)

If we call this function with the OptoMMP address for the serial number, we’ll get back an integer number in the pnData array at the starting index - pnData[0] – that’s what we’re looking for.
Here’s what the main code looks like (you’ll need some other setup lines for this to run):

uint32_t MMP = 0xF030014C; // MMP address for the serial number
uint16_t length = sizeof(int); // reading one integer
int data[1]; // array to hold the single returned number
int nResult = EPIC.ReadBlockAsIntegers(MMP, length, data);
int serialNumber = data[0];

nResult is a number that says whether the function call was successful or not – in critical applications you can run a check on this to make sure it worked before running further code. I’ll skip that for this example.

After getting that serialNumber from data[0] I can now do whatever I want with it. In the sample code attached here I just print it to the console, but you can use this code however you like!
getSerialNumber.zip (70.7 KB)


Getting Analog Scanner Scan Time

The next function is pretty similar, except instead of reading the device serial number, it fetches the analog scanner scan time as a floating point / decimal number.

int ReadFloat(uint32_t dwDestOffset, float * pfValue);

As with the function above, it’s important to match your datatypes, which is shown in the sample code below. Again we have the MMP address, and a pointer to the variable that should hold the response.

uint32_t MMP = 0xF0300140; // MMP address for the analog scanner time
float value; // result value to hold the analog scanner time
int nResult = EPIC.ReadFloat(MMP, &value);

In this case you can just refer to value as a float in the following code and it will hold the decimal number of milliseconds it takes for the analog scanner to run through its cycle.
Check out the sample code attached here if you want to try it with your own system:
getAnalogScanTime.zip (70.6 KB)


Getting (and decoding) MAC Address

Finally, we have the tricky one… the MAC address.
If you just want to use the code, I’ll link it right here, but if you’re interested about how it works, keep reading.
MAC Address TL;DR - The incoming value is an unsigned 64-bit integer, but we’re used to a format more like AA:00:22:00:XX:YY and that has letters and punctuation, not just numbers… so there’s a lot of converting… here’s the code: getMACAddress.zip (84.7 KB)

. . .

So how does it work?

First, I read in the raw 64 bits (8 bytes) that should fill an unsigned 64-bit integer, then take the 2’s compliment of those bytes, then convert that number to it’s hex string. (Stack Overflow links will be at the end of the post . . . . )

Everything I do after that is formatting turn line 1 into line 2:
1> 0xa03d043aeac0a8
2> 00:A0:3D:04:3A:EA
I’ll only post the first part that collects and specifies the MAC address 00a03d043aea, the rest of the formatting is optional, and can be found in the code attached below this code:

uint32_t MMP = 0xF030002E; // address for the MAC address
int length = 8; // MAC address is 64 bits = 8 bytes
uint8_t response[8]; // variable to hold the eight bytes
int nResult = EPIC.ReadBytes(MMP, length, response);
// convert the 8 bytes into an unsigned, 2's compliment 64-bit integer
uint64_t value = ((uint64_t)response[0] << 56 |
            (uint64_t)response[1] << 48 |
            (uint64_t)response[2] << 40 |
            (uint64_t)response[3] << 32 |
            (uint64_t)response[4] << 24 |
            (uint64_t)response[5] << 16 |
            (uint64_t)response[6] << 8 |
            (uint64_t)response[7]);
std::stringstream buffer; // buffer to hold the number represented as a hexidecimal string
buffer << std::setw(16) << std::setfill('0') << std::hex << value;
std::string hexString = buffer.str();
hexString = hexString.substr(0,12);

The full source code and program you can either compile or run yourself are in this zip:
getMACAddress.zip (84.7 KB)


– If you want to run these programs, please read this! –

In order to run these attached programs you will need both secure shell access (GROOV-LIC-SHELL) and the root user password. To run these programs just type ./getSerialNumber in the folder where the file is held, but first you will need to make the file itself executable by running sudo chmod +x getSerialNumber otherwise it will not work.
As always with shell access, proceed with caution, and respect sudo!


With all that said, here are my Stack Overflow sources for those that want to dig a bit deeper into these details, and as always I welcome feedback and improvements on this code.
If you end up using this, I’d love to know where, and if you see any issues, please point them out in the thread below. And as always - happy coding!


Regarding the MAC Address portion, there are a few issues:

  • A MAC address is a 48-bit number, not a 64-bit one. (e.g. 6 bytes)
  • You’re starting your read from the MAC address entry in the memory map, but reading 8 bytes means you’re going to grab the 6 bytes of MAC address plus the first 2 bytes of the IP address that immediately follows, which may taint your result.
  • The way you pack the resulting bytes into a 64-bit integer is endian dependent, so you may get different results depending on what platform you compile and run this code.

I’d approach it more like this:

uint32_t MMP = 0xF030002E; // address for the MAC address
int length = 6; // MAC address is 48 bits = 6 bytes
uint8_t response[6]; // variable to hold the six bytes
int nResult = EPIC.ReadBytes(MMP, length, response);

char hexString[18] = {0}; // 6 bytes at 2 characters each, plus 5 colons between them, plus a null terminator, initialized to 0
snprintf(hexString, 18, "%02x:%02x:%02x:%02x:%02x:%02x", response[0], response[1], response[2], response[3], response[4], response[5]);

I should also note: you don’t have to use the OptoMMP C++ Toolkit. It’s a really straightforward protocol, and the protocol guide has all the details you’d need to use it.

Fun little example / tidbit, groov Find for Mac issues a Block Read starting at offset 0xF0300020 (Status Read Area, Unit Type) and reads 304 bytes in one chunk, and then decodes it like this:

- (O22DiscoveredDevice *)makeDeviceWithData:(NSData *)deviceData address:(struct sockaddr_storage *)address {
    char nameBuffer[1024] = { 0 };

    // First 4 bytes of the response is a unit id in network order.
    uint32_t unitId = [deviceData o22_bigUInt32AtIndex:0];

    // The AR1 and AT1 repurpose the "Number of bytes of installed RAM" field as a serial number.
    uint32_t serialNumber;
    if (unitId == O22DeviceIdentifierGROOV_AT1 || unitId == O22DeviceIdentifierGROOV_AR1) {
        serialNumber = [deviceData o22_bigUInt32AtIndex:8];
    }
    else {
        serialNumber = [deviceData o22_bigUInt32AtIndex:300];
    }

    // The MAC address of the first interface starts at offset 14 in the device data.
    NSString *macAddress = [NSString stringWithFormat:@"%02x-%02x-%02x-%02x-%02x-%02x",
                            [deviceData o22_byteAtIndex:14],
                            [deviceData o22_byteAtIndex:15],
                            [deviceData o22_byteAtIndex:16],
                            [deviceData o22_byteAtIndex:17],
                            [deviceData o22_byteAtIndex:18],
                            [deviceData o22_byteAtIndex:19]];

    // The part number is up to 20 bytes long, starting at offset 96
    char partNumberStr[20];
    memmove(partNumberStr, deviceData.bytes + 96, 20);
    NSString *partNumber = [[NSString alloc] initWithBytes:partNumberStr length:safe_strlen(partNumberStr, 20) encoding:NSASCIIStringEncoding];

    // Annoyingly, the GROOV-AT1 and GROOV-AR1 add quotation marks to their names, so
    // those need to be trimmed off.
    if ([partNumber hasPrefix:@"\""]) {
        partNumber = [partNumber substringWithRange:NSMakeRange(1, partNumber.length - 2)];
    }

    // Use inet_ntop to turn that sockaddr_storage into a human readable address.
    NSString *ipAddress;
    if (address->ss_family == AF_INET6) {
        struct sockaddr_in6 *addr = (struct sockaddr_in6 *)address;
        inet_ntop(AF_INET6, &addr->sin6_addr, nameBuffer, 1024);
        ipAddress = [NSString stringWithCString:nameBuffer encoding:NSASCIIStringEncoding];
    } else {
        struct sockaddr_in *addr = (struct sockaddr_in *)address;
        inet_ntop(AF_INET, &addr->sin_addr, nameBuffer, 1024);
        ipAddress = [NSString stringWithCString:nameBuffer encoding:NSASCIIStringEncoding];
    }

    return [[O22DiscoveredDevice alloc] initWithUnitId:unitId
                                          serialNumber:serialNumber
                                            partNumber:partNumber
                                            macAddress:macAddress
                                        networkAddress:*address
                                             ipAddress:ipAddress
                                              hostname:nil
                                      lastResponseTime:[NSDate date]];
}

(The GROOV-AT1 and GROOV-AR1 don’t implement the full memory map by any means, they just have a tiny little MMP responder to help with device discovery.)