Search code examples
python-3.xctypes

Python3 processing ctypes data


I am connecting to a fanuc cnc machine with Python using a C library. I learned C++ about 20 years ago (haven't used it since) and am not super strong in Python so I'm struggling with data types.

I have data coming from the c library in the following format

data structure

I am using the following code to read it:

cnc_ids = (ctypes.c_uint32 * 11)()
ret = focas.cnc_rddynamic2(libh, 1, 44, cnc_ids)

if ret != 0:
    raise Exception(f"Failed to read cnc id! ({ret})")

for i in range(11): 
    print(f"{i}: {cnc_ids[i]}")

If all of the pieces of data were a length of 4 bytes this would be easy, but the first two are only 2 bytes each.

I imagine I could just split the first 4 byte group into two with some extra code, but I will be interfacing about 300 different functions, all with similar structures so it will be a recurring issue. Some of these functions will have several different lengths of data that need to be processed.

What is the best way to process this data?

My end goal is to output the data in a json format that will be used for an api within flask.

Additional info - The typedef is also provided. If there is just a way I can use that directly that would be awesome. Below is the example they give for this function.

typedef struct odbdy2 {
    short  dummy ;     /* not used                */
    short  axis ;      /* axis number             */
    long   alarm ;     /* alarm status            */
    long   prgnum ;    /* current program number  */
    long   prgmnum ;   /* main program number     */
    long   seqnum ;    /* current sequence number */
    long   actf ;      /* actual feedrate         */
    long   acts ;      /* actual spindle speed    */
    union {
        struct {
            long  absolute[MAX_AXIS] ; /* absolute */
            long  machine[MAX_AXIS] ;  /* machine  */
            long  relative[MAX_AXIS] ; /* relative */
            long  distance[MAX_AXIS] ; /* distance to go */
        } faxis ; /* In case of all axes */
        struct {
            long  absolute ; /* absolute */
            long  machine ;  /* machine  */
            long  relative ; /* relative */
            long  distance ; /* distance to go */
        } oaxis ; /* In case of 1 axis */
    } pos ;
} ODBDY2 ;            /* MAX_AXIS is the maximum controlled axes. */

Solution

  • With ctypes, the structure can be declared and used directly. If you receive the data as a buffer of 32-bit values, you can cast that buffer into the structure as shown below:

    import ctypes as ct
    
    MAX_AXIS = 3  # Not provided by OP, guess...
    
    class FAXIS(ct.Structure):
        _fields_ = (('absolute', ct.c_long * MAX_AXIS),
                    ('machine', ct.c_long * MAX_AXIS),
                    ('relative', ct.c_long * MAX_AXIS),
                    ('distance', ct.c_long * MAX_AXIS))
    
        def __repr__(self):
            return f'FAXIS({list(self.absolute)}, {list(self.machine)}, {list(self.relative)}, {list(self.distance)})'
    
    class OAXIS(ct.Structure):
        _fields_ = (('absolute', ct.c_long),
                    ('machine', ct.c_long),
                    ('relative', ct.c_long),
                    ('distance', ct.c_long))
    
        def __repr__(self):
            return f'OAXIS({self.absolute}, {self.machine}, {self.relative}, {self.distance})'
    
    class POS(ct.Union):
        _fields_ = (('faxis', FAXIS),
                    ('oaxis', OAXIS))
    
        def __repr__(self):
            return f'POS({self.faxis!r}, {self.oaxis!r})'
    
    class ODBDY2(ct.Structure):
        _fields_ = (('dummy', ct.c_short),
                    ('axis', ct.c_short),
                    ('alarm', ct.c_long),
                    ('prgnum', ct.c_long),
                    ('prgmnum', ct.c_long),
                    ('seqnum', ct.c_long),
                    ('actf', ct.c_long),
                    ('acts', ct.c_long),
                    ('pos', POS))
    
        def __repr__(self):
            return f'ODBDY2({self.dummy}, {self.axis}, {self.alarm}, {self.prgnum}, {self.prgmnum}, {self.seqnum}, {self.actf}, {self.acts}, {self.pos!r})'
    
    cnc_one_axis = (ct.c_uint32 * 11)(1,2,3,4,5,6,7,8,9,10,11)
    cnc_all_axis = (ct.c_uint32 * 19)(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19)
    
    p = ct.pointer(cnc_all_axis)
    data = ct.cast(p, ct.POINTER(ODBDY2))
    print(data.contents)
    
    p = ct.pointer(cnc_one_axis)
    data = ct.cast(p, ct.POINTER(ODBDY2)) # Notice only OAXIS has valid data
    print(data.contents)
    

    Output:

    ODBDY2(1, 0, 2, 3, 4, 5, 6, 7, POS(FAXIS([8, 9, 10], [11, 12, 13], [14, 15, 16], [17, 18, 19]), OAXIS(8, 9, 10, 11)))
    ODBDY2(1, 0, 2, 3, 4, 5, 6, 7, POS(FAXIS([8, 9, 10], [11, 0, 1], [0, -1838440016, 32762], [1, 0, -1]), OAXIS(8, 9, 10, 11)))