Python

Python loads the shared library from the AMMC zip directly. The zip also contains lib_test.py, a working script that does exactly what is below.

pip install cffi
from cffi import FFI

ffi = FFI()
ffi.cdef("""
    char *p3_to_json(const char *msg);
    void p3_free_string(char *ptr);
""")

lib = "windows64\\libammc.dll"
print(f"Trying to open AMMC lib from path: {lib}")
C = ffi.dlopen(lib)
p3_msg = "8e023300e5630000010001047a00000003041fd855000408589514394cd8040005026d0006025000080200008104501304008f"
result = C.p3_to_json(p3_msg.encode('ascii'))
assert result != ffi.NULL
json = ffi.string(result).decode('ascii')
C.p3_free_string(result)   # the caller owns the string and must free it
assert 'PASSING' in json

Use linux_x86-64/libammc.so or apple_m/libammc.dylib instead of the .dll on other platforms.

A small helper

Wrapping the two calls keeps the free where it belongs and gives you real Python objects:

import json as jsonlib
from cffi import FFI

ffi = FFI()
ffi.cdef("""
    char *p3_to_json(const char *msg);
    void  p3_free_string(char *ptr);
""")
C = ffi.dlopen("apple_m/libammc.dylib")


def to_json(p3_hex: str) -> list:
    """Converts hex-encoded decoder bytes into a list of messages."""
    result = C.p3_to_json(p3_hex.encode("ascii"))
    if result == ffi.NULL:
        return []
    try:
        return jsonlib.loads(ffi.string(result).decode("ascii"))
    finally:
        C.p3_free_string(result)


for passing in to_json("8e023300e5630000010001047a000000030"
                       "41fd855000408589514394cd80400050"
                       "26d0006025000080200008104501304008f"):
    print(passing["passing_number"], passing["transponder"], passing["rtc_time"])
122 5625887 2013-03-19 21:36:33.607 +02:00

The try/finally matters: without p3_free_string every converted message leaks, which adds up quickly on a busy timing loop.

Reading a decoder

The library does no networking, so the socket is yours:

import socket

sock = socket.create_connection(("10.0.11.10", 5403))
while True:
    data = sock.recv(4096)
    if not data:
        break
    for passing in to_json(data.hex()):
        print(passing)

Each call parses independently and keeps nothing between calls, so a message split across two recv calls is lost by both. If you need every passing without gaps, do not parse the socket yourself — run ammc-amb -w 9000 and read its WebSocket, which handles the framing for you.