A few things: 1. Got it working again; 2. Renamed files to make more sense; 3. Replaced template tmp.tnt with an emptied out file that previously took data, now data is collected correctly (bug, I'm not sure where this need comes from but this is, as far as I know, a permanent workaround); 4. Added automatic COM interface restart on errors compiling; 5. Implemented variable acquisition times.

This commit is contained in:
2025-06-12 11:04:57 +02:00
parent 05324a8966
commit 365f0a2374
8 changed files with 125 additions and 115 deletions

View File

@@ -0,0 +1,316 @@
# HEADER SYNTAX
# TODO: See if I can't get my name into this...
# PSEQ1.001.18 BIN
# # 0*3 [filename]
# 0x4 0*3 0*4
# # 0*3 [user]
# 0x8 0*3 (2 numbers)Ah0000 (timestamp) (4 bytes, in LSB first - little endian, then 4 zeros)
# 0x12 0*3 (device control 2)
# (small number, 0x2, 0x4) 0*3 (same as previous) 0*15 0x1
# 0*11
# 0x1 0*3 [space]
# # 0*3 Name:
# # 0*3 [name]
# 0*56
# # 0*3 [col 1 name]
# 0*56
# # 0*3 [col 2 name]
# ...
# 0*56
#
# DELAY SYNTAX
# (same number as above) 0*3 0x1 0*7 0x1 0*3 0x1 (don't ask)
# 0*11
# # 0*3 [default]
# # * 0*3 Delay
# # * 0*3 Delay
# 0*56
# # 0*3 [val 1]
# 0*56
# # 0*3 [val 2]
# ...
# 0*56
# EVENT SYNTAX
#
# (this part specifies the section)
# 0x2 0*3 [some number] 1 [X] [Y]
# 0x1 0*3 [Z] [W] 0*2 0x1
#
# (this part specifies the subsection)
# 0x11
# # 0*3 [default value]
# # 0*3 [event type (submenu)]
# # 0*3 [event type (submenu)]
# 0*8
# [ (# 0*3 [table 1D name] 0x1) if table exists, else (nothing) ]
# 0*8
# [ (# 0*3 [table 2D name] 0x1) if table exists, else (nothing) ]
# 0*8
# ...
# 0*8
# 0*11
# # 0*3 [value]
# 0*56
# 0*3 [value 2]
# 0*56
# ...
#
# (this part specifies another subsection) (only for CTRL section)
# 0x2
# 0*3 0xd (seemingly random identifiers)
# 0*3 0x18
# 0*3 0xd
# 0*3 0x1
# 0x11
# # 0*3 [value]
# # 0*3 [event type (submenu)]
# # 0*3 [event type (submenu)]
# 0*56
# # 0*3 [value]
# 0*56
# TABLE SPEC SYNTAX
# 0*56
# 0*56
# 0*11
# 0*8
# [num_tables]
# 0*3
# # 0*3 [table_1_name]
# # 0*3 [table values] 0*16 [type] (normally just HP)
# 0*2 [starting offset (in hex) - 1 is default] 0*3 0x4 8)7 0x1 0*31 0x5
# 0*4
# ...
# 0*10 0x0
Z = '\x00' # for my sanity
event_codes = {
'F1_Ampl': ('\x1f', 'P', 'E', '3', 'R', '\x08'), # F1
'F1_Ph': ('\x05', 'M', 'E', 'H', 'P', '\x02'),
'F1_PhMod': ('\x17', 'P', 'E', '3', 'P', '\x08'),
'F1_HOP': ('\x0d', 'M', 'E', 'X', 'T', '\x01'),
'F1_UnBlank': ('\x17', 'M', 'E', 'X', 'T', '\x01'),
'F1_PhRst': ('\x00', 'M', 'E', 'X', 'T', '\x01'), # RST
'F1_ExtTrig': ('\x09', 'M', 'E', 'X', 'T', '\x01'),
'Scope_Trig': ('\x14', 'M', 'E', 'X', 'T', '\x01'), # CTRL (first)
'Loop': ('\x0d', '\x18', '\x0d', '\x01'),
'CBranch': ('\x0e', '\x00', '\x0e', '\x01'),
'CTest': ('\x0f', '\x00', '\x0f', '\x01'),
'Ext_Trig': ('\x10', '\x00', '\x10', '\x01'),
'RT_Update': ('\x11', '\x00', '\x11', '\x01'),
'Acq': ('\x17', 'Q', 'A', 'C', 'A', '\x18'), # ACQ
'Acq_phase': ('\x03', 'Q', 'A', 'H', 'P', '\x02'),
'Rx_Blank': ('\x0f', 'M', 'E', 'X', 'T', '\x01'), # RX
}
event_types = list(event_codes.keys())
event_defaults = { 'F1_Ampl': 0, # F1
'F1_PhMod': -1,
'F1_Ph': -1,
'F1_UnBlank': 0,
'F1_HOP': 0,
'F1_PhRst': 0, # RST
'F1_ExtTrig': 0,
'Ext_Trig': 0, # CTRL
'Loop': 0,
'Scope_Trig': 0, # special CTRL (first)
'CBranch': 0,
'CTest': 0,
'RT_Update': 0,
'Acq': 0, # ACQ
'Acq_phase': -1,
'Rx_Blank': 0, # RX
}
def fm(s, spacing=3):
'''formats a string with its length, then 3 zeroes, then the str'''
a = f'{chr(len(s))}{Z*spacing}{s}'
return a
def get_info_header(filename, author, col_names, tuning_number, binary_name='PSEQ1.001.18 BIN'):
headerstr = ''
headerstr += 'PSEQ1.001.18 BIN'
headerstr += fm(filename)
headerstr += f'\x04{Z*3}{Z*4}'
headerstr += fm(author)
headerstr += f'\x08{Z*3}\xda\xf1\x50\x00{Z*4}' # TODO: Timestamp, but right now it doesn't really matter.
headerstr += f'\x12{Z*3}'
headerstr += f'{tuning_number}{Z*3}{tuning_number}{Z*15}\x01{Z*11}'
headerstr += fm(' ')
headerstr += fm('Name:')
headerstr += fm('Name:')
headerstr += Z*56
for i in col_names:
headerstr += fm(i)
headerstr += Z*56
return headerstr
def get_delay_header(col_delays, tuning_number):
headerstr = ''
headerstr += f'{tuning_number}{Z*3}\x01{Z*7}\x01{Z*3}\x01'
headerstr += Z*11
headerstr += fm('1u')
headerstr += fm('Delay')
headerstr += fm('Delay')
headerstr += Z*56
for i in col_delays:
headerstr += fm(str(i))
headerstr += Z*56
return headerstr
def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_delays):
'''Generates the file information for the events section.
Params
------
event_type: str describing the event
vals: an array of strs to be written.
tables: an array of dictionaries of table names to be written in format [ { '1D': None/str } (for col0), { '1D': None/str } (for col1) ... ]
table_reg: a dictionary of the table registry
tuning_number: the number of columns, minus 1
col_delays: the array of column delays
'''
codes = event_codes[event_type]
headerstr = ''
if(len(codes) == 6): # regular header
headerstr += f'{tuning_number}{Z*3}{codes[0]}1{codes[1]}{codes[2]}'
headerstr += f'{codes[5]}{Z*3}{codes[3]}{codes[4]}{Z*2}\x01'
elif(len(codes) == 4): # exteension
headerstr += f'{tuning_number}{Z*3}{codes[0]}{Z*3}{codes[1]}{Z*3}{codes[2]}{Z*3}{codes[3]}'
else:
print('PANIC')
raise Exception
headerstr += Z*11
headerstr += fm(str(event_defaults[event_type]))
headerstr += fm(event_type)
headerstr += fm(event_type)
headerstr += Z*56
for i in range(len(vals)):
headerstr += fm(str(vals[i]))
if(event_type == 'Acq' and str(vals[i]) == '1'):
acq_points = 1024
acq_time = col_delays[i]
if('u' in acq_time):
acq_time = float(acq_time.strip()[:-1])
elif('m' in acq_time):
acq_time = 1000*float(acq_time.strip()[:-1])
elif('n' in acq_time):
acq_time = 0.001*float(acq_time.strip()[:-1])
dwell_us = acq_time / acq_points
dwell = f'{dwell_us*1000}n'
freq = 1/(dwell_us/1e6) / 2 # put it in Hz. TODO: Figure out why the factor of 2 is necessary...
sweep = f'{freq}Hz'
filtr = f'{freq}Hz'
#sweep = '2500000.0Hz'
#filtr = '2500000.0Hz'
#dwell = '400.0n' # hard limit apparently...
headerstr += Z*52
headerstr += f'\x01{Z*3}' + fm(str(acq_points))
headerstr += fm(str(sweep))
headerstr += fm(str(filtr))
headerstr += fm(str(dwell))
headerstr += fm(str(col_delays[i]))
headerstr += f'\x00{Z*5}' # I believe this is to link to dashboard... (set to zero or else it will just go to default)
else:
if not(tables[i] in list(table_reg.keys())):
headerstr += Z*56
else:
headerstr += Z*8 + fm(tables[i]) + '\x01' # 1D
headerstr += Z*43
return headerstr
def get_table_spec(tables):
'''Generates the file information for a set of tables.
Parameters
----------
tables: a dictionary of form { [table_name]: { 'values': '1 2 3', 'typestr': 'HP', 'start': 1 } }
'''
specstr = ''
specstr += Z*56
specstr += Z*56
specstr += Z*11
specstr += Z*5
if(len(list(tables.keys())) > 0):
specstr += Z*3
specstr += chr(len(list(tables.keys())))
for t in list(tables.keys()):
specstr += Z*3
specstr += fm(t)
specstr += fm(tables[t]['values']) + Z*16 + tables[t]['typestr']
specstr += Z*2 + chr(tables[t]['start']) + Z*3 + chr(0x04) + Z*7 + chr(0x01) + Z*31 + chr(0x05) + Z*4
specstr += Z*10+Z
return specstr
def generate_default_sequence(col_names, col_delays):
'''Generates a dictionary for use in create_sequence_file, and populates it with all the default values as specified by event_defaults and delays.
Parameters
----------
col_names: an iterable of each of the column titles
col_delays: an iterable of each of the column delay values.
Returns
-------
a data dictionary in the form required by create_sequence_file.
'''
full_dict = { 'columns': {}, 'tables': {} }
for c, delay in zip(col_names, col_delays):
sub_dict = {}
for e in event_types:
sub_dict[e] = { 'value': str(event_defaults[e]), 'table': '' }
sub_dict['Delay'] = delay
full_dict['columns'][c] = sub_dict.copy()
return full_dict
def create_sequence_file(filename, data, author='NA'):
'''Generates a Tecmag sequence file for use in Tecmag NMR (TNMR).
Parameters
----------
filename: str
data: a dictionary in the form { 'columns': { [column_name_0]: { 'F1_Ampl': [value], ..., 'Rx_Blank': [value], 'Delay': [value] }, ... }, 'tables': { 'table_1': {...}, ... } }. If any sub-entries are empty, they will be given default values (requires that all event_types are present). See event_types and event_defaults.
author [optional]: str to describe the file creator.
'''
content = ''
column_names = list(data['columns'].keys())
tuning_number = (len(column_names)+1).to_bytes().decode('utf-8')
column_delays = []
for i in column_names:
column_delays += [ str(data['columns'][i]['Delay']) ]
content += get_info_header(filename, author, column_names, tuning_number)
content += get_delay_header(column_delays, tuning_number)
for evnt in event_types:
evnt_data_values = []
evnt_data_tables = []
for i in column_names:
evnt_data_values += [ str(data['columns'][i][evnt]['value']) ]
evnt_data_tables += [ data['columns'][i][evnt]['table'] ]
content += get_event_header(evnt, evnt_data_values, evnt_data_tables, data['tables'], tuning_number, column_delays)
content += ' '
content += get_table_spec(data['tables'])
with open(f'{filename}' if '.tps' in filename[-4:] else f'{filename}.tps', 'bw') as file:
bs = []
for char in content:
bs += [ord(char)]
file.write(bytes(bs))
file.close()