fixed app to use new thorlabs driver

This commit is contained in:
Thomas Ales [M S E]
2026-02-09 16:28:47 -06:00
parent 23f6331ba2
commit 57613b7aca
12 changed files with 1474 additions and 3333 deletions
+318
View File
@@ -0,0 +1,318 @@
'''
ThorLABS APT Protocol Message Registry
Thomas Ales | Feb 2026
Version 1
'''
import struct
from .apt_constants import StatusBits as sb
class APTProtocol():
ADDRESSES = { 'HOST_PC': 0x01, 'CONTROLLER': 0x11,
'X_AXIS': 0x21, 'Y_AXIS': 0x22
}
# N.B.: If the format is anything other than the following
# two, it is considered a 'long' message. In this case
# the format key refers only to the payload part of the
# message.
# BB - Short Message, Only Source/Destination
# BBBB - Short Message, Using Parameters 1 & 2
# Dictionary entries MUST BE in the order they
# appear in the thorlabs documentation, if you don't
# the unpacking logic goes all to 💩 and 'fun' things
# happen.
MSGS = {
0x0002: {
'name': 'MGMSG_HW_DISCONNECT',
'format': 'BB',
'response': None,
'fields': ['destination', 'source'],
'data_fields': None
},
0x0005: {
'name': 'MGMSG_HW_REQ_INFO',
'format': 'BB',
'response': 0x0006,
'fields': ['destination', 'source'],
'data_fields': None
},
0x0006: {
'name': 'MGMSG_HW_GET_INFO',
'format': '<I8sH3B61xHHH',
'response': None,
'fields': ['destination', 'source'],
'data_fields': ['serial', 'model', 'type',
'fw_minor', 'fw_interim', 'fw_major',
'hw_ver', 'mod_state', 'num_channels']
},
0x0011: {
'name': 'MGMSG_HW_START_UPDATEMSGS',
'format': 'BB',
'response': None,
'fields': ['destination', 'source'],
'data_fields': None
},
0x0012: {
'name': 'MGMSG_HW_STOP_UPDATEMSGS',
'format': 'BB',
'response': None,
'fields': ['destination', 'source'],
'data_fields': None
},
0x0060: {
'name': 'MGMSG_RACK_REQ_BAYUSED',
'format': 'BBBB',
'response': 0x0061,
'fields': ['bay_id', 'destination', 'source'],
'data_fields': None
},
0x0061: {
'name': 'MGMSG_RACK_GET_BAYUSED',
'format': 'BBBB',
'response': None,
'fields': ['bay_id', 'bay_state', 'destination', 'source'],
'data_fields': None
},
0x0210: {
'name': 'MGMSG_MOD_SET_CHANENABLESTATE',
'format': 'BBBB',
'response': None,
'fields': ['chan_ident', 'enable_state', 'destination', 'source'],
'data_fields': None
},
0x0211: {
'name': 'MGMSG_MOD_REQ_CHANENABLESTATE',
'format': 'BB',
'response': 0x0212,
'fields': ['destination', 'source'],
'data_fields': None
},
0x0212: {
'name': 'MGMSG_MOD_GET_CHANENABLESTATE',
'format': 'BBBB',
'response': None,
'fields': ['chan_ident', 'enable_state', 'destination', 'source'],
'data_fields': None
},
0x0413: {
'name': 'MGMSG_MOT_SET_VELPARAMS',
'format': '<Hlll',
'response': None,
'fields': ['destination', 'source'],
'data_fields': ['chan_ident', 'min_velocity', 'acceleration',
'max_velocity']
},
0x0414: {
'name': 'MGMSG_MOT_REQ_VELPARAMS',
'format': 'BBBB',
'response': 0x0415,
'fields': ['chan_ident', 'zero_this', 'destination', 'source'],
'data_fields': None
},
0x0415: {
'name': 'MGMSG_MOT_GET_VELPARAMS',
'format': '<Hlll',
'response': None,
'fields': ['destination', 'source'],
'data_fields': ['chan_ident', 'min_velocity', 'acceleration',
'max_velocity']
},
0x0443: {
'name': 'MGMSG_MOT_MOVE_HOME',
'format': 'BBBB',
'response': 0x0444,
'fields': ['chan_ident', 'destination', 'source'],
'data_fields': None
},
0x0444: {
'name': 'MGMSG_MOT_MOVE_HOMED',
'format': 'BBBB',
'response': None,
'fields': ['chan_ident', 'destination', 'source'],
'data_fields': None
},
0x0448: {
'name': 'MGMSG_MOT_MOVE_RELATIVE',
'format': '<Hl',
'response': 0x0464,
'fields': ['destination', 'source'],
'data_fields': ['chan_ident', 'relative_distance']
},
0x0453: {
'name': 'MGMSG_MOT_MOVE_ABSOLUTE',
'format': '<Hl',
'response': 0x0464,
'fields': ['destination', 'source'],
'data_fields': ['chan_ident', 'absolute_distance']
},
0x0464: {
'name': 'MGMSG_MOT_MOVE_COMPLETED',
'format': '<Hl2HI',
'response': None,
'fields': ['destination', 'source'],
'data_fields': ['chan_ident', 'position', 'velocity',
'motor_current', 'status_bits']
},
0x0490: {
'name': 'MGMSG_MOT_REQ_USTATUSUPDATE',
'format': 'BB',
'response': None,
'fields': ['destination', 'source'],
'data_fields': None
},
0x0491: {
'name': 'MGMSG_MOT_GET_USTATUSUPDATE',
'format': '<Hl2HI',
'response': 0x0492,
'fields': ['destination', 'source'],
'data_fields': ['chan_ident', 'position',
'velocity', 'motor_current', 'status_bits']
},
0x0492: {
'name': 'MGMSG_MOT_ACK_USTATUSUPDATE',
'format': 'BB',
'response': None,
'fields': ['destination', 'source'],
'data_fields': None
},
0x0500: {
'name': 'MGMSG_MOT_SET_TRIGGER',
'format': 'BBBB',
'response': None,
'fields': ['chan_ident', 'mode', 'destination', 'source'],
'data_fields': None
},
0x0501: {
'name': 'MGMSG_MOT_REQ_TRIGGER',
'format': 'BBBB',
'response': 0x0502,
'fields': ['chan_ident', 'mode', 'destination', 'source'],
'data_fields': None
},
0x0502: {
'name': 'MGMSG_MOT_GET_TRIGGER',
'format': 'BBBB',
'response': None,
'fields': ['chan_ident', 'mode', 'destination', 'source'],
'data_fields': None
}
}
def __init__(self):
pass
@classmethod
def build_message(cls, msg_id, **kwargs):
if msg_id not in APTProtocol.MSGS:
raise ValueError(f'Unknown Message ID: {hex(msg_id)}')
msg_spec = APTProtocol.MSGS[msg_id]
for param in msg_spec['fields']:
if param not in kwargs:
raise ValueError(f"Missing required parameter '{param}' for {msg_spec['name']}.")
msg_format = msg_spec.get('format')
if msg_format == 'BB':
# this is a simple source/destination command
dest = kwargs['destination']
src = kwargs['source']
message = struct.pack('<HBBBB', msg_id,
0x00, 0x00,
dest, src)
elif msg_format == 'BBBB':
# this is a source/destination plus parameters
# fields order: [param1, param2, destination, source] or
# [param1, destination, source] (param2 = 0)
fields = msg_spec['fields']
dest = kwargs['destination']
src = kwargs['source']
# Get param fields (everything except destination/source)
param_fields = [f for f in fields if f not in ('destination', 'source')]
p1 = kwargs.get(param_fields[0], 0x00) if len(param_fields) > 0 else 0x00
p2 = kwargs.get(param_fields[1], 0x00) if len(param_fields) > 1 else 0x00
message = struct.pack('<HBBBB', msg_id,
p1, p2, dest, src)
else:
data_fields = msg_spec.get('data_fields')
# verify data fields are available
if data_fields is None:
raise ValueError("I need data for a long message!")
# Extract the message specific values and
# calculate payload size
df_values = [kwargs[field] for field in data_fields]
data_payload = struct.pack(msg_format, *df_values)
payload_len = len(data_payload)
header = struct.pack('<HHBB', msg_id, payload_len,
kwargs['destination'] | 0x80,
kwargs['source'])
# put message together
message = header + data_payload
return message
@classmethod
def unpack_message(cls, bdata):
# Get the message ID
msg_id = struct.unpack("<H", bdata[0:2])[0]
# See if the message ID has been implemented
# in the registry
if msg_id not in APTProtocol.MSGS:
raise ValueError(f"This op-code {msg_id} isn't in the registry!")
msg_spec = APTProtocol.MSGS[msg_id]
fmt_string = msg_spec.get('format')
# Check if it's a long message, or just a header
if bdata[4] & 0x80:
# this is a long message.
payload_size = struct.unpack("<H", bdata[2:4])[0]
src = bdata[5]
dest = bdata[4] & 0x7F
payload = bdata[6:6+payload_size]
data_fields = msg_spec.get('data_fields')
if data_fields is None:
raise ValueError(f"No data fields have been defined for {msg_spec['name']}!")
unpacked_payload = struct.unpack(fmt_string, payload)
data = {field: value for field, value in zip(data_fields,
unpacked_payload)}
data['destination'] = dest
data['source'] = src
return msg_id, data
else:
# it is a header only message
# determine type
dest = bdata[4]
src = bdata[5]
if fmt_string == 'BB':
# simple source/dest command
data = {'destination': dest, 'source': src}
return msg_id, data
elif fmt_string == 'BBBB':
# simple command with parameters
# Use field names from spec
fields = msg_spec['fields']
param_fields = [f for f in fields if f not in ('destination', 'source')]
data = {'destination': dest, 'source': src}
if len(param_fields) > 0:
data[param_fields[0]] = bdata[2]
if len(param_fields) > 1:
data[param_fields[1]] = bdata[3]
return msg_id, data
else:
raise ValueError(f"Message format {fmt_string} isn't defined/valid!")
@classmethod
def get_name(cls, msg_id):
return cls.MSGS.get(msg_id, {}).get('name', f'UNKNOWN_{hex(msg_id)}')