pololu / pololu-tic-software

Software and drivers for the Pololu Tic Stepper Motor Controller.
Other
25 stars 12 forks source link

Possible to compile 64-bit version on Windows to be able to use in Python? #2

Closed wholden closed 6 years ago

wholden commented 6 years ago

I was looking into the possibility of using Python ctypes to interface with libpololu-tic in order to control stepper motors from a Python interface. The issue I ran into is that following the build-from-source instructions, the libpololu-tic dll is compiled as a 32-bit program (if I understand properly).

I get a OSError: [WinError 193] %1 is not a valid Win32 application from Python 64-bit when trying to load the dll. Testing with Python 32-bit instead, I was able to get ctypes to communicate with libpololu-tic and verified that the serial number was read properly. Ideally, I'd like to be able to run it from Python 64-bit.

Is there a simple way to be able to compile libpololu-tic as a 64-bit program?

I'm not very familiar with building c-libraries from source. I briefly attempted running the make commands from mingw64 instead of mingw32, but it didn't recognize 'cmake' (I only installed the packages in the BUILDING.md instructions into msys2). Even if I figure out how to install cmake into mingw64, would libpololu-tic compile properly? (I suppose I'd have to recompile libusbp as well)

Maybe this is already a work-in-progress, looking at section 12 in the pololu docs ("We plan to expand this section of the user’s guide to contain tutorials and example code for many different programming languages.")

DavidEGrayson commented 6 years ago

I just updated the instructions in BUILDING.md so that you can build 64-bit software with MSYS2, so you could try that. I expect it to work. By the way, it might be easier to just run ticcmd from your Python program, so you don't have to worry about managing pointers from the library, dealing with errors, and possibly dealing with incompatibilities in compiler toolchains if you are using a Python version that does not come from MSYS2.

wholden commented 6 years ago

Hello David, thank you for your quick reply! I'll give it a try building a 64-bit version and see if it works in Python. I saw that using ticcmd was the more recommended method. I'll probably end up using that method for stability, but I was curious to try my hand with ctypes and Python.

The issue was about whether or not it was possible to do 64-bit, so I'll go ahead and close the issue. I'll post a followup about whether or not it works in Python 64-bit.

wholden commented 6 years ago

Tested with 64-bit Python and works after compiling with mingw64.

One interesting difference though between 32-bit and 64-bit. I was testing the tic_list_connected_devices function. In 32-bit Python, I could call the function without passing the optional size_t * device_count parameter. In 64-bit Python however, this leads to an access violation error. Passing the device_count parameter, however, fixed the problem.

In case it might be help to anyone (disclaimer, ctypes amateur):

import os
import ctypes
from ctypes import *

#Couldn't get windows to load DLLs properly without changing working directory to the location of the mingw64-created DLLs.

os.chdir('C:/msys64/mingw64/bin/')

#Load tic and libusbp libraries

libusbp = windll.LoadLibrary('C:/msys64/mingw64/bin/libusbp-1.dll')

libtic = windll.LoadLibrary('C:/msys64/mingw64/bin/libpololu-tic-1.dll')

#Create ctypes structs to be able to feed to DLL functions.

class LIBUSBP_GENERIC_INTERFACE(Structure):
    _fields_ = [("interface_number", c_int8),
                ("device_instand_id", POINTER(c_char)),
                ("filename", POINTER(c_char))]

class TIC_DEVICE(Structure):
    _fields_ = [("usb_interface", POINTER(LIBUSBP_GENERIC_INTERFACE)),
                ("serial_number", POINTER(c_char)),
                ("os_id", POINTER(c_char)),
                ("firmware_version", c_uint16),
                ("product", c_uint8)
               ]

#Initialize function and specify ctypes argtypes:

tic_list_connected_devices = libtic.tic_list_connected_devices

tic_list_connected_devices.argtypes = [POINTER(POINTER(POINTER(TIC_DEVICE))),POINTER(c_size_t)]

#Initialize empty ctypes variables to pass to function:

devcount = c_size_t()

mem = POINTER(POINTER(TIC_DEVICE))()

#Call function (1 tic device connected to computer)

tic_list_connected_devices(byref(mem),byref(devcount))

#Verify function worked by checking serial number and devcount (serial number agrees with output of ticgui)

mem.contents.contents.serial_number[0:8]
#outputs: b'00201645'

devcount
#outputs: c_ulonglong(1), i.e. 1 device connected
DavidEGrayson commented 6 years ago

I don't know much about ctypes in Python but from my experience with DLLs in general here are some tips:

wholden commented 6 years ago

David, thank you for your very useful feedback! I learned a lot from your advice.

This is the modified version taking the above advice into consideration. I can see how dealing with the pointers and freeing errors could become cumbersome. At this point, ticcmd seems pretty favorable.

For reference: Using ticcmd in python to check for connected tic devices and list their status:

import subprocess
subprocess.check_output(['ticcmd','-s'])

Whereas putting things together in ctypes would start by looking like:

import os
import ctypes
from ctypes import *

#Modifying sys.path doesn't affect where windows searches for DLLs, so instead we have to append to the environment path.
os.environ['PATH'] = os.environ['PATH'] + 'C:/msys64/mingw64/bin'.replace('/','\\')+';'

#Load tic library
libtic = windll.LoadLibrary('libpololu-tic-1')

#Initialize functions and specify ctypes argtypes:
tic_list_connected_devices = libtic.tic_list_connected_devices
tic_device_get_serial_number = libtic.tic_device_get_serial_number
tic_error_get_message = libtic.tic_error_get_message
tic_error_free = libtic.tic_error_free

#Specify arguments and return types so that ctypes takes care of conversions
class TIC_DEVICE(Structure):
    pass

tic_list_connected_devices.argtypes = [POINTER(POINTER(POINTER(TIC_DEVICE))),POINTER(c_size_t)]
tic_list_connected_devices.restype = c_void_p

tic_device_get_serial_number.argtypes = [POINTER(TIC_DEVICE),]
tic_device_get_serial_number.restype = c_char_p

tic_error_get_message.argtypes = [POINTER(c_void_p),]
tic_error_get_message.restype = c_char_p

tic_error_free.argtypes = [POINTER(c_void_p),]
tic_error_free.restype = None

#Initialize empty ctypes variables to pass to function:
devcount = c_size_t()
mem = POINTER(POINTER(TIC_DEVICE))()

#Call function (1 tic device connected to computer)
terr = tic_list_connected_devices(byref(mem),byref(devcount))

#Verify function worked by checking serial number and devcount (serial number agrees with output of ticgui)

devcount
#returns: c_ulonglong(1)

tic_device_get_serial_number(mem[0])
#returns: b'00201645'

tic_error_get_message(terr)
#returns b'No error.'

tic_error_free(terr)

#In this case, terr was actually 'None' because list_connected_devices returned a null pointer indicating no error.