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:
316
frappy_psi/tnmr/sequence_fileformat.py
Normal file
316
frappy_psi/tnmr/sequence_fileformat.py
Normal 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()
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user