From 3357927ccd36c280f68f351f22f924a2d3b1d567 Mon Sep 17 00:00:00 2001 From: spirrobe Date: Fri, 22 Dec 2023 16:35:12 +0100 Subject: [PATCH 01/11] Update RadarControl.py Several addtionial features and bugfixes. Testing still required for radar control/generic scans as listed in the issues of the original repo --- RadarControl.py | 1818 ++++++++++++++++++++++++++--------------------- 1 file changed, 1004 insertions(+), 814 deletions(-) diff --git a/RadarControl.py b/RadarControl.py index 465ee73..cd27018 100644 --- a/RadarControl.py +++ b/RadarControl.py @@ -1,10 +1,10 @@ # This module contains a Python API class that provides a platform independent interface # to control RPG FMCW cloud radars. The class can be used for a development of adaptive -# observation strategies. The class provides an access to the latest measured sample +# observation strategies. The class provides an access to the latest measured sample # (both integrated and spectral measured quantities). Based on user anaysis of the last # sample, scanning and measurement settings can be switched in near-real time. The class -# can also be used to access housekeeping data and to monitor the radar status. -# In addition to the functions which can be used in the users scripts, the module also +# can also be used to access housekeeping data and to monitor the radar status. +# In addition to the functions which can be used in the users scripts, the module also # provides command line interface to control RPG FMCW radars directly from a command prompt. # # LICENSE @@ -15,16 +15,16 @@ # # ACKNOWLEDGEMENT # -# If you find the provided modules useful for you study, we kindly ask -# to acknowledge in publications our efforts to provide *free* and +# If you find the provided modules useful for you study, we kindly ask +# to acknowledge in publications our efforts to provide *free* and # *open-source* solutions for the scientific community. These acknowledgements -# will help us to fulfil requirements for a publication in the Journal +# will help us to fulfil requirements for a publication in the Journal # of Open Source Software. # # Found a bug? Please contact us via email: -# -# Interested in contributing to the develoment of modules? Have an idea how to further -# improve functionality? Please send us your ideas and suggestions to +# +# Interested in contributing to the development of modules? Have an idea how to further +# improve functionality? Please send us your ideas and suggestions to # import socket @@ -35,226 +35,235 @@ import math from array import array -from pathlib import Path +from pathlib import Path + class ByteReader: - - def __init__(self,byte_input,start_idx = 1): - - # index starts from 1 because the 0th byte + + def __init__(self, byte_input, start_idx=1): + + # index starts from 1 because the 0th byte # in the host response repeats the requested command # This repeated byte is checked - # in the __check_first_byte_of_responce fucntion + # in the __check_first_byte_of_response fucntion # of the Client class (see below in this module) - self.__i = start_idx; - self.__input = byte_input; - - def __get_byte_num(self,value_type): - + self.__i = start_idx + self.__input = byte_input + + def __get_byte_num(self, value_type): + # float, integer, unsigned integer - if value_type in ('f','i','I'): - return 4 # [bytes] - + if value_type in ('f', 'i', 'I'): + return 4 # [bytes] + # byte if value_type == 'b': - return 1 # [byte] - + return 1 # [byte] + # short integer, unsigned short integer - if value_type in ('h','H'): - return 2 # [bytes] - - if value_type in ('q','Q'): - return 8 # [bytes] - + if value_type in ('h', 'H'): + return 2 # [bytes] + + if value_type in ('q', 'Q'): + return 8 # [bytes] + raise Exception("Unknown variable type") - - def __read_single_value(self,value_type,endian = '<'): - - V = self.__read_vector(value_type,1,endian = endian) - + + def __read_single_value(self, value_type, endian='<'): + + V = self.__read_vector(value_type, 1, endian=endian) return V[0] - - def __read_vector(self,value_type,value_num,endian = '<'): - - size = self.__get_byte_num(value_type) + + def __read_vector(self, value_type, value_num, endian='<'): + + size = self.__get_byte_num(value_type) firstidx = self.__i - lastidx = self.__i + value_num * size - - F = struct.unpack(value_type * value_num,self.__input[firstidx:lastidx]) - + lastidx = self.__i + value_num * size + + F = struct.unpack(value_type * value_num, + self.__input[firstidx:lastidx]) + self.__i += value_num * size - return F - + def _read_float(self): - + try: return self.__read_single_value('f') except: raise Exception('ByteReader: Cannot read a float') - + def _read_unsigned_short(self): - + try: return self.__read_single_value('H') except: - raise Exception('ByteReader: Cannot read a float') - + raise Exception('ByteReader: Cannot read a unsigned short int') + + def _read_signed_short(self): + + try: + return self.__read_single_value('h', '<') + except: + raise Exception('ByteReader: Cannot read a signed short int ') + def _read_unsigned_int(self): - + try: return self.__read_single_value('I') except: - raise Exception('ByteReader: Cannot read a float') - + raise Exception('ByteReader: Cannot read a unsigned int') + def _read_int(self): - + try: return self.__read_single_value('i') except: - raise Exception('ByteReader: Cannot read an integer') - + raise Exception('ByteReader: Cannot read an signed integer') + def _read_int_big_endian(self): - + try: - return self.__read_single_value('i','>') + return self.__read_single_value('i', '>') except: raise Exception('ByteReader: Cannot read an integer') - + def _read_int_little_endian(self): - + try: - return self.__read_single_value('i','<') + return self.__read_single_value('i', '<') except: raise Exception('ByteReader: Cannot read an integer') - + def _read_long_long(self): - + try: return self.__read_single_value('q') except: raise Exception('ByteReader: Cannot read long long') - + def _read_byte(self): - + try: return self.__read_single_value('b') except: raise Exception('ByteReader: Cannot read a byte') - + def _read_string(self): try: - for j in range(self.__i,len(self.__input)): + for j in range(self.__i, len(self.__input)): if self.__input[j] == 0: break - + s = self.__input[self.__i:j].decode('UTF-8') self.__i = j + 1 return s except: raise Exception('ByteReader: Cannot read a string') - + def _read_null_separated_strings_to_list(self): - + try: a = self.__input[self.__i:].decode('UTF-8') b = a.split('\x00') return b except: raise Exception('ByteReader: Cannot read strings') - - def _read_float_vector(self,value_num): - + + def _read_float_vector(self, value_num): + try: - return self.__read_vector('f',value_num) + return self.__read_vector('f', value_num) except: raise Exception('ByteReader: Cannot read float vector') - - def _read_int_vector(self,value_num): - + + def _read_int_vector(self, value_num): + try: - return self.__read_vector('i',value_num) + return self.__read_vector('i', value_num) except: raise Exception('ByteReader: Cannot read int vector') - - def _read_byte_vector(self,value_num): - + + def _read_byte_vector(self, value_num): + try: - return self.__read_vector('b',value_num) + return self.__read_vector('b', value_num) except: raise Exception('ByteReader: Cannot read byte vector') - - def _read_short_int_vector(self,value_num): - + + def _read_short_int_vector(self, value_num): + try: - return self.__read_vector('h',value_num) + return self.__read_vector('h', value_num) except: raise Exception('ByteReader: Cannot read short int vector') - + + class MDFList(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - + + def __init__(self, byte_input, SuppressOutput=False): + self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) + ByteReader.__init__(self, byte_input) self.__read_mdf_list() - + def __output(self): - + if self.__suppressout: return - + print('\nList of MDF files on the host PC: \n') for mdf in self.mdf_list: print(mdf) - + def __read_mdf_list(self): - - self.mdf_num = ByteReader._read_int(self) + + self.mdf_num = ByteReader._read_int(self) self.mdf_list = ByteReader._read_null_separated_strings_to_list(self) - self.__output() + self.__output() + class Status(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - + + def __init__(self, byte_input, SuppressOutput=False): + self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) + ByteReader.__init__(self, byte_input) self.__read_radar_status() - + def __check_connection(self): - - self.connection = ByteReader._read_byte(self) - + + self.connection = ByteReader._read_byte(self) + if self.connection == 0: if self.__suppressout == False: print('The HOST is not connected to the radar') return False - + if self.connection != 1: if self.__suppressout == False: - print('Invalid responce: ',self.connection) + print('Invalid response: ', self.connection) return False - + if self.__suppressout == False: print('The HOST is connected to the radar') return True - + def __output_status(self): - + if self.__suppressout: return - + if self.connection == 0: print('The HOST is not connected to the radar') return - + if self.connection != 1: - print('Invalid responce') + print('Invalid response') return - + if self.status == 1: print('The radar is in STANDBY mode') @@ -269,22 +278,22 @@ def __output_status(self): if self.status == 5: print('Transmitter calibration running') - + if self.status == 2 or self.status == 3 or self.status == 5: - + print('Number of MDFs in current measurement: {}'.format(self.mdf_num)) - + for i in range(self.mdf_num): - + print('MDF {}: '.format(i+1) + self.mdf_name[i]) - + if self.mdf_num > 1: - + print('MBF: ' + self.mbf_name) print('Number of repetitions in batch: {}'.format(self.RepN)) print('Current batch repetition index: {}'.format(self.CurRep)) print('Current MDF index: {}'.format(self.CurMDF)) - + if self.hierarchy == 0: print('Single radar') @@ -293,13 +302,12 @@ def __output_status(self): if self.hierarchy == 2: print('Dual radar, radar is in Slave mode') - - + def __read_radar_status(self): - + if self.__check_connection() == False: return - + self.status = ByteReader._read_byte(self) if self.status == 2 or self.status == 3 or self.status == 5: @@ -307,46 +315,47 @@ def __read_radar_status(self): self.mdf_num = ByteReader._read_int(self) self.mdf_name = [] - + for i in range(self.mdf_num): - - self.mdf_name.append(ByteReader._read_string(self)) + + self.mdf_name.append(ByteReader._read_string(self)) if self.mdf_num > 1: self.mbf_name = ByteReader._read_string(self) - self.RepN = ByteReader._read_int(self) - self.CurRep = ByteReader._read_int(self) - self.CurMDF = ByteReader._read_int(self) + self.RepN = ByteReader._read_int(self) + self.CurRep = ByteReader._read_int(self) + self.CurMDF = ByteReader._read_int(self) self.hierarchy = ByteReader._read_byte(self) - + self.__output_status() - + + class RadarID(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - + + def __init__(self, byte_input, SuppressOutput=False): + self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) + ByteReader.__init__(self, byte_input) self.__read_radar_id() - + def __output(self): - + if self.__suppressout: return - + if self.connection == 0: print('The HOST is not connected to the radar') return - + if self.connection != 1: - print('Invalid responce: ',self.connection) + print('Invalid response: ', self.connection) return - + print('Software version: {0:.2f}'.format(self.swversion)) print('Subversion: {}'.format(self.sbversion)) - + if self.model == 0: print('Radar model: RPG-FMCW-94-SP') if self.model == 1: @@ -355,24 +364,24 @@ def __output(self): print('Radar model: RPG-FMCW-35-SP') if self.model == 3: print('Radar model: RPG-FMCW-35-DP') - + print('Fabrication year: {}'.format(self.year)) print('Fabrication number: {}'.format(self.number)) print('Customer: ' + self.customer) print('License: {}'.format(self.license)) - + if self.scanner == 0: print('No positioner found') if self.scanner == 1: print('Positioner found') - + if self.polmode == 0: print('Single pol. radar') if self.polmode == 1: print('Dual pol. radar: LDR mode') if self.polmode == 2: print('Dual pol. radar: STSR mode') - + print('IF range min. frequency [Hz]: {0:.1f}'.format(self.IFmin)) print('IF range max. frequency [Hz]: {0:.1f}'.format(self.IFmax)) print('Wavelength [m]: {0:.4f}'.format(self.wavelen)) @@ -383,7 +392,7 @@ def __output(self): print('Subreflector blockage [%]: {0:.3f}'.format(self.ant_block)) print('Receiver gain V-pol. [linear]: {0:.3f}'.format(self.vrec_g)) print('Receiver gain H-pol. [linear]: {0:.3f}'.format(self.hrec_g)) - + if self.fft_win == 0: print('FFT window: Rectangular') if self.fft_win == 1: @@ -396,7 +405,7 @@ def __output(self): print('FFT window: Slepian2') if self.fft_win == 5: print('FFT window: Slepian3') - + if self.recovery == 0: print('Recovery after power failure: Disabled') if self.recovery == 1: @@ -420,472 +429,476 @@ def __output(self): print('InterLAN status: Autodetection') print('Radar IP: ' + self.radar_ip) - + def __read_radar_id(self): - + if self.__check_connection() == False: return - + self.swversion = ByteReader._read_float(self) self.sbversion = ByteReader._read_int(self) - self.model = ByteReader._read_int(self) - self.year = ByteReader._read_int(self) - self.number = ByteReader._read_int(self) - self.customer = ByteReader._read_string(self) - self.license = ByteReader._read_int(self) - self.scanner = ByteReader._read_byte(self) - self.polmode = ByteReader._read_byte(self) - self.IFmin = ByteReader._read_float(self) - self.IFmax = ByteReader._read_float(self) - self.wavelen = ByteReader._read_float(self) - self.ant_d = ByteReader._read_float(self) - self.ant_sep = ByteReader._read_float(self) - self.ant_g = ByteReader._read_float(self) - self.hpbw = ByteReader._read_float(self) + self.model = ByteReader._read_int(self) + self.year = ByteReader._read_int(self) + self.number = ByteReader._read_int(self) + self.customer = ByteReader._read_string(self) + self.license = ByteReader._read_int(self) + self.scanner = ByteReader._read_byte(self) + self.polmode = ByteReader._read_byte(self) + self.IFmin = ByteReader._read_float(self) + self.IFmax = ByteReader._read_float(self) + self.wavelen = ByteReader._read_float(self) + self.ant_d = ByteReader._read_float(self) + self.ant_sep = ByteReader._read_float(self) + self.ant_g = ByteReader._read_float(self) + self.hpbw = ByteReader._read_float(self) self.ant_block = ByteReader._read_float(self) - self.vrec_g = ByteReader._read_float(self) - self.hrec_g = ByteReader._read_float(self) - self.fft_win = ByteReader._read_int(self) - self.recovery = ByteReader._read_byte(self) - self.calibrat = ByteReader._read_byte(self) - self.pow_diag = ByteReader._read_byte(self) - self.scan_off = ByteReader._read_float(self) - self.interlan = ByteReader._read_byte(self) - self.radar_ip = ByteReader._read_string(self) + self.vrec_g = ByteReader._read_float(self) + self.hrec_g = ByteReader._read_float(self) + self.fft_win = ByteReader._read_int(self) + self.recovery = ByteReader._read_byte(self) + self.calibrat = ByteReader._read_byte(self) + self.pow_diag = ByteReader._read_byte(self) + self.scan_off = ByteReader._read_float(self) + self.interlan = ByteReader._read_byte(self) + self.radar_ip = ByteReader._read_string(self) self.__output() - + def __check_connection(self): - - self.connection = ByteReader._read_byte(self) - + + self.connection = ByteReader._read_byte(self) + if self.connection == 0: if self.__suppressout == False: print('The HOST is not connected to the radar') return False - + if self.connection != 1: if self.__suppressout == False: - print('Invalid responce') + print('Invalid response') return False - + if self.__suppressout == False: print('The HOST is connected to the radar') return True + class HouseKeeping(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - + + def __init__(self, byte_input, SuppressOutput=False): + self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) - + ByteReader.__init__(self, byte_input) + self.__read() - + def __read(self): - + self.gps_flag = ByteReader._read_byte(self) - + if self.gps_flag == 1: - - self.pos_stat = ByteReader._read_byte(self) + + self.pos_stat = ByteReader._read_byte(self) self.time_stat = ByteReader._read_byte(self) - + if self.pos_stat == 1: - - self.longitude = ByteReader._read_float(self) - self.latitude = ByteReader._read_float(self) + + self.longitude = ByteReader._read_float(self) + self.latitude = ByteReader._read_float(self) self.gps_pos_time = ByteReader._read_int(self) - + if self.time_stat == 1: - + self.gps_sync_time = ByteReader._read_int(self) - + self.radar_time = ByteReader._read_int(self) - self.met_found = ByteReader._read_byte(self) - + self.met_found = ByteReader._read_byte(self) + if self.met_found == 1: - + self.env_temp = ByteReader._read_float(self) self.pressure = ByteReader._read_float(self) - self.rel_hum = ByteReader._read_float(self) - self.wind_sp = ByteReader._read_float(self) + self.rel_hum = ByteReader._read_float(self) + self.wind_sp = ByteReader._read_float(self) self.wind_dir = ByteReader._read_float(self) - + self.scanner_found = ByteReader._read_byte(self) - + if self.met_found == 1: - + self.elev = ByteReader._read_float(self) - self.azm = ByteReader._read_float(self) - - self.rec_temp = ByteReader._read_float(self) + self.azm = ByteReader._read_float(self) + + self.rec_temp = ByteReader._read_float(self) self.trans_temp = ByteReader._read_float(self) - self.pc_temp = ByteReader._read_float(self) - self.rain_stat = ByteReader._read_byte(self) - - self.heat_switch = ByteReader._read_int(self) - self.blow_switch = ByteReader._read_int(self) + self.pc_temp = ByteReader._read_float(self) + self.rain_stat = ByteReader._read_byte(self) + + self.heat_switch = ByteReader._read_int(self) + self.blow_switch = ByteReader._read_int(self) self.rad_srive_cnt = ByteReader._read_int(self) - + self.free_mem = [] - self.tot_mem = [] - + self.tot_mem = [] + for i in range(self.rad_srive_cnt): - + self.free_mem.append(ByteReader._read_int(self)) self.tot_mem.append(ByteReader._read_int(self)) - - self.inc_el = ByteReader._read_float(self) + + self.inc_el = ByteReader._read_float(self) self.inc_el_ax = ByteReader._read_float(self) self.meas_mode = ByteReader._read_byte(self) - self.hirarchy = ByteReader._read_byte(self) - + self.hirarchy = ByteReader._read_byte(self) + if self.hirarchy == 1: - + self.sl_model_no = ByteReader._read_int(self) - - self.sl_error = ByteReader._read_byte(self) - self.meas_run = ByteReader._read_byte(self) + + self.sl_error = ByteReader._read_byte(self) + self.meas_run = ByteReader._read_byte(self) self.hdd_overflow = ByteReader._read_byte(self) - self.alarm_code = ByteReader._read_byte(self) - + self.alarm_code = ByteReader._read_byte(self) + + class LastSample(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - + + def __init__(self, byte_input, SuppressOutput=False): + self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) - + ByteReader.__init__(self, byte_input) + self.__default_dr = 30 # [m] - self.__defauls_dv = 0.04 # [m/s] - + self.__defauls_dv = 0.04 # [m/s] + self.__read_last_sample() - - + def __check_running_measurements(self): - - self.meas_run = ByteReader._read_byte(self) - + + self.meas_run = ByteReader._read_byte(self) + if self.meas_run == 0: if self.__suppressout == False: print('Measurements are not running') return False - + if self.meas_run != 1: if self.__suppressout == False: - print('Invalid responce') + print('Invalid response') return False - + if self.__suppressout == False: print('Measurements are running') return True - + def __read_last_sample(self): - + if self.__check_running_measurements() == False: return - - self.samp_idx = ByteReader._read_int(self) - self.head_len = ByteReader._read_int(self) - self.cgprog = ByteReader._read_int(self) - self.model = ByteReader._read_int(self) + + self.samp_idx = ByteReader._read_int(self) + self.head_len = ByteReader._read_int(self) + self.cgprog = ByteReader._read_int(self) + self.model = ByteReader._read_int(self) # self.number = ByteReader._read_int_big_endian(self) - - self.progname = ByteReader._read_string(self) - self.customer = ByteReader._read_string(self) - - self.freq = ByteReader._read_float(self) - self.ant_sep = ByteReader._read_float(self) - self.ant_d = ByteReader._read_float(self) - self.ant_g = ByteReader._read_float(self) - self.hpbw = ByteReader._read_float(self) - self.radar_c = ByteReader._read_float(self) - self.pol_mode = ByteReader._read_byte(self) - self.comp_ena = ByteReader._read_byte(self) - self.antialia = ByteReader._read_byte(self) - self.samp_dur = ByteReader._read_float(self) - self.gps_lat = ByteReader._read_float(self) - self.gps_lon = ByteReader._read_float(self) - self.call_int = ByteReader._read_int(self) - self.raltn = ByteReader._read_int(self) - self.taltn = ByteReader._read_int(self) - self.haltn = ByteReader._read_int(self) - self.sequn = ByteReader._read_int(self) - self.ralts = ByteReader._read_float_vector(self,self.raltn) - self.talts = ByteReader._read_float_vector(self,self.taltn) - self.halts = ByteReader._read_float_vector(self,self.haltn) - self.nfft = ByteReader._read_int_vector(self,self.sequn) - self.rng_off = ByteReader._read_int_vector(self,self.sequn) - self.c_rep = ByteReader._read_int_vector(self,self.sequn) - self.seqint = ByteReader._read_float_vector(self,self.sequn) - self.dr = ByteReader._read_float_vector(self,self.sequn) - self.max_vel = ByteReader._read_float_vector(self,self.sequn) - self.center_f = ByteReader._read_float_vector(self,self.sequn) - self.cal_par = ByteReader._read_int(self) + + self.progname = ByteReader._read_string(self) + self.customer = ByteReader._read_string(self) + + self.freq = ByteReader._read_float(self) + self.ant_sep = ByteReader._read_float(self) + self.ant_d = ByteReader._read_float(self) + self.ant_g = ByteReader._read_float(self) + self.hpbw = ByteReader._read_float(self) + self.radar_c = ByteReader._read_float(self) + self.pol_mode = ByteReader._read_byte(self) + self.comp_ena = ByteReader._read_byte(self) + self.antialia = ByteReader._read_byte(self) + self.samp_dur = ByteReader._read_float(self) + self.gps_lat = ByteReader._read_float(self) + self.gps_lon = ByteReader._read_float(self) + self.call_int = ByteReader._read_int(self) + self.raltn = ByteReader._read_int(self) + self.taltn = ByteReader._read_int(self) + self.haltn = ByteReader._read_int(self) + self.sequn = ByteReader._read_int(self) + self.ralts = ByteReader._read_float_vector(self, self.raltn) + self.talts = ByteReader._read_float_vector(self, self.taltn) + self.halts = ByteReader._read_float_vector(self, self.haltn) + self.nfft = ByteReader._read_int_vector(self, self.sequn) + self.rng_off = ByteReader._read_int_vector(self, self.sequn) + self.c_rep = ByteReader._read_int_vector(self, self.sequn) + self.seqint = ByteReader._read_float_vector(self, self.sequn) + self.dr = ByteReader._read_float_vector(self, self.sequn) + self.max_vel = ByteReader._read_float_vector(self, self.sequn) + self.center_f = ByteReader._read_float_vector(self, self.sequn) + self.cal_par = ByteReader._read_int(self) self.samp_rate = ByteReader._read_int(self) self.max_range = ByteReader._read_int(self) - + # range_bin_num = math.ceil(self.max_range / self.__default_range_res) # vel_bin_num = math.ceil(max(self.max_vel) / self.__defaulf_vel_res) - + # self.spectrum = np.empty((vel_bin_num,range_bin_num)) # self.spectrum[:] = np.nan - - self.sup_pow_lev = ByteReader._read_byte(self) - self.spk_filter = ByteReader._read_byte(self) - self.phase_corr = ByteReader._read_byte(self) - self.rel_pow_cor = ByteReader._read_byte(self) - self.fft_window = ByteReader._read_byte(self) - - self.fft_input_range = ByteReader._read_int(self) - self.noise_filter = ByteReader._read_float(self) + + self.sup_pow_lev = ByteReader._read_byte(self) + self.spk_filter = ByteReader._read_byte(self) + self.phase_corr = ByteReader._read_byte(self) + self.rel_pow_cor = ByteReader._read_byte(self) + self.fft_window = ByteReader._read_byte(self) + + self.fft_input_range = ByteReader._read_int(self) + self.noise_filter = ByteReader._read_float(self) self.chirp_table_prog = ByteReader._read_int(self) - self.lim_meas = ByteReader._read_byte(self) - + self.lim_meas = ByteReader._read_byte(self) + if self.lim_meas == 1: self.end_of_meas = ByteReader._read_int(self) - self.scan_type = ByteReader._read_byte(self) - self.scan_mode = ByteReader._read_byte(self) - self.scan_dur = ByteReader._read_int(self) - self.base_fn = ByteReader._read_string(self) - - self.arch_data = ByteReader._read_byte(self) - self.disk_full = ByteReader._read_byte(self) + self.scan_type = ByteReader._read_byte(self) + self.scan_mode = ByteReader._read_byte(self) + self.scan_dur = ByteReader._read_int(self) + self.base_fn = ByteReader._read_string(self) + + self.arch_data = ByteReader._read_byte(self) + self.disk_full = ByteReader._read_byte(self) self.sup_pow_lev = ByteReader._read_byte(self) - self.meas_trig = ByteReader._read_byte(self) - self.meas_start = ByteReader._read_int(self) - self.meas_class = ByteReader._read_byte(self) - self.store_lv0 = ByteReader._read_byte(self) - self.store_lv1 = ByteReader._read_byte(self) - - self.rep_idx = ByteReader._read_int(self) - self.mdf_idx = ByteReader._read_int(self) - self.rep_num = ByteReader._read_int(self) - self.mdf_num = ByteReader._read_int(self) - self.mbf_name = ByteReader._read_string(self) - + self.meas_trig = ByteReader._read_byte(self) + self.meas_start = ByteReader._read_int(self) + self.meas_class = ByteReader._read_byte(self) + self.store_lv0 = ByteReader._read_byte(self) + self.store_lv1 = ByteReader._read_byte(self) + + self.rep_idx = ByteReader._read_int(self) + self.mdf_idx = ByteReader._read_int(self) + self.rep_num = ByteReader._read_int(self) + self.mdf_num = ByteReader._read_int(self) + self.mbf_name = ByteReader._read_string(self) + self.mdf_list = [] - + for i in range(self.mdf_num): self.mdf_list.append(ByteReader._read_string(self)) - + # LV0 Data - self.samp_t = ByteReader._read_unsigned_int(self) + self.samp_t = ByteReader._read_unsigned_int(self) self.samp_ms = ByteReader._read_int(self) - self.qf = ByteReader._read_byte(self) - - self.rr = ByteReader._read_float(self) + self.qf = ByteReader._read_byte(self) + + self.rr = ByteReader._read_float(self) self.rel_hum = ByteReader._read_float(self) - self.env_t = ByteReader._read_float(self) - self.baro_p = ByteReader._read_float(self) - self.ws = ByteReader._read_float(self) - self.wd = ByteReader._read_float(self) + self.env_t = ByteReader._read_float(self) + self.baro_p = ByteReader._read_float(self) + self.ws = ByteReader._read_float(self) + self.wd = ByteReader._read_float(self) self.dd_volt = ByteReader._read_float(self) - self.dd_tb = ByteReader._read_float(self) - self.lwp = ByteReader._read_float(self) - self.pow_if = ByteReader._read_float(self) - self.elv = ByteReader._read_float(self) - self.azm = ByteReader._read_float(self) - self.status = ByteReader._read_float(self) - self.t_pow = ByteReader._read_float(self) - self.t_temp = ByteReader._read_float(self) - self.r_temp = ByteReader._read_float(self) + self.dd_tb = ByteReader._read_float(self) + self.lwp = ByteReader._read_float(self) + self.pow_if = ByteReader._read_float(self) + self.elv = ByteReader._read_float(self) + self.azm = ByteReader._read_float(self) + self.status = ByteReader._read_float(self) + self.t_pow = ByteReader._read_float(self) + self.t_temp = ByteReader._read_float(self) + self.r_temp = ByteReader._read_float(self) self.pc_temp = ByteReader._read_float(self) - self.sky_tb = ByteReader._read_float(self) - self.inc_el = ByteReader._read_float(self) + self.sky_tb = ByteReader._read_float(self) + self.inc_el = ByteReader._read_float(self) self.inc_elax = ByteReader._read_float(self) - - self.t_prof = ByteReader._read_float_vector(self,self.taltn) - self.ah_prof = ByteReader._read_float_vector(self,self.haltn) - self.rh_prof = ByteReader._read_float_vector(self,self.haltn) - - self.s_lev = ByteReader._read_float_vector(self,self.raltn) - - prof_msk = ByteReader._read_byte_vector(self,self.raltn) - + + self.t_prof = ByteReader._read_float_vector(self, self.taltn) + self.ah_prof = ByteReader._read_float_vector(self, self.haltn) + self.rh_prof = ByteReader._read_float_vector(self, self.haltn) + + self.s_lev = ByteReader._read_float_vector(self, self.raltn) + + prof_msk = ByteReader._read_byte_vector(self, self.raltn) + for msk in prof_msk: - + if msk == 0: continue - + block_n = ByteReader._read_byte(self) - - min_block_idx = ByteReader._read_short_int_vector(self,block_n) - max_block_idx = ByteReader._read_short_int_vector(self,block_n) - + + min_block_idx = ByteReader._read_short_int_vector(self, block_n) + max_block_idx = ByteReader._read_short_int_vector(self, block_n) + for i in range(block_n): - + block_width = max_block_idx[i] - min_block_idx[i] + 1 - - Spec = ByteReader._read_float_vector(self,block_width) - + + Spec = ByteReader._read_float_vector(self, block_width) + if self.antialia == 1: alias_msk = ByteReader._read_byte(self) - min_vel = ByteReader._read_float(self) - + min_vel = ByteReader._read_float(self) + # LV1 data - - self.ze = self.__init_nan_vector(self.raltn) + + self.ze = self.__init_nan_vector(self.raltn) self.mv = self.__init_nan_vector(self.raltn) self.sw = self.__init_nan_vector(self.raltn) self.sk = self.__init_nan_vector(self.raltn) - self.kt = self.__init_nan_vector(self.raltn) + self.kt = self.__init_nan_vector(self.raltn) self.ref_rat = self.__init_nan_vector(self.raltn) - self.corr = self.__init_nan_vector(self.raltn) - self.phi = self.__init_nan_vector(self.raltn) - self.ze45 = self.__init_nan_vector(self.raltn) - self.sldr = self.__init_nan_vector(self.raltn) - self.scorr = self.__init_nan_vector(self.raltn) - self.kdp = self.__init_nan_vector(self.raltn) + self.corr = self.__init_nan_vector(self.raltn) + self.phi = self.__init_nan_vector(self.raltn) + self.ze45 = self.__init_nan_vector(self.raltn) + self.sldr = self.__init_nan_vector(self.raltn) + self.scorr = self.__init_nan_vector(self.raltn) + self.kdp = self.__init_nan_vector(self.raltn) self.diff_at = self.__init_nan_vector(self.raltn) - + for i in range(self.raltn): - + if prof_msk[i] == 0: continue - + self.ze[i] = float(ByteReader._read_long_long(self)) / 10**7 self.mv[i] = float(ByteReader._read_long_long(self)) / 10**7 self.sw[i] = float(ByteReader._read_long_long(self)) / 10**7 self.sk[i] = float(ByteReader._read_long_long(self)) / 10**7 self.kt[i] = float(ByteReader._read_long_long(self)) / 10**7 - + if self.pol_mode == 0: continue - + self.ref_rat[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.corr[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.phi[i] = float(ByteReader._read_long_long(self)) / 10**7 - + self.corr[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.phi[i] = float(ByteReader._read_long_long(self)) / 10**7 + if self.pol_mode != 2: continue - - self.ze45[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.sldr[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.scorr[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.kdp[i] = float(ByteReader._read_long_long(self)) / 10**7 + + self.ze45[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.sldr[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.scorr[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.kdp[i] = float(ByteReader._read_long_long(self)) / 10**7 self.diff_at[i] = float(ByteReader._read_long_long(self)) / 10**7 - - def __init_nan_vector(self,n): - - v = np.empty(n) + + def __init_nan_vector(self, n): + + v = np.empty(n) v[:] = np.nan return v + class Client(Status): - - def __init__(self,HostIP,Port,Password,SuppressOutput = False): - + + def __init__(self, HostIP, Port, Password, SuppressOutput=False): + self.__HostIP = "" - self.__Port = 0 - self.__PWC = 0 - + self.__Port = 0 + self.__PWC = 0 + self.__suppressoutput = SuppressOutput - + try: socket.inet_aton(HostIP) except socket.error: raise Exception('Not valid IP4 address') - if not isinstance(Port,int): + if not isinstance(Port, int): raise Exception('The Port input variable must be an integer') if Port < 0 or Port > 65535: - raise Exception('The HostIP input variable must be in the range from 0 to 65535') - + raise Exception( + 'The HostIP input variable must be in the range from 0 to 65535') + self.__HostIP = HostIP - self.__Port = Port - self.__PWC = self.__get_password_code(Password) - - def __get_password_code(self,password): - + self.__Port = Port + self.__PWC = self.__get_password_code(Password) + + def __get_password_code(self, password): + passcode = 0 password = password.upper() - + for i in range(len(password)): passcode = passcode + ord(password[i]) * (i+1)**2 - + return(passcode) - - def __send_receive(self,msgcode,filename = None,length = None,content = None): - - MESSAGE = self.__form_request_message(msgcode,filename = filename,length = length,content = content) - + + def __send_receive(self, msgcode, filename=None, length=None, content=None): + + MESSAGE = self.__form_request_message( + msgcode, filename=filename, length=length, content=content) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - + try: - s.connect((self.__HostIP,self.__Port)) + s.connect((self.__HostIP, self.__Port)) except socket.error: raise Exception('Cannot connect to the server') - + s.sendall(MESSAGE) - + data = bytearray() - + while True: packet = s.recv(1024) if not packet: break data.extend(packet) - - if self.__check_first_byte_of_responce(data,msgcode) == False: + + if self.__check_first_byte_of_response(data, msgcode) == False: return None - + return data - - def __check_first_byte_of_responce(self,responce,msgcode): - - if responce[0] == 253: + + def __check_first_byte_of_response(self, response, msgcode): + + if response[0] == 253: if self.__suppressoutput == False: print('The requested command is not known') return False - - if responce[0] == 255: + + if response[0] == 255: if self.__suppressoutput == False: print('The password is wrong') return False - - if responce[0] is not msgcode: + + if response[0] is not msgcode: if self.__suppressoutput == False: - print('Invalid responce') + print('Invalid response') return False - + return True - - def __form_request_message(self,msgcode,filename = None,length = None,content = None): - - MESSAGE = (msgcode).to_bytes(1,byteorder='little') + \ - (self.__PWC).to_bytes(4,byteorder='little') - + + def __form_request_message(self, msgcode, filename=None, length=None, content=None): + + MESSAGE = (msgcode).to_bytes(1, byteorder='little') + \ + (self.__PWC).to_bytes(4, byteorder='little') + if filename is not None: - MESSAGE += bytearray(filename + chr(0),'utf-8') - + MESSAGE += bytearray(filename + chr(0), 'utf-8') + if length is not None: - MESSAGE += (length).to_bytes(4,byteorder='little') + \ - bytearray(content) - + MESSAGE += (length).to_bytes(4, byteorder='little') + \ + bytearray(content) + return MESSAGE - + def get_radar_status(self): - + try: - # Form the request message, 173 is the command code from the software manual - d = self.__send_receive(173) - S = Status(d,self.__suppressoutput) + # Form the request message, 173 is the command code from the software manual + d = self.__send_receive(173) + S = Status(d, self.__suppressoutput) return S except: print('Cannot get radar status') return None - def start_radar_measurements(self,mdf_name): - + def start_radar_measurements(self, mdf_name): + try: - # Form the request message, 171 is the command code from the software manual - d = self.__send_receive(171,filename = mdf_name) - + # Form the request message, 171 is the command code from the software manual + d = self.__send_receive(171, filename=mdf_name) + if self.__suppressoutput == False: - + if d[1] == 0: print('Host is not connected to radar') @@ -899,21 +912,22 @@ def start_radar_measurements(self,mdf_name): print('Specified MDF/MBF path not valid') if d[1] == 4: - print('Host is connected to Slave radar. Slave cannot start measurement') - + print( + 'Host is connected to Slave radar. Slave cannot start measurement') + return d[1] except: print('Failed to start radar measurements') return None - + def terminate_radar_measurements(self): - + try: - # Form the request message, 170 is the command code from the software manual + # Form the request message, 170 is the command code from the software manual d = self.__send_receive(170) - + if self.__suppressoutput == False: - + if d[1] == 0: print('Host is not connected to radar') @@ -933,29 +947,31 @@ def terminate_radar_measurements(self): print('cannot terminate: transmitter power calibration running') if d[1] == 6: - print('Host is connected to Slave radar! Slave cannot terminate measurement') - + print( + 'Host is connected to Slave radar! Slave cannot terminate measurement') + return d[1] except: print('Failed to terminate radar measurements') return None - - def start_radar_measurements_local_mdf(self,filename): - + + def start_radar_measurements_local_mdf(self, filename): + try: - if not isinstance(filename,str): + if not isinstance(filename, str): print('mdf_name must be a string') if not Path(filename).is_file(): print('MDF file is not found') - - f = open(filename,'r') + + f = open(filename, 'r') read_data = f.read() - m = mmap.mmap(f.fileno(),0,access=mmap.ACCESS_READ) + m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) f.close() - - d = self.__send_receive(172,filename = filename,length = len(read_data),content = m) - + + d = self.__send_receive( + 172, filename=filename, length=len(read_data), content=m) + if d[1] == 0: print('Host is not connected to radar') @@ -967,460 +983,619 @@ def start_radar_measurements_local_mdf(self,filename): if d[1] == 3: print('Host is connected to Slave radar. Slave cannot start measurement') - + return d[1] except: print('Failed to start radar measurements from local mdf') return None - def get_mdf_list(self): - + try: - d = self.__send_receive(174) - R = MDFList(d,self.__suppressoutput) + d = self.__send_receive(174) + R = MDFList(d, self.__suppressoutput) return R except: - print('Failed to get MDF list') + print('Failed to get MDF list') return None - + def get_radar_id(self): - + try: d = self.__send_receive(175) - R = RadarID(d,self.__suppressoutput) + R = RadarID(d, self.__suppressoutput) return R except: print('Failed to get radar id') return None - - def install_local_mdf(self,filename): - + def install_local_mdf(self, filename): + try: - if not isinstance(filename,str): + if not isinstance(filename, str): print('mdf_name must be a string') if not Path(filename).is_file(): print('MDF file is not found') - f = open(filename,'r') + f = open(filename, 'r') read_data = f.read() - m = mmap.mmap(f.fileno(),0,access=mmap.ACCESS_READ) + m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) f.close() - + a = filename.split('\\') - - d = self.__send_receive(176,filename = a[-1] + chr(0),length = len(read_data),content = m) - + + d = self.__send_receive( + 176, filename=a[-1] + chr(0), length=len(read_data), content=m) + if d is None: return False - - if self.__suppressoutput == False: + + if self.__suppressoutput == False: print('The file has been successfully transferred') - + return True except: print('Failed to install mdf on the radar') return False - + def get_last_sample(self): - + d = self.__send_receive(177) - S = LastSample(d,self.__suppressoutput) + S = LastSample(d, self.__suppressoutput) return S - - + + class Scan: - - def __init__(self,elv = 90,azm = 0,elv_target = None,azm_target = None,elv_spd = 1.0,azm_spd = 1.0): - + + def __init__(self, + elv=90, + azm=0, + elv_target=None, + azm_target=None, + elv_spd=1.0, + azm_spd=1.0, + align_with_wind=0, + ): + if elv < 0.0 or elv > 180.0: - + raise Exception('Wrong elevation') - + if azm < 0.0 or azm > 360.0: - + raise Exception('Wrong azimuth') - + if elv_target is not None: - + if elv_target < 0.0 or elv_target > 180.0: raise Exception('Wrong target elevation') - + if azm_target is not None: - + if azm_target < 0.0 or azm_target > 360.0: - + raise Exception('Wrong target azimuth') - + if elv_spd < 0.0 or elv_spd > 5.0: - + raise Exception('Wrong elevation speed') - + if azm_spd < 0.0 or azm_spd > 5.0: - + raise Exception('Wrong azimuth speed') - + # constant angle if elv_target is None and azm_target is None: - + self.ScanType = 0 - self.ConstEl = elv - self.ConstAz = azm - + self.ConstEl = elv + self.ConstAz = azm + return - + if elv_target is None or azm_target is None: - + raise Exception('Wrong targets') - + self.ScanType = 1 self.ScanMode = 0 - self.ScanStartEl = elv - self.ScanStopEl = elv_target - self.ScanIncEl = 0 - self.ScanSpeedEl = elv_spd + self.ScanStartEl = elv + self.ScanStopEl = elv_target + self.ScanIncEl = 0 + self.ScanSpeedEl = elv_spd + + self.ScanStartAz = azm + self.ScanStopAz = azm_target + self.ScanIncAz = 0 + self.ScanSpeedAz = azm_spd + + self.AzWindAlign = align_with_wind + - self.ScanStartAz = azm - self.ScanStopAz = azm_target - self.ScanIncAz = 0 - self.ScanSpeedAz = azm_spd - class MeasDefFile: - + def __init__(self): - + self.__filecode = 48856 - - def __write_to_file(self,file_id,var_type,val_list): - + + def __write_to_file(self, file_id, var_type, val_list): + A = array(var_type, val_list) A.tofile(file_id) - - def create(self, \ - filename, \ - chirp_prg, \ - scan, \ - cal_int = 3600, - timing = False, \ - duration = 3600, \ - filelen = 3600, \ - start = False, \ - start_time = 0, \ - trig = 0, \ - LV0 = True, - LV1 = True, \ - NoiseT = 6.0, - windfunc = 4): - + + def create(self, + filename, + chirp_prg, + scan_or_list_of_scans, + # the default of None makes a frame that just goes through + # all the defines scans once without repetion + frames=None, + cal_int=3600, + timing=False, + duration=3600, + filelen=3600, + # comment spirrobe: start = False (=0) means immediately + start=False, + start_time=0, + trig=0, + LV0=True, + LV1=True, + NoiseT=6.0, + windfunc=4, + basename='rpgfmcwscan_'): + SpecCmp = True - - # general scans must be limited in time - if scan.ScanType == 1 and timing == True: + + _windfuncs = {'Rectangle': 0, + 'Parzen': 1, + 'Blackman': 2, + 'Welch': 3, + 'Slepian2': 4, + 'Slepian3': 5, + } + + if isinstance(windfunc, str): + windfunc = _windfuncs.get(windfunc.lower(), False) + if windfunc is False or windfunc not in list(range(6)): + raise ValueError('Windowfunction has to be either integer or', + f'a known name ({_windfuncs.keys()})') + + if isinstance(scan_or_list_of_scans, list): + scans = scan_or_list_of_scans + else: + scans = [scan_or_list_of_scans] + + # Comment spirrobe: + # assert that all scans are of the same type, albeit it is unclear + # if this would not be supported in principle but made impossible + # by the MDF structure + if all(scans[0].ScanType == scan.ScanType for scan in scans): + scantype = scans[0].ScanType + if (scantype == 1 + and all(scans[0].ScanMode == scan.ScanMode for scan in scans)): + scanmode = scans[0].ScanMode + else: + raise ValueError("Mixing of different scanmodes", + " is not possible with the MDF file structure") + else: + raise ValueError("Mixing of different scantypes", + " is not possible with the MDF file structure") + + + # the framenumbers, in order + # the framecount (default 1) as the len of the nested list + # the start scans (default 0) + # the stop scans (default 0) + # the repetitions per frame (default 1) + if frames is None: + frames = [[0, len(scans), 1]] + framecount = len(frames) + framestarts = [frame[0] for frame in frames] + + # warn that it never includes the scan 0 indicating an issue + if min(framestarts) > 0: + print('Scan numbering starts at index 0, which was not found', + 'in the frames definition, are you sure you defined it right?') + + framestops = [frame[1] for frame in frames] + # warn that it goes over the number of defined scans + if min(framestarts) > 0: + print('Scan numbering exceeds the number of available scans', + ', are you sure you defined it right?') + + output_file = open(filename, 'wb') + + self.__write_to_file(output_file, 'i', [ + self.__filecode, chirp_prg, cal_int]) + self.__write_to_file(output_file, 'b', [LV0, LV1, SpecCmp, 0, 0, 0, 0]) + self.__write_to_file(output_file, 'f', [NoiseT]) + + # the passing in of scans makes the most sense as a list + # but for the MDF format the parameters of the scans are in parameter + # order so we have to adjust this accordingly here. + self.__write_to_file(output_file, + 'b', + [scan.ScanType for scan in scans]) + + if scantype == 0: + self.__write_to_file(output_file, 'f', + [scan.ConstEl for scan in scans]) + + self.__write_to_file(output_file, 'f', + [scan.ConstAz for scan in scans]) + elif scantype == 1: + # header type for scantype 1, scanmode refers to continuous + self.__write_to_file(output_file, 'b', [scanmode]) + # this is the ScanCnt + self.__write_to_file(output_file, 'i', [len(scans)]) + self.__write_to_file(output_file, 'f', [self.ScanStartEl, + self.ScanStopEl, + self.ScanIncEl, + self.ScanSpeedEl, + self.ScanStartAz, + self.ScanStopAz, + self.ScanIncAz, + self.ScanSpeedAz]) + + self.__write_to_file(output_file, 'b', self.AzWindAlign) + + self.__write_to_file(output_file, 'i', framecount) + self.__write_to_file(output_file, 'i', framestarts) + self.__write_to_file(output_file, 'i', framestops) + self.__write_to_file(output_file, 'i', [frame[2] for frame in frames]) + + + # general scans must be limited in time, this is + if scantype == 1 and timing is True: timing = False - - output_file = open(filename,'wb') - - self.__write_to_file(output_file,'i',[self.__filecode, chirp_prg, cal_int]) - self.__write_to_file(output_file,'b',[LV0, LV1, SpecCmp, 0, 0, 0, 0]) - self.__write_to_file(output_file,'f',[NoiseT]) - self.__write_to_file(output_file,'b',[scan.ScanType]) - - if scan.ScanType == 0: - self.__write_to_file(output_file,'f',[scan.ConstEl,scan.ConstAz]) - - if scan.ScanType == 1: - self.__write_to_file(output_file,'b',[scan.ScanMode]) - self.__write_to_file(output_file,'i',[1]) - self.__write_to_file(output_file,'f',[scan.ScanStartEl,scan.ScanStopEl,scan.ScanIncEl,scan.ScanSpeedEl,scan.ScanStartAz,scan.ScanStopAz,scan.ScanIncAz,scan.ScanSpeedAz]) - self.__write_to_file(output_file,'b',[0]) - self.__write_to_file(output_file,'i',[1, 0, 0, 1]) - - self.__write_to_file(output_file,'b',[timing]) - - if timing == False: - self.__write_to_file(output_file,'i',[duration, 0]) - - if timing == True: - self.__write_to_file(output_file,'i',[filelen]) - - self.__write_to_file(output_file,'b',[start]) - + + self.__write_to_file(output_file, 'b', [timing]) + + # timing can either be False or True + if timing is False: + self.__write_to_file(output_file, 'i', [duration, len(basename)]) + self.__write_to_file(output_file, 'i', [basename]) + elif timing: + self.__write_to_file(output_file, 'i', [filelen]) + + self.__write_to_file(output_file, 'b', [start]) + if start == 1: - self.__write_to_file(output_file,'i',[start_time, trig]) - - self.__write_to_file(output_file,'i',[windfunc, 1000]) - + self.__write_to_file(output_file, 'i', [start_time, trig]) + + self.__write_to_file(output_file, 'i', [windfunc, 1000]) + output_file.close() - + def output(self): - + if self.filecode != self.__filecode: print("No loaded MDF file") return - - print("Chirp generator Program Index: ",self.CGProgNo) - print("Calibration interval [s]: ",self.ZeroCallInt) - print("Store LV0: ",self.Lev0Ena) - print("Store LV1: ",self.Lev1Ena) - print("Spectral compression: ",self.SpecCompEna) - print("Store polarimetric spectral parameters in LV0: ",self.SpecParLV0Ena) - print("File Backup: ",self.FileBackupEna) - print("Antialiasing: ",self.AntiAliasEna) - print("Suppress Power Leveling: ",self.PowLevSupEna) - print("Noise Threshold Factor: ",self.NoiseThresh) - + print('#'*20, '\nGeneral parameters', '\n'+'#'*20) + print("Chirp generator Program Index: ", self.CGProgNo) + print("Calibration interval [s]: ", self.ZeroCallInt) + print("Store LV0: ", self.Lev0Ena) + print("Store LV1: ", self.Lev1Ena) + print("Spectral compression: ", self.SpecCompEna) + print("Store polarimetric spectral parameters in LV0: ", self.SpecParLV0Ena) + print("File Backup: ", self.FileBackupEna) + print("Antialiasing: ", self.AntiAliasEna) + print("Suppress Power Leveling: ", self.PowLevSupEna) + print("Noise Threshold Factor: ", self.NoiseThresh) + + print('\n'+'#'*20, '\nOperational parameters', '\n'+'#'*20) if self.ScanType == 0: - print('Radar operates at elevation {0:.1f} and azimuth {0:.1f} deg'.format(self.ConstEl,self.ConstAz)) - - if self.ScanType == 1: + print('Radar operates at elevation {0:.1f} and azimuth {0:.1f} deg'.format( + self.ConstEl, self.ConstAz)) + + elif self.ScanType == 1: print("Radar performs a general scan") - + if self.ScanMode == 0: print("Scan Mode: Continuous ") - + if self.ScanMode == 1: print("Scan Mode: Discrete, stop at each sample ") - + for i in range(self.ScanCnt): - - print('Scan index: ',i) - print('Elevation from {0:.1f} to {1:.1f} deg'.format(self.ScanStartEl[i],self.ScanStopEl[i])) - print('Elevation increment angle [deg]: ',self.ScanIncEl[i]) - print('Elevation speed [deg/s]: ',self.ScanSpeedEl[i]) - print('Azimuth from {0:.1f} to {1:.1f} deg'.format(self.ScanStartAz[i],self.ScanStopAz[i])) - print('Azimuth increment angle [deg]: ',self.ScanIncAz[i]) - print('Azimuth speed [deg/s]: ',self.ScanSpeedAz[i]) - + + print('-'*20, '\nScan index: ', i) + print('Elevation from {0:.1f} to {1:.1f} deg'.format( + self.ScanStartEl[i], self.ScanStopEl[i])) + print('Elevation increment angle [deg]: ', self.ScanIncEl[i]) + print('Elevation speed [deg/s]: ', self.ScanSpeedEl[i]) + print('Azimuth from {0:.1f} to {1:.1f} deg'.format( + self.ScanStartAz[i], self.ScanStopAz[i])) + print('Azimuth increment angle [deg]: ', self.ScanIncAz[i]) + print('Azimuth speed [deg/s]: ', self.ScanSpeedAz[i]) + print('Align to wind [deg/s]: ', self.AzWindAlign[i]) + + print('\n'+'#'*20, '\nRepetion and order of scans', '\n'+'#'*20) for i in range(self.FrameCnt): - - print('Frame index: ',i) - print('Frame start scan: ',self.FrameStartScn[i]) - print('Frame stop scan: ',self.FrameStopScn[i]) - print('Frame repetition number: ',self.FrameRep[i]) - + + print('Frame index: ', i) + print('Frame start scan: ', self.FrameStartScn[i]) + print('Frame stop scan: ', self.FrameStopScn[i]) + print('Frame repetition number: ', self.FrameRep[i]) + if self.Timing == 0: - + print('Limited measurement') - print('Duration [s]: ',self.Duration) - + print('Duration [s]: ', self.Duration) + if self.Timing == 1: - + print('Unlimited measurement') - print('File Lenght [s]',self.FileLen) - + print('File Length [s]', self.FileLen) + if self.MeasStart == 1: - - print('Start time: ',self.StartTime) - print('Trigger condition: ',self.TrigCond) + + print('Start time: ', self.StartTime) + print('Trigger condition: ', self.TrigCond) if self.WindFunc == 0: - + print('Window Function: Rectangle') - + if self.WindFunc == 1: - + print('Window Function: Parzen') - + if self.WindFunc == 2: - + print('Window Function: Blackman') - + if self.WindFunc == 3: - + print('Window Function: Welch') - + if self.WindFunc == 4: - + print('Window Function: Slepian2') - + if self.WindFunc == 5: - + print('Window Function: Slepian3') - - print('ADC voltage range [V]: ',self.ADCVoltRng/1000) - - def read(self,filepath): - + + print('ADC voltage range [mV]: ', self.ADCVoltRng/1000) + + def read(self, filepath): + with open(filepath, mode='rb') as file: - + # can close the file here already .. data = file.read() - - R = ByteReader(data,0) - - self.filecode = R._read_int() - - if self.filecode != 48856: - raise Exception("Filecode does not correspond to MDF format") - - self.CGProgNo = R._read_int() - self.ZeroCallInt = R._read_int() - self.Lev0Ena = R._read_byte() - self.Lev1Ena = R._read_byte() - self.SpecCompEna = R._read_byte() - self.SpecParLV0Ena = R._read_byte() - self.FileBackupEna = R._read_byte() - self.AntiAliasEna = R._read_byte() - self.PowLevSupEna = R._read_byte() - self.NoiseThresh = R._read_float() - self.ScanType = R._read_byte() - - if self.ScanType == 0: - self.ConstEl = R._read_float() - self.ConstAz = R._read_float() - - if self.ScanType == 1: - self.ScanMode = R._read_byte() - self.ScanCnt = R._read_int() - - self.ScanStartEl = [] - self.ScanStopEl = [] - self.ScanIncEl = [] - self.ScanSpeedEl = [] - self.ScanStartAz = [] - self.ScanStopAz = [] - self.ScanIncAz = [] - self.ScanSpeedAz = [] - self.AzWindAlign = [] - - for i in range(self.ScanCnt): - self.ScanStartEl.append(R._read_float()) - self.ScanStopEl.append(R._read_float()) - self.ScanIncEl.append(R._read_float()) - self.ScanSpeedEl.append(R._read_float()) - self.ScanStartAz.append(R._read_float()) - self.ScanStopAz.append(R._read_float()) - self.ScanIncAz.append(R._read_float()) - self.ScanSpeedAz.append(R._read_float()) - self.AzWindAlign.append(R._read_byte()) - - self.FrameCnt = R._read_int() - - self.FrameStartScn = [] - self.FrameStopScn = [] - self.FrameRep = [] - - for i in range(self.FrameCnt): - - self.FrameStartScn.append(R._read_int()) - self.FrameStopScn.append(R._read_int()) - self.FrameRep.append(R._read_int()) - - self.Timing = R._read_byte() - - if self.Timing == 0: - - self.Duration = R._read_int() - self.BaseNmLen = R._read_int() - - if self.Timing == 1: - - self.FileLen = R._read_int() - - self.MeasStart = R._read_byte() - - if self.MeasStart == 1: - - self.StartTime = R._read_int() - self.TrigCond = R._read_int() - - self.WindFunc = R._read_int() - self.ADCVoltRng = R._read_int() - -def get_radar_status(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.get_radar_status() - -def start_radar_measurements(HOSTIP,PORT,PASSWORD,MDF_FILENAME): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.start_radar_measurements(MDF_FILENAME) - -def start_radar_measurements_local_mdf(HOSTIP,PORT,PASSWORD,MDF_FILENAME): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.start_radar_measurements_local_mdf(MDF_FILENAME) - -def terminate_radar_measurements(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.terminate_radar_measurements() - -def get_mdf_list(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.get_mdf_list() - -def get_radar_id(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.get_radar_id() - -def install_local_mdf(HOSTIP,PORT,PASSWORD,MDF_FILENAME): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.install_local_mdf(MDF_FILENAME) - -def get_last_sample(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD,SuppressOutput = True) - X.get_last_sample() + + R = ByteReader(data, 0) + + self.filecode = R._read_int() + + if self.filecode != 48856: + raise Exception("Filecode does not correspond to MDF format") + + self.CGProgNo = R._read_int() + self.ZeroCallInt = R._read_int() + self.Lev0Ena = R._read_byte() + self.Lev1Ena = R._read_byte() + self.SpecCompEna = R._read_byte() + self.SpecParLV0Ena = R._read_byte() + self.FileBackupEna = R._read_byte() + self.AntiAliasEna = R._read_byte() + self.PowLevSupEna = R._read_byte() + self.NoiseThresh = R._read_float() + self.ScanType = R._read_byte() + + if self.ScanType == 0: + self.ConstEl = R._read_float() + self.ConstAz = R._read_float() + + if self.ScanType == 1: + self.ScanMode = R._read_byte() + self.ScanCnt = R._read_int() + + self.ScanStartEl = [] + self.ScanStopEl = [] + self.ScanIncEl = [] + self.ScanSpeedEl = [] + self.ScanStartAz = [] + self.ScanStopAz = [] + self.ScanIncAz = [] + self.ScanSpeedAz = [] + self.AzWindAlign = [] + + # 8 vars for each scan to be read, 4 elv, 4 az + scandata = [[R._read_float() for j in range(self.ScanCnt)] + for i in range(8)] + + # elevation related params + self.ScanStartEl.extend(scandata[0]) + self.ScanStopEl.extend(scandata[1]) + self.ScanIncEl.extend(scandata[2]) + self.ScanSpeedEl.extend(scandata[3]) + + # azimuth related params + self.ScanStartAz.extend(scandata[4]) + self.ScanStopAz.extend(scandata[5]) + self.ScanIncAz.extend(scandata[6]) + self.ScanSpeedAz.extend(scandata[7]) + + # whether to align the azimuth into the wind, + # for each scan a byte + self.AzWindAlign.extend([R._read_byte() + for j in range(self.ScanCnt)]) + + # comment spirrobe + # maybe this needs to be refactored to in case there are + # several frames. requires testing. + self.FrameCnt = R._read_int() + + self.FrameStartScn = [] + self.FrameStopScn = [] + self.FrameRep = [] + + self.FrameStartScn.extend([R._read_int() + for j in range(self.FrameCnt)]) + self.FrameStopScn.extend([R._read_int() + for j in range(self.FrameCnt)]) + self.FrameRep.extend([R._read_int() + for j in range(self.FrameCnt)]) + + self.Timing = R._read_byte() + + if self.Timing == 0: + + self.Duration = R._read_int() + self.BaseNmLen, self.BaseNm = R._read_int(), R._read_string() + + elif self.Timing == 1: + + self.FileLen = R._read_int() + + self.MeasStart = R._read_byte() + + if self.MeasStart == 1: + + self.StartTime = R._read_int() + self.TrigCond = R._read_int() + + self.WindFunc = R._read_int() + # comment spirrobe: + # is this really the ADCVoltRng in an RPG FMCW? + # here in the RPG FMCW it is always 1000 (see also .create method) + # is this an internal scaling factor? what does it scale? + self.ADCVoltRng = R._read_int() + self.extra = R._read_signed_short() + + # comment spirrobe: + # this was the number I found during testing, as I do not know + # what the 256 stands for in this case there could be other cases + # where something else is defined in the MDF. + if self.extra == 256: + self.BaseNmLen, self.BaseNm = R._read_int(), R._read_string() + + if R._ByteReader__i == R._ByteReader__input.__len__(): + print('Finished processing {filepath}') + else: + print(f'{R._ByteReader__i} bytes of', + f'{R._ByteReader__input.__len__()} bytes read') + print(f'{R._ByteReader__input[R._ByteReader__i:]} remains') + + +def get_radar_status(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.get_radar_status() + + +def start_radar_measurements(HOSTIP, PORT, PASSWORD, MDF_FILENAME): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.start_radar_measurements(MDF_FILENAME) + + +def start_radar_measurements_local_mdf(HOSTIP, PORT, PASSWORD, MDF_FILENAME): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.start_radar_measurements_local_mdf(MDF_FILENAME) + + +def terminate_radar_measurements(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.terminate_radar_measurements() + + +def get_mdf_list(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.get_mdf_list() + + +def get_radar_id(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.get_radar_id() + + +def install_local_mdf(HOSTIP, PORT, PASSWORD, MDF_FILENAME): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.install_local_mdf(MDF_FILENAME) + + +def get_last_sample(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD, SuppressOutput=True) + X.get_last_sample() + if __name__ == '__main__': - + status = False - + if len(sys.argv) > 1: if sys.argv[1] == "get_radar_status": if len(sys.argv) == 3: if sys.argv[2] == "help": print('\nCommand template:\n') - print('python RadarControl.py get_radar_status HOSTIP PORT PASSWORD') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print( + 'python RadarControl.py get_radar_status HOSTIP PORT PASSWORD') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') status = True if sys.argv[1] == "start_radar_measurements": if len(sys.argv) == 3: if sys.argv[2] == "help": print('\nCommand template:\n') - print('python RadarControl.py start_radar_measurements HOSTIP PORT PASSWORD MDF_FILENAME') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print( + 'python RadarControl.py start_radar_measurements HOSTIP PORT PASSWORD MDF_FILENAME') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') print('MDF_FILENAME is a string with a MDF/MBF filename on the host PC in the format *.MDF/*.MBF. The string can also contain the full path if the needed file is not in the default MDF / MBF directory.') - print('In order to get the list of available MDF/MBF files in the default MDF / MBF directory, refer to the get_mdf_list command.') + print( + 'In order to get the list of available MDF/MBF files in the default MDF / MBF directory, refer to the get_mdf_list command.') status = True if sys.argv[1] == "start_radar_measurements_local_mdf": if len(sys.argv) == 3: if sys.argv[2] == "help": print('\nCommand template:\n') - print('python RadarControl.py start_radar_measurements_local_mdf HOSTIP PORT PASSWORD MDF_FILENAME') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print( + 'python RadarControl.py start_radar_measurements_local_mdf HOSTIP PORT PASSWORD MDF_FILENAME') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') print('MDF_FILENAME is a string with a MDF/MBF filename on the local PC in the format path/*.MDF (path/*.MBF). The string must contain the full path. The MDF/MBF file will be transferred to the host PC and launched. Please note, that the file will NOT be stored in the default MDF/MBF folder on the host PC.') status = True @@ -1428,10 +1603,14 @@ def get_last_sample(HOSTIP,PORT,PASSWORD): if len(sys.argv) == 3: if sys.argv[2] == "help": print('\nCommand template:\n') - print('python RadarControl.py terminate_radar_measurements HOSTIP PORT PASSWORD') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print( + 'python RadarControl.py terminate_radar_measurements HOSTIP PORT PASSWORD') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') status = True if sys.argv[1] == "get_mdf_list": @@ -1439,9 +1618,12 @@ def get_last_sample(HOSTIP,PORT,PASSWORD): if sys.argv[2] == "help": print('\nCommand template:\n') print('python RadarControl.py get_mdf_list HOSTIP PORT PASSWORD') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') status = True if sys.argv[1] == "get_radar_id": @@ -1449,28 +1631,36 @@ def get_last_sample(HOSTIP,PORT,PASSWORD): if sys.argv[2] == "help": print('\nCommand template:\n') print('python RadarControl.py get_radar_id HOSTIP PORT PASSWORD') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') status = True if sys.argv[1] == "install_local_mdf": if len(sys.argv) == 3: if sys.argv[2] == "help": print('\nCommand template:\n') - print('python RadarControl.py install_local_mdf HOSTIP PORT PASSWORD MDF_FILENAME') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print( + 'python RadarControl.py install_local_mdf HOSTIP PORT PASSWORD MDF_FILENAME') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') print('MDF_FILENAME is a string with a MDF/MBF filename on the local PC in the format path/*.MDF (path/*.MBF). The string must contain the full path. The MDF/MBF file will be transferred and stores in the default MDF/MBF folder on the host PC. Please note, that the measurements will NOT be started. Please refer to the start_radar_measurements command.') status = True if len(sys.argv) == 5: - globals()[sys.argv[1]](sys.argv[2],sys.argv[3],sys.argv[4]) + globals()[sys.argv[1]](sys.argv[2], sys.argv[3], sys.argv[4]) status = True - + if len(sys.argv) == 6: - globals()[sys.argv[1]](sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5]) + globals()[sys.argv[1]](sys.argv[2], + sys.argv[3], sys.argv[4], sys.argv[5]) status = True if status == False: @@ -1485,4 +1675,4 @@ def get_last_sample(HOSTIP,PORT,PASSWORD): print('install_local_mdf') print('\nIn order to get help for a certain command use the following command:') - print('python RadarControl.py CommandName "help"') \ No newline at end of file + print('python RadarControl.py CommandName "help"') From 129a6fa9ad759b5f963c8324852bad3e16e95912 Mon Sep 17 00:00:00 2001 From: spirrobe Date: Fri, 22 Dec 2023 16:52:17 +0100 Subject: [PATCH 02/11] Update RadarControl.py fixing of .create for scanlist but some issue with re-reading.. --- RadarControl.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/RadarControl.py b/RadarControl.py index cd27018..7de7bf5 100644 --- a/RadarControl.py +++ b/RadarControl.py @@ -1194,7 +1194,6 @@ def create(self, frames = [[0, len(scans), 1]] framecount = len(frames) framestarts = [frame[0] for frame in frames] - # warn that it never includes the scan 0 indicating an issue if min(framestarts) > 0: print('Scan numbering starts at index 0, which was not found', @@ -1230,19 +1229,32 @@ def create(self, # header type for scantype 1, scanmode refers to continuous self.__write_to_file(output_file, 'b', [scanmode]) # this is the ScanCnt + self.__write_to_file(output_file, 'i', [len(scans)]) - self.__write_to_file(output_file, 'f', [self.ScanStartEl, - self.ScanStopEl, - self.ScanIncEl, - self.ScanSpeedEl, - self.ScanStartAz, - self.ScanStopAz, - self.ScanIncAz, - self.ScanSpeedAz]) - - self.__write_to_file(output_file, 'b', self.AzWindAlign) - - self.__write_to_file(output_file, 'i', framecount) + self.__write_to_file(output_file, 'f', + [scan.ScanStartEl for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanStopEl for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanIncEl for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanSpeedEl for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanStartAz for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanStopAz for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanIncAz for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanSpeedAz for scan in scans]) + + self.__write_to_file(output_file, 'f', + [scan.ScanSpeedAz for scan in scans]) + + self.__write_to_file(output_file, 'b', + [scan.AzWindAlign for scan in scans]) + + self.__write_to_file(output_file, 'i', [framecount]) self.__write_to_file(output_file, 'i', framestarts) self.__write_to_file(output_file, 'i', framestops) self.__write_to_file(output_file, 'i', [frame[2] for frame in frames]) @@ -1257,7 +1269,9 @@ def create(self, # timing can either be False or True if timing is False: self.__write_to_file(output_file, 'i', [duration, len(basename)]) - self.__write_to_file(output_file, 'i', [basename]) + self.__write_to_file(output_file, 'b', + [i for i in basename.encode('UTF-8')] + ) elif timing: self.__write_to_file(output_file, 'i', [filelen]) From 91eaef3af002d6479d62234333ef5621871a07c5 Mon Sep 17 00:00:00 2001 From: spirrobe Date: Sat, 23 Dec 2023 00:59:51 +0100 Subject: [PATCH 03/11] Update RadarControl.py fixed reading/writing of mdf and tested with different configurations, examples follow --- RadarControl.py | 57 ++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/RadarControl.py b/RadarControl.py index 7de7bf5..662505b 100644 --- a/RadarControl.py +++ b/RadarControl.py @@ -1140,6 +1140,7 @@ def create(self, # comment spirrobe: start = False (=0) means immediately start=False, start_time=0, + # unclear how to support the ignore date, ignore hour trig=0, LV0=True, LV1=True, @@ -1217,8 +1218,8 @@ def create(self, # order so we have to adjust this accordingly here. self.__write_to_file(output_file, 'b', - [scan.ScanType for scan in scans]) - + [scantype]) + # scan.ScanType for scan in scans]) if scantype == 0: self.__write_to_file(output_file, 'f', [scan.ConstEl for scan in scans]) @@ -1229,8 +1230,9 @@ def create(self, # header type for scantype 1, scanmode refers to continuous self.__write_to_file(output_file, 'b', [scanmode]) # this is the ScanCnt - self.__write_to_file(output_file, 'i', [len(scans)]) + + # elevation related 4 params self.__write_to_file(output_file, 'f', [scan.ScanStartEl for scan in scans]) self.__write_to_file(output_file, 'f', @@ -1239,6 +1241,8 @@ def create(self, [scan.ScanIncEl for scan in scans]) self.__write_to_file(output_file, 'f', [scan.ScanSpeedEl for scan in scans]) + + # azimuth related 4 params self.__write_to_file(output_file, 'f', [scan.ScanStartAz for scan in scans]) self.__write_to_file(output_file, 'f', @@ -1248,9 +1252,7 @@ def create(self, self.__write_to_file(output_file, 'f', [scan.ScanSpeedAz for scan in scans]) - self.__write_to_file(output_file, 'f', - [scan.ScanSpeedAz for scan in scans]) - + # elevation related 4 params self.__write_to_file(output_file, 'b', [scan.AzWindAlign for scan in scans]) @@ -1267,12 +1269,13 @@ def create(self, self.__write_to_file(output_file, 'b', [timing]) # timing can either be False or True - if timing is False: - self.__write_to_file(output_file, 'i', [duration, len(basename)]) + if timing == 0: + self.__write_to_file(output_file, 'i', [duration]) + self.__write_to_file(output_file, 'i', [len(basename)]) self.__write_to_file(output_file, 'b', - [i for i in basename.encode('UTF-8')] + [ord(i) for i in basename] ) - elif timing: + elif timing == 1: self.__write_to_file(output_file, 'i', [filelen]) self.__write_to_file(output_file, 'b', [start]) @@ -1419,7 +1422,6 @@ def read(self, filepath): self.ScanIncAz = [] self.ScanSpeedAz = [] self.AzWindAlign = [] - # 8 vars for each scan to be read, 4 elv, 4 az scandata = [[R._read_float() for j in range(self.ScanCnt)] for i in range(8)] @@ -1441,11 +1443,12 @@ def read(self, filepath): self.AzWindAlign.extend([R._read_byte() for j in range(self.ScanCnt)]) + + # comment spirrobe # maybe this needs to be refactored to in case there are # several frames. requires testing. self.FrameCnt = R._read_int() - self.FrameStartScn = [] self.FrameStopScn = [] self.FrameRep = [] @@ -1458,18 +1461,16 @@ def read(self, filepath): for j in range(self.FrameCnt)]) self.Timing = R._read_byte() - if self.Timing == 0: - self.Duration = R._read_int() - self.BaseNmLen, self.BaseNm = R._read_int(), R._read_string() + self.BaseNmLen = R._read_int() + self.BaseNm = ''.join([chr(R._read_byte()) + for j in range(self.BaseNmLen)]) elif self.Timing == 1: - self.FileLen = R._read_int() self.MeasStart = R._read_byte() - if self.MeasStart == 1: self.StartTime = R._read_int() @@ -1479,20 +1480,22 @@ def read(self, filepath): # comment spirrobe: # is this really the ADCVoltRng in an RPG FMCW? # here in the RPG FMCW it is always 1000 (see also .create method) - # is this an internal scaling factor? what does it scale? self.ADCVoltRng = R._read_int() - self.extra = R._read_signed_short() - - # comment spirrobe: - # this was the number I found during testing, as I do not know - # what the 256 stands for in this case there could be other cases - # where something else is defined in the MDF. - if self.extra == 256: - self.BaseNmLen, self.BaseNm = R._read_int(), R._read_string() if R._ByteReader__i == R._ByteReader__input.__len__(): - print('Finished processing {filepath}') + print(f'Finished processing {filepath}') else: + # this is pure guesswork based on making different MDF files via + # the gui + self.extra = R._read_signed_short() + + # comment spirrobe: + # this was the number I found during testing, as I do not know + # what the 256 stands for in this case there could be other cases + # where something else is defined in the MDF. + if self.extra == 256: + self.BaseNmLen, self.BaseNm = R._read_int(), R._read_string() + print(f'{R._ByteReader__i} bytes of', f'{R._ByteReader__input.__len__()} bytes read') print(f'{R._ByteReader__input[R._ByteReader__i:]} remains') From ca85c04568bdd856d02bac1197d40f1f12485113 Mon Sep 17 00:00:00 2001 From: Robert Spirig Date: Tue, 2 Jan 2024 15:19:09 +0100 Subject: [PATCH 04/11] addition of MBF files, fix of MDF with proper filenames and examples of forth/back scanning as well as single ppi/rhi scans --- LICENSE | 1008 +++++++-------- README.md | 112 +- RadarControl.py | 3299 ++++++++++++++++++++++++++--------------------- scan_rpgfmcw.py | 408 ++++++ 4 files changed, 2779 insertions(+), 2048 deletions(-) create mode 100644 scan_rpgfmcw.py diff --git a/LICENSE b/LICENSE index 8000a6f..bc0390e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,504 +1,504 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random - Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/README.md b/README.md index bb31571..e18ac08 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,56 @@ -# Supplementary scripts for meteorological equipment from RPG - -The main aim of this repository is to make selected modules developed at RPG (Radiometer Physics GmbH) available for the scientific (meteorological) community. - -## License - -Copyright (c) Radiometer Physics GmbH. All rights reserved. - -The software is licensed under [GNU Lesser General Public License v2.1](./LICENSE). - -## Acknowledgement - -If you find the provided modules useful for you study, we kindly ask to acknowledge in publications our efforts to provide *free* and *open-source* solutions for the scientific community. These acknowledgements will help us to fulfil requirements for a publication in -the Journal of Open Source Software. - -## Found a bug? - -Please contact us via email: - -## Interested in contributing to the develoment of modules? Have an idea how to further improve functionality? - -Please send us your ideas and suggestions to - -## Included modules - -### RadarControl.py - -This module contains a Python API class that provides a platform independent interface to control RPG FMCW cloud radars. The class can be used for a development of adaptive observation strategies. The class provides an access to the latest measured sample (both integrated and spectral measured quantities). Based on user anaysis of the last sample, scanning and measurement settings can be switched in near-real time. The class can also be used to access housekeeping data and to monitor the radar status. In addition to the functions which can be used in the users scripts, the module also provides command line interface to control RPG FMCW radars directly from a command prompt. - -Usage of the module from a command prompt: - -The Python module provides an assistance in using available commands. This way users do not need to browse through the code to understand how to use the class. In order to use the module, please open the command promt and change the working directory to the one containing the python module. In order to get a list of available functions, execute the following command in the command prompt (syntaxis is for Windows, in Linux and MAC OS it may differ): -``` -> python RadarControl.py -``` - -In order to get a syntax template for a command (COMMAND) please execute the following: -``` -> python RadarControl.py COMMAND "help" -``` - -Currently implemented functionality for the command prompt use is listed below: - -`get_radar_status` provides basic information about the current radar activity. - -`start_radar_measurements` starts radar measurement using a MDF file on the host PC. - -`start_radar_measurements_local_mdf` starts radar measurements using a MDF located on the user PC (MDF is not copied to the host). - -`terminate_radar_measurements` stops currently running radar measurements. - -`get_mdf_list` provides a list of MDF files available on the host PC in the default folder. - -`get_radar_id` provides information about the radar. - -`install_local_mdf` copies an MDF from the user PC to the host PC. The MDF is not started. +# Supplementary scripts for meteorological equipment from RPG + +The main aim of this repository is to make selected modules developed at RPG (Radiometer Physics GmbH) available for the scientific (meteorological) community. + +## License + +Copyright (c) Radiometer Physics GmbH. All rights reserved. + +The software is licensed under [GNU Lesser General Public License v2.1](./LICENSE). + +## Acknowledgement + +If you find the provided modules useful for you study, we kindly ask to acknowledge in publications our efforts to provide *free* and *open-source* solutions for the scientific community. These acknowledgements will help us to fulfil requirements for a publication in +the Journal of Open Source Software. + +## Found a bug? + +Please contact us via email: + +## Interested in contributing to the develoment of modules? Have an idea how to further improve functionality? + +Please send us your ideas and suggestions to + +## Included modules + +### RadarControl.py + +This module contains a Python API class that provides a platform independent interface to control RPG FMCW cloud radars. The class can be used for a development of adaptive observation strategies. The class provides an access to the latest measured sample (both integrated and spectral measured quantities). Based on user anaysis of the last sample, scanning and measurement settings can be switched in near-real time. The class can also be used to access housekeeping data and to monitor the radar status. In addition to the functions which can be used in the users scripts, the module also provides command line interface to control RPG FMCW radars directly from a command prompt. + +Usage of the module from a command prompt: + +The Python module provides an assistance in using available commands. This way users do not need to browse through the code to understand how to use the class. In order to use the module, please open the command promt and change the working directory to the one containing the python module. In order to get a list of available functions, execute the following command in the command prompt (syntaxis is for Windows, in Linux and MAC OS it may differ): +``` +> python RadarControl.py +``` + +In order to get a syntax template for a command (COMMAND) please execute the following: +``` +> python RadarControl.py COMMAND "help" +``` + +Currently implemented functionality for the command prompt use is listed below: + +`get_radar_status` provides basic information about the current radar activity. + +`start_radar_measurements` starts radar measurement using a MDF file on the host PC. + +`start_radar_measurements_local_mdf` starts radar measurements using a MDF located on the user PC (MDF is not copied to the host). + +`terminate_radar_measurements` stops currently running radar measurements. + +`get_mdf_list` provides a list of MDF files available on the host PC in the default folder. + +`get_radar_id` provides information about the radar. + +`install_local_mdf` copies an MDF from the user PC to the host PC. The MDF is not started. diff --git a/RadarControl.py b/RadarControl.py index 465ee73..5b9a7c4 100644 --- a/RadarControl.py +++ b/RadarControl.py @@ -1,1488 +1,1811 @@ -# This module contains a Python API class that provides a platform independent interface -# to control RPG FMCW cloud radars. The class can be used for a development of adaptive -# observation strategies. The class provides an access to the latest measured sample -# (both integrated and spectral measured quantities). Based on user anaysis of the last -# sample, scanning and measurement settings can be switched in near-real time. The class -# can also be used to access housekeeping data and to monitor the radar status. -# In addition to the functions which can be used in the users scripts, the module also -# provides command line interface to control RPG FMCW radars directly from a command prompt. -# -# LICENSE -# -# Copyright (c) Radiometer Physics GmbH. All rights reserved. -# -# The software is licensed under GNU Lesser General Public License v2.1. -# -# ACKNOWLEDGEMENT -# -# If you find the provided modules useful for you study, we kindly ask -# to acknowledge in publications our efforts to provide *free* and -# *open-source* solutions for the scientific community. These acknowledgements -# will help us to fulfil requirements for a publication in the Journal -# of Open Source Software. -# -# Found a bug? Please contact us via email: -# -# Interested in contributing to the develoment of modules? Have an idea how to further -# improve functionality? Please send us your ideas and suggestions to -# - -import socket -import sys -import mmap -import struct -import numpy as np -import math - -from array import array -from pathlib import Path - -class ByteReader: - - def __init__(self,byte_input,start_idx = 1): - - # index starts from 1 because the 0th byte - # in the host response repeats the requested command - # This repeated byte is checked - # in the __check_first_byte_of_responce fucntion - # of the Client class (see below in this module) - self.__i = start_idx; - self.__input = byte_input; - - def __get_byte_num(self,value_type): - - # float, integer, unsigned integer - if value_type in ('f','i','I'): - return 4 # [bytes] - - # byte - if value_type == 'b': - return 1 # [byte] - - # short integer, unsigned short integer - if value_type in ('h','H'): - return 2 # [bytes] - - if value_type in ('q','Q'): - return 8 # [bytes] - - raise Exception("Unknown variable type") - - def __read_single_value(self,value_type,endian = '<'): - - V = self.__read_vector(value_type,1,endian = endian) - - return V[0] - - def __read_vector(self,value_type,value_num,endian = '<'): - - size = self.__get_byte_num(value_type) - firstidx = self.__i - lastidx = self.__i + value_num * size - - F = struct.unpack(value_type * value_num,self.__input[firstidx:lastidx]) - - self.__i += value_num * size - - return F - - def _read_float(self): - - try: - return self.__read_single_value('f') - except: - raise Exception('ByteReader: Cannot read a float') - - def _read_unsigned_short(self): - - try: - return self.__read_single_value('H') - except: - raise Exception('ByteReader: Cannot read a float') - - def _read_unsigned_int(self): - - try: - return self.__read_single_value('I') - except: - raise Exception('ByteReader: Cannot read a float') - - def _read_int(self): - - try: - return self.__read_single_value('i') - except: - raise Exception('ByteReader: Cannot read an integer') - - def _read_int_big_endian(self): - - try: - return self.__read_single_value('i','>') - except: - raise Exception('ByteReader: Cannot read an integer') - - def _read_int_little_endian(self): - - try: - return self.__read_single_value('i','<') - except: - raise Exception('ByteReader: Cannot read an integer') - - def _read_long_long(self): - - try: - return self.__read_single_value('q') - except: - raise Exception('ByteReader: Cannot read long long') - - def _read_byte(self): - - try: - return self.__read_single_value('b') - except: - raise Exception('ByteReader: Cannot read a byte') - - def _read_string(self): - - try: - for j in range(self.__i,len(self.__input)): - if self.__input[j] == 0: - break - - s = self.__input[self.__i:j].decode('UTF-8') - self.__i = j + 1 - return s - except: - raise Exception('ByteReader: Cannot read a string') - - def _read_null_separated_strings_to_list(self): - - try: - a = self.__input[self.__i:].decode('UTF-8') - b = a.split('\x00') - return b - except: - raise Exception('ByteReader: Cannot read strings') - - def _read_float_vector(self,value_num): - - try: - return self.__read_vector('f',value_num) - except: - raise Exception('ByteReader: Cannot read float vector') - - def _read_int_vector(self,value_num): - - try: - return self.__read_vector('i',value_num) - except: - raise Exception('ByteReader: Cannot read int vector') - - def _read_byte_vector(self,value_num): - - try: - return self.__read_vector('b',value_num) - except: - raise Exception('ByteReader: Cannot read byte vector') - - def _read_short_int_vector(self,value_num): - - try: - return self.__read_vector('h',value_num) - except: - raise Exception('ByteReader: Cannot read short int vector') - -class MDFList(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - - self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) - self.__read_mdf_list() - - def __output(self): - - if self.__suppressout: - return - - print('\nList of MDF files on the host PC: \n') - - for mdf in self.mdf_list: - print(mdf) - - def __read_mdf_list(self): - - self.mdf_num = ByteReader._read_int(self) - self.mdf_list = ByteReader._read_null_separated_strings_to_list(self) - self.__output() - -class Status(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - - self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) - self.__read_radar_status() - - def __check_connection(self): - - self.connection = ByteReader._read_byte(self) - - if self.connection == 0: - if self.__suppressout == False: - print('The HOST is not connected to the radar') - return False - - if self.connection != 1: - if self.__suppressout == False: - print('Invalid responce: ',self.connection) - return False - - if self.__suppressout == False: - print('The HOST is connected to the radar') - return True - - def __output_status(self): - - if self.__suppressout: - return - - if self.connection == 0: - print('The HOST is not connected to the radar') - return - - if self.connection != 1: - print('Invalid responce') - return - - if self.status == 1: - print('The radar is in STANDBY mode') - - if self.status == 2: - print('Measurement running') - - if self.status == 3: - print('Zero calibration running') - - if self.status == 4: - print('Absolute calibration running') - - if self.status == 5: - print('Transmitter calibration running') - - if self.status == 2 or self.status == 3 or self.status == 5: - - print('Number of MDFs in current measurement: {}'.format(self.mdf_num)) - - for i in range(self.mdf_num): - - print('MDF {}: '.format(i+1) + self.mdf_name[i]) - - if self.mdf_num > 1: - - print('MBF: ' + self.mbf_name) - print('Number of repetitions in batch: {}'.format(self.RepN)) - print('Current batch repetition index: {}'.format(self.CurRep)) - print('Current MDF index: {}'.format(self.CurMDF)) - - if self.hierarchy == 0: - print('Single radar') - - if self.hierarchy == 1: - print('Dual radar, radar is in Master mode') - - if self.hierarchy == 2: - print('Dual radar, radar is in Slave mode') - - - def __read_radar_status(self): - - if self.__check_connection() == False: - return - - self.status = ByteReader._read_byte(self) - - if self.status == 2 or self.status == 3 or self.status == 5: - - self.mdf_num = ByteReader._read_int(self) - - self.mdf_name = [] - - for i in range(self.mdf_num): - - self.mdf_name.append(ByteReader._read_string(self)) - - if self.mdf_num > 1: - - self.mbf_name = ByteReader._read_string(self) - self.RepN = ByteReader._read_int(self) - self.CurRep = ByteReader._read_int(self) - self.CurMDF = ByteReader._read_int(self) - - self.hierarchy = ByteReader._read_byte(self) - - self.__output_status() - -class RadarID(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - - self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) - self.__read_radar_id() - - def __output(self): - - if self.__suppressout: - return - - if self.connection == 0: - print('The HOST is not connected to the radar') - return - - if self.connection != 1: - print('Invalid responce: ',self.connection) - return - - print('Software version: {0:.2f}'.format(self.swversion)) - print('Subversion: {}'.format(self.sbversion)) - - if self.model == 0: - print('Radar model: RPG-FMCW-94-SP') - if self.model == 1: - print('Radar model: RPG-FMCW-94-DP') - if self.model == 2: - print('Radar model: RPG-FMCW-35-SP') - if self.model == 3: - print('Radar model: RPG-FMCW-35-DP') - - print('Fabrication year: {}'.format(self.year)) - print('Fabrication number: {}'.format(self.number)) - print('Customer: ' + self.customer) - print('License: {}'.format(self.license)) - - if self.scanner == 0: - print('No positioner found') - if self.scanner == 1: - print('Positioner found') - - if self.polmode == 0: - print('Single pol. radar') - if self.polmode == 1: - print('Dual pol. radar: LDR mode') - if self.polmode == 2: - print('Dual pol. radar: STSR mode') - - print('IF range min. frequency [Hz]: {0:.1f}'.format(self.IFmin)) - print('IF range max. frequency [Hz]: {0:.1f}'.format(self.IFmax)) - print('Wavelength [m]: {0:.4f}'.format(self.wavelen)) - print('Antenna diameter [m]: {0:.3f}'.format(self.ant_d)) - print('Antenna separation [m]: {0:.3f}'.format(self.ant_sep)) - print('Antenna gain [linear]: {0:.3f}'.format(self.ant_g)) - print('Half-power-beam-width [deg]: {0:.3f}'.format(self.hpbw)) - print('Subreflector blockage [%]: {0:.3f}'.format(self.ant_block)) - print('Receiver gain V-pol. [linear]: {0:.3f}'.format(self.vrec_g)) - print('Receiver gain H-pol. [linear]: {0:.3f}'.format(self.hrec_g)) - - if self.fft_win == 0: - print('FFT window: Rectangular') - if self.fft_win == 1: - print('FFT window: Parzen') - if self.fft_win == 2: - print('FFT window: Blackman') - if self.fft_win == 3: - print('FFT window: Welch') - if self.fft_win == 4: - print('FFT window: Slepian2') - if self.fft_win == 5: - print('FFT window: Slepian3') - - if self.recovery == 0: - print('Recovery after power failure: Disabled') - if self.recovery == 1: - print('Recovery after power failure: Enabled') - - if self.calibrat == 0: - print('Absolute calibration: Not available') - if self.calibrat == 1: - print('Absolute calibration: Available') - - if self.pow_diag == 0: - print('Power supply diagnostic: Not installed') - if self.pow_diag == 1: - print('Power supply diagnostic: Installed') - - print('Positioner azimuth offset [deg]: {0:.3f}'.format(self.scan_off)) - - if self.interlan == 0: - print('InterLAN status: Detection disabled') - if self.interlan == 1: - print('InterLAN status: Autodetection') - - print('Radar IP: ' + self.radar_ip) - - def __read_radar_id(self): - - if self.__check_connection() == False: - return - - self.swversion = ByteReader._read_float(self) - self.sbversion = ByteReader._read_int(self) - self.model = ByteReader._read_int(self) - self.year = ByteReader._read_int(self) - self.number = ByteReader._read_int(self) - self.customer = ByteReader._read_string(self) - self.license = ByteReader._read_int(self) - self.scanner = ByteReader._read_byte(self) - self.polmode = ByteReader._read_byte(self) - self.IFmin = ByteReader._read_float(self) - self.IFmax = ByteReader._read_float(self) - self.wavelen = ByteReader._read_float(self) - self.ant_d = ByteReader._read_float(self) - self.ant_sep = ByteReader._read_float(self) - self.ant_g = ByteReader._read_float(self) - self.hpbw = ByteReader._read_float(self) - self.ant_block = ByteReader._read_float(self) - self.vrec_g = ByteReader._read_float(self) - self.hrec_g = ByteReader._read_float(self) - self.fft_win = ByteReader._read_int(self) - self.recovery = ByteReader._read_byte(self) - self.calibrat = ByteReader._read_byte(self) - self.pow_diag = ByteReader._read_byte(self) - self.scan_off = ByteReader._read_float(self) - self.interlan = ByteReader._read_byte(self) - self.radar_ip = ByteReader._read_string(self) - - self.__output() - - def __check_connection(self): - - self.connection = ByteReader._read_byte(self) - - if self.connection == 0: - if self.__suppressout == False: - print('The HOST is not connected to the radar') - return False - - if self.connection != 1: - if self.__suppressout == False: - print('Invalid responce') - return False - - if self.__suppressout == False: - print('The HOST is connected to the radar') - return True - -class HouseKeeping(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - - self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) - - self.__read() - - def __read(self): - - self.gps_flag = ByteReader._read_byte(self) - - if self.gps_flag == 1: - - self.pos_stat = ByteReader._read_byte(self) - self.time_stat = ByteReader._read_byte(self) - - if self.pos_stat == 1: - - self.longitude = ByteReader._read_float(self) - self.latitude = ByteReader._read_float(self) - self.gps_pos_time = ByteReader._read_int(self) - - if self.time_stat == 1: - - self.gps_sync_time = ByteReader._read_int(self) - - self.radar_time = ByteReader._read_int(self) - self.met_found = ByteReader._read_byte(self) - - if self.met_found == 1: - - self.env_temp = ByteReader._read_float(self) - self.pressure = ByteReader._read_float(self) - self.rel_hum = ByteReader._read_float(self) - self.wind_sp = ByteReader._read_float(self) - self.wind_dir = ByteReader._read_float(self) - - self.scanner_found = ByteReader._read_byte(self) - - if self.met_found == 1: - - self.elev = ByteReader._read_float(self) - self.azm = ByteReader._read_float(self) - - self.rec_temp = ByteReader._read_float(self) - self.trans_temp = ByteReader._read_float(self) - self.pc_temp = ByteReader._read_float(self) - self.rain_stat = ByteReader._read_byte(self) - - self.heat_switch = ByteReader._read_int(self) - self.blow_switch = ByteReader._read_int(self) - self.rad_srive_cnt = ByteReader._read_int(self) - - self.free_mem = [] - self.tot_mem = [] - - for i in range(self.rad_srive_cnt): - - self.free_mem.append(ByteReader._read_int(self)) - self.tot_mem.append(ByteReader._read_int(self)) - - self.inc_el = ByteReader._read_float(self) - self.inc_el_ax = ByteReader._read_float(self) - self.meas_mode = ByteReader._read_byte(self) - self.hirarchy = ByteReader._read_byte(self) - - if self.hirarchy == 1: - - self.sl_model_no = ByteReader._read_int(self) - - self.sl_error = ByteReader._read_byte(self) - self.meas_run = ByteReader._read_byte(self) - self.hdd_overflow = ByteReader._read_byte(self) - self.alarm_code = ByteReader._read_byte(self) - -class LastSample(ByteReader): - - def __init__(self,byte_input,SuppressOutput = False): - - self.__suppressout = SuppressOutput - ByteReader.__init__(self,byte_input) - - self.__default_dr = 30 # [m] - self.__defauls_dv = 0.04 # [m/s] - - self.__read_last_sample() - - - def __check_running_measurements(self): - - self.meas_run = ByteReader._read_byte(self) - - if self.meas_run == 0: - if self.__suppressout == False: - print('Measurements are not running') - return False - - if self.meas_run != 1: - if self.__suppressout == False: - print('Invalid responce') - return False - - if self.__suppressout == False: - print('Measurements are running') - return True - - def __read_last_sample(self): - - if self.__check_running_measurements() == False: - return - - self.samp_idx = ByteReader._read_int(self) - self.head_len = ByteReader._read_int(self) - self.cgprog = ByteReader._read_int(self) - self.model = ByteReader._read_int(self) -# self.number = ByteReader._read_int_big_endian(self) - - self.progname = ByteReader._read_string(self) - self.customer = ByteReader._read_string(self) - - self.freq = ByteReader._read_float(self) - self.ant_sep = ByteReader._read_float(self) - self.ant_d = ByteReader._read_float(self) - self.ant_g = ByteReader._read_float(self) - self.hpbw = ByteReader._read_float(self) - self.radar_c = ByteReader._read_float(self) - self.pol_mode = ByteReader._read_byte(self) - self.comp_ena = ByteReader._read_byte(self) - self.antialia = ByteReader._read_byte(self) - self.samp_dur = ByteReader._read_float(self) - self.gps_lat = ByteReader._read_float(self) - self.gps_lon = ByteReader._read_float(self) - self.call_int = ByteReader._read_int(self) - self.raltn = ByteReader._read_int(self) - self.taltn = ByteReader._read_int(self) - self.haltn = ByteReader._read_int(self) - self.sequn = ByteReader._read_int(self) - self.ralts = ByteReader._read_float_vector(self,self.raltn) - self.talts = ByteReader._read_float_vector(self,self.taltn) - self.halts = ByteReader._read_float_vector(self,self.haltn) - self.nfft = ByteReader._read_int_vector(self,self.sequn) - self.rng_off = ByteReader._read_int_vector(self,self.sequn) - self.c_rep = ByteReader._read_int_vector(self,self.sequn) - self.seqint = ByteReader._read_float_vector(self,self.sequn) - self.dr = ByteReader._read_float_vector(self,self.sequn) - self.max_vel = ByteReader._read_float_vector(self,self.sequn) - self.center_f = ByteReader._read_float_vector(self,self.sequn) - self.cal_par = ByteReader._read_int(self) - self.samp_rate = ByteReader._read_int(self) - self.max_range = ByteReader._read_int(self) - -# range_bin_num = math.ceil(self.max_range / self.__default_range_res) -# vel_bin_num = math.ceil(max(self.max_vel) / self.__defaulf_vel_res) - -# self.spectrum = np.empty((vel_bin_num,range_bin_num)) -# self.spectrum[:] = np.nan - - self.sup_pow_lev = ByteReader._read_byte(self) - self.spk_filter = ByteReader._read_byte(self) - self.phase_corr = ByteReader._read_byte(self) - self.rel_pow_cor = ByteReader._read_byte(self) - self.fft_window = ByteReader._read_byte(self) - - self.fft_input_range = ByteReader._read_int(self) - self.noise_filter = ByteReader._read_float(self) - self.chirp_table_prog = ByteReader._read_int(self) - self.lim_meas = ByteReader._read_byte(self) - - if self.lim_meas == 1: - self.end_of_meas = ByteReader._read_int(self) - self.scan_type = ByteReader._read_byte(self) - self.scan_mode = ByteReader._read_byte(self) - self.scan_dur = ByteReader._read_int(self) - self.base_fn = ByteReader._read_string(self) - - self.arch_data = ByteReader._read_byte(self) - self.disk_full = ByteReader._read_byte(self) - self.sup_pow_lev = ByteReader._read_byte(self) - self.meas_trig = ByteReader._read_byte(self) - self.meas_start = ByteReader._read_int(self) - self.meas_class = ByteReader._read_byte(self) - self.store_lv0 = ByteReader._read_byte(self) - self.store_lv1 = ByteReader._read_byte(self) - - self.rep_idx = ByteReader._read_int(self) - self.mdf_idx = ByteReader._read_int(self) - self.rep_num = ByteReader._read_int(self) - self.mdf_num = ByteReader._read_int(self) - self.mbf_name = ByteReader._read_string(self) - - self.mdf_list = [] - - for i in range(self.mdf_num): - self.mdf_list.append(ByteReader._read_string(self)) - - # LV0 Data - self.samp_t = ByteReader._read_unsigned_int(self) - self.samp_ms = ByteReader._read_int(self) - self.qf = ByteReader._read_byte(self) - - self.rr = ByteReader._read_float(self) - self.rel_hum = ByteReader._read_float(self) - self.env_t = ByteReader._read_float(self) - self.baro_p = ByteReader._read_float(self) - self.ws = ByteReader._read_float(self) - self.wd = ByteReader._read_float(self) - self.dd_volt = ByteReader._read_float(self) - self.dd_tb = ByteReader._read_float(self) - self.lwp = ByteReader._read_float(self) - self.pow_if = ByteReader._read_float(self) - self.elv = ByteReader._read_float(self) - self.azm = ByteReader._read_float(self) - self.status = ByteReader._read_float(self) - self.t_pow = ByteReader._read_float(self) - self.t_temp = ByteReader._read_float(self) - self.r_temp = ByteReader._read_float(self) - self.pc_temp = ByteReader._read_float(self) - self.sky_tb = ByteReader._read_float(self) - self.inc_el = ByteReader._read_float(self) - self.inc_elax = ByteReader._read_float(self) - - self.t_prof = ByteReader._read_float_vector(self,self.taltn) - self.ah_prof = ByteReader._read_float_vector(self,self.haltn) - self.rh_prof = ByteReader._read_float_vector(self,self.haltn) - - self.s_lev = ByteReader._read_float_vector(self,self.raltn) - - prof_msk = ByteReader._read_byte_vector(self,self.raltn) - - for msk in prof_msk: - - if msk == 0: - continue - - block_n = ByteReader._read_byte(self) - - min_block_idx = ByteReader._read_short_int_vector(self,block_n) - max_block_idx = ByteReader._read_short_int_vector(self,block_n) - - for i in range(block_n): - - block_width = max_block_idx[i] - min_block_idx[i] + 1 - - Spec = ByteReader._read_float_vector(self,block_width) - - if self.antialia == 1: - alias_msk = ByteReader._read_byte(self) - min_vel = ByteReader._read_float(self) - - # LV1 data - - self.ze = self.__init_nan_vector(self.raltn) - self.mv = self.__init_nan_vector(self.raltn) - self.sw = self.__init_nan_vector(self.raltn) - self.sk = self.__init_nan_vector(self.raltn) - self.kt = self.__init_nan_vector(self.raltn) - self.ref_rat = self.__init_nan_vector(self.raltn) - self.corr = self.__init_nan_vector(self.raltn) - self.phi = self.__init_nan_vector(self.raltn) - self.ze45 = self.__init_nan_vector(self.raltn) - self.sldr = self.__init_nan_vector(self.raltn) - self.scorr = self.__init_nan_vector(self.raltn) - self.kdp = self.__init_nan_vector(self.raltn) - self.diff_at = self.__init_nan_vector(self.raltn) - - for i in range(self.raltn): - - if prof_msk[i] == 0: - continue - - self.ze[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.mv[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.sw[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.sk[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.kt[i] = float(ByteReader._read_long_long(self)) / 10**7 - - if self.pol_mode == 0: - continue - - self.ref_rat[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.corr[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.phi[i] = float(ByteReader._read_long_long(self)) / 10**7 - - if self.pol_mode != 2: - continue - - self.ze45[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.sldr[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.scorr[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.kdp[i] = float(ByteReader._read_long_long(self)) / 10**7 - self.diff_at[i] = float(ByteReader._read_long_long(self)) / 10**7 - - def __init_nan_vector(self,n): - - v = np.empty(n) - v[:] = np.nan - return v - -class Client(Status): - - def __init__(self,HostIP,Port,Password,SuppressOutput = False): - - self.__HostIP = "" - self.__Port = 0 - self.__PWC = 0 - - self.__suppressoutput = SuppressOutput - - try: - socket.inet_aton(HostIP) - except socket.error: - raise Exception('Not valid IP4 address') - - if not isinstance(Port,int): - raise Exception('The Port input variable must be an integer') - - if Port < 0 or Port > 65535: - raise Exception('The HostIP input variable must be in the range from 0 to 65535') - - self.__HostIP = HostIP - self.__Port = Port - self.__PWC = self.__get_password_code(Password) - - def __get_password_code(self,password): - - passcode = 0 - password = password.upper() - - for i in range(len(password)): - passcode = passcode + ord(password[i]) * (i+1)**2 - - return(passcode) - - def __send_receive(self,msgcode,filename = None,length = None,content = None): - - MESSAGE = self.__form_request_message(msgcode,filename = filename,length = length,content = content) - - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - - try: - s.connect((self.__HostIP,self.__Port)) - except socket.error: - raise Exception('Cannot connect to the server') - - s.sendall(MESSAGE) - - data = bytearray() - - while True: - packet = s.recv(1024) - if not packet: - break - data.extend(packet) - - if self.__check_first_byte_of_responce(data,msgcode) == False: - return None - - return data - - def __check_first_byte_of_responce(self,responce,msgcode): - - if responce[0] == 253: - if self.__suppressoutput == False: - print('The requested command is not known') - return False - - if responce[0] == 255: - if self.__suppressoutput == False: - print('The password is wrong') - return False - - if responce[0] is not msgcode: - if self.__suppressoutput == False: - print('Invalid responce') - return False - - return True - - def __form_request_message(self,msgcode,filename = None,length = None,content = None): - - MESSAGE = (msgcode).to_bytes(1,byteorder='little') + \ - (self.__PWC).to_bytes(4,byteorder='little') - - if filename is not None: - MESSAGE += bytearray(filename + chr(0),'utf-8') - - if length is not None: - MESSAGE += (length).to_bytes(4,byteorder='little') + \ - bytearray(content) - - return MESSAGE - - def get_radar_status(self): - - try: - # Form the request message, 173 is the command code from the software manual - d = self.__send_receive(173) - S = Status(d,self.__suppressoutput) - return S - except: - print('Cannot get radar status') - return None - - def start_radar_measurements(self,mdf_name): - - try: - # Form the request message, 171 is the command code from the software manual - d = self.__send_receive(171,filename = mdf_name) - - if self.__suppressoutput == False: - - if d[1] == 0: - print('Host is not connected to radar') - - if d[1] == 1: - print('radar in STANDBY mode: starting measurement') - - if d[1] == 2: - print('radar not in STANDBY mode') - - if d[1] == 3: - print('Specified MDF/MBF path not valid') - - if d[1] == 4: - print('Host is connected to Slave radar. Slave cannot start measurement') - - return d[1] - except: - print('Failed to start radar measurements') - return None - - def terminate_radar_measurements(self): - - try: - # Form the request message, 170 is the command code from the software manual - d = self.__send_receive(170) - - if self.__suppressoutput == False: - - if d[1] == 0: - print('Host is not connected to radar') - - if d[1] == 1: - print('no measurement running, STANDBY mode') - - if d[1] == 2: - print('running measurement will be terminated') - - if d[1] == 3: - print('cannot terminate: zero calibration running') - - if d[1] == 4: - print('no measurement: absolute calibration running') - - if d[1] == 5: - print('cannot terminate: transmitter power calibration running') - - if d[1] == 6: - print('Host is connected to Slave radar! Slave cannot terminate measurement') - - return d[1] - except: - print('Failed to terminate radar measurements') - return None - - def start_radar_measurements_local_mdf(self,filename): - - try: - if not isinstance(filename,str): - print('mdf_name must be a string') - - if not Path(filename).is_file(): - print('MDF file is not found') - - f = open(filename,'r') - read_data = f.read() - m = mmap.mmap(f.fileno(),0,access=mmap.ACCESS_READ) - f.close() - - d = self.__send_receive(172,filename = filename,length = len(read_data),content = m) - - if d[1] == 0: - print('Host is not connected to radar') - - if d[1] == 1: - print('radar in STANDBY mode: starting measurement') - - if d[1] == 2: - print('radar not in STANDBY mode') - - if d[1] == 3: - print('Host is connected to Slave radar. Slave cannot start measurement') - - return d[1] - except: - print('Failed to start radar measurements from local mdf') - return None - - - def get_mdf_list(self): - - try: - d = self.__send_receive(174) - R = MDFList(d,self.__suppressoutput) - return R - except: - print('Failed to get MDF list') - return None - - def get_radar_id(self): - - try: - d = self.__send_receive(175) - R = RadarID(d,self.__suppressoutput) - return R - except: - print('Failed to get radar id') - return None - - - def install_local_mdf(self,filename): - - try: - if not isinstance(filename,str): - print('mdf_name must be a string') - - if not Path(filename).is_file(): - print('MDF file is not found') - - f = open(filename,'r') - read_data = f.read() - m = mmap.mmap(f.fileno(),0,access=mmap.ACCESS_READ) - f.close() - - a = filename.split('\\') - - d = self.__send_receive(176,filename = a[-1] + chr(0),length = len(read_data),content = m) - - if d is None: - return False - - if self.__suppressoutput == False: - print('The file has been successfully transferred') - - return True - except: - print('Failed to install mdf on the radar') - return False - - def get_last_sample(self): - - d = self.__send_receive(177) - S = LastSample(d,self.__suppressoutput) - return S - - -class Scan: - - def __init__(self,elv = 90,azm = 0,elv_target = None,azm_target = None,elv_spd = 1.0,azm_spd = 1.0): - - if elv < 0.0 or elv > 180.0: - - raise Exception('Wrong elevation') - - if azm < 0.0 or azm > 360.0: - - raise Exception('Wrong azimuth') - - if elv_target is not None: - - if elv_target < 0.0 or elv_target > 180.0: - - raise Exception('Wrong target elevation') - - if azm_target is not None: - - if azm_target < 0.0 or azm_target > 360.0: - - raise Exception('Wrong target azimuth') - - if elv_spd < 0.0 or elv_spd > 5.0: - - raise Exception('Wrong elevation speed') - - if azm_spd < 0.0 or azm_spd > 5.0: - - raise Exception('Wrong azimuth speed') - - # constant angle - if elv_target is None and azm_target is None: - - self.ScanType = 0 - self.ConstEl = elv - self.ConstAz = azm - - return - - if elv_target is None or azm_target is None: - - raise Exception('Wrong targets') - - self.ScanType = 1 - self.ScanMode = 0 - - self.ScanStartEl = elv - self.ScanStopEl = elv_target - self.ScanIncEl = 0 - self.ScanSpeedEl = elv_spd - - self.ScanStartAz = azm - self.ScanStopAz = azm_target - self.ScanIncAz = 0 - self.ScanSpeedAz = azm_spd - -class MeasDefFile: - - def __init__(self): - - self.__filecode = 48856 - - def __write_to_file(self,file_id,var_type,val_list): - - A = array(var_type, val_list) - A.tofile(file_id) - - def create(self, \ - filename, \ - chirp_prg, \ - scan, \ - cal_int = 3600, - timing = False, \ - duration = 3600, \ - filelen = 3600, \ - start = False, \ - start_time = 0, \ - trig = 0, \ - LV0 = True, - LV1 = True, \ - NoiseT = 6.0, - windfunc = 4): - - SpecCmp = True - - # general scans must be limited in time - if scan.ScanType == 1 and timing == True: - timing = False - - output_file = open(filename,'wb') - - self.__write_to_file(output_file,'i',[self.__filecode, chirp_prg, cal_int]) - self.__write_to_file(output_file,'b',[LV0, LV1, SpecCmp, 0, 0, 0, 0]) - self.__write_to_file(output_file,'f',[NoiseT]) - self.__write_to_file(output_file,'b',[scan.ScanType]) - - if scan.ScanType == 0: - self.__write_to_file(output_file,'f',[scan.ConstEl,scan.ConstAz]) - - if scan.ScanType == 1: - self.__write_to_file(output_file,'b',[scan.ScanMode]) - self.__write_to_file(output_file,'i',[1]) - self.__write_to_file(output_file,'f',[scan.ScanStartEl,scan.ScanStopEl,scan.ScanIncEl,scan.ScanSpeedEl,scan.ScanStartAz,scan.ScanStopAz,scan.ScanIncAz,scan.ScanSpeedAz]) - self.__write_to_file(output_file,'b',[0]) - self.__write_to_file(output_file,'i',[1, 0, 0, 1]) - - self.__write_to_file(output_file,'b',[timing]) - - if timing == False: - self.__write_to_file(output_file,'i',[duration, 0]) - - if timing == True: - self.__write_to_file(output_file,'i',[filelen]) - - self.__write_to_file(output_file,'b',[start]) - - if start == 1: - self.__write_to_file(output_file,'i',[start_time, trig]) - - self.__write_to_file(output_file,'i',[windfunc, 1000]) - - output_file.close() - - def output(self): - - if self.filecode != self.__filecode: - print("No loaded MDF file") - return - - print("Chirp generator Program Index: ",self.CGProgNo) - print("Calibration interval [s]: ",self.ZeroCallInt) - print("Store LV0: ",self.Lev0Ena) - print("Store LV1: ",self.Lev1Ena) - print("Spectral compression: ",self.SpecCompEna) - print("Store polarimetric spectral parameters in LV0: ",self.SpecParLV0Ena) - print("File Backup: ",self.FileBackupEna) - print("Antialiasing: ",self.AntiAliasEna) - print("Suppress Power Leveling: ",self.PowLevSupEna) - print("Noise Threshold Factor: ",self.NoiseThresh) - - if self.ScanType == 0: - print('Radar operates at elevation {0:.1f} and azimuth {0:.1f} deg'.format(self.ConstEl,self.ConstAz)) - - if self.ScanType == 1: - print("Radar performs a general scan") - - if self.ScanMode == 0: - print("Scan Mode: Continuous ") - - if self.ScanMode == 1: - print("Scan Mode: Discrete, stop at each sample ") - - for i in range(self.ScanCnt): - - print('Scan index: ',i) - print('Elevation from {0:.1f} to {1:.1f} deg'.format(self.ScanStartEl[i],self.ScanStopEl[i])) - print('Elevation increment angle [deg]: ',self.ScanIncEl[i]) - print('Elevation speed [deg/s]: ',self.ScanSpeedEl[i]) - print('Azimuth from {0:.1f} to {1:.1f} deg'.format(self.ScanStartAz[i],self.ScanStopAz[i])) - print('Azimuth increment angle [deg]: ',self.ScanIncAz[i]) - print('Azimuth speed [deg/s]: ',self.ScanSpeedAz[i]) - - for i in range(self.FrameCnt): - - print('Frame index: ',i) - print('Frame start scan: ',self.FrameStartScn[i]) - print('Frame stop scan: ',self.FrameStopScn[i]) - print('Frame repetition number: ',self.FrameRep[i]) - - if self.Timing == 0: - - print('Limited measurement') - print('Duration [s]: ',self.Duration) - - if self.Timing == 1: - - print('Unlimited measurement') - print('File Lenght [s]',self.FileLen) - - if self.MeasStart == 1: - - print('Start time: ',self.StartTime) - print('Trigger condition: ',self.TrigCond) - - if self.WindFunc == 0: - - print('Window Function: Rectangle') - - if self.WindFunc == 1: - - print('Window Function: Parzen') - - if self.WindFunc == 2: - - print('Window Function: Blackman') - - if self.WindFunc == 3: - - print('Window Function: Welch') - - if self.WindFunc == 4: - - print('Window Function: Slepian2') - - if self.WindFunc == 5: - - print('Window Function: Slepian3') - - print('ADC voltage range [V]: ',self.ADCVoltRng/1000) - - def read(self,filepath): - - with open(filepath, mode='rb') as file: - - data = file.read() - - R = ByteReader(data,0) - - self.filecode = R._read_int() - - if self.filecode != 48856: - raise Exception("Filecode does not correspond to MDF format") - - self.CGProgNo = R._read_int() - self.ZeroCallInt = R._read_int() - self.Lev0Ena = R._read_byte() - self.Lev1Ena = R._read_byte() - self.SpecCompEna = R._read_byte() - self.SpecParLV0Ena = R._read_byte() - self.FileBackupEna = R._read_byte() - self.AntiAliasEna = R._read_byte() - self.PowLevSupEna = R._read_byte() - self.NoiseThresh = R._read_float() - self.ScanType = R._read_byte() - - if self.ScanType == 0: - self.ConstEl = R._read_float() - self.ConstAz = R._read_float() - - if self.ScanType == 1: - self.ScanMode = R._read_byte() - self.ScanCnt = R._read_int() - - self.ScanStartEl = [] - self.ScanStopEl = [] - self.ScanIncEl = [] - self.ScanSpeedEl = [] - self.ScanStartAz = [] - self.ScanStopAz = [] - self.ScanIncAz = [] - self.ScanSpeedAz = [] - self.AzWindAlign = [] - - for i in range(self.ScanCnt): - self.ScanStartEl.append(R._read_float()) - self.ScanStopEl.append(R._read_float()) - self.ScanIncEl.append(R._read_float()) - self.ScanSpeedEl.append(R._read_float()) - self.ScanStartAz.append(R._read_float()) - self.ScanStopAz.append(R._read_float()) - self.ScanIncAz.append(R._read_float()) - self.ScanSpeedAz.append(R._read_float()) - self.AzWindAlign.append(R._read_byte()) - - self.FrameCnt = R._read_int() - - self.FrameStartScn = [] - self.FrameStopScn = [] - self.FrameRep = [] - - for i in range(self.FrameCnt): - - self.FrameStartScn.append(R._read_int()) - self.FrameStopScn.append(R._read_int()) - self.FrameRep.append(R._read_int()) - - self.Timing = R._read_byte() - - if self.Timing == 0: - - self.Duration = R._read_int() - self.BaseNmLen = R._read_int() - - if self.Timing == 1: - - self.FileLen = R._read_int() - - self.MeasStart = R._read_byte() - - if self.MeasStart == 1: - - self.StartTime = R._read_int() - self.TrigCond = R._read_int() - - self.WindFunc = R._read_int() - self.ADCVoltRng = R._read_int() - -def get_radar_status(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.get_radar_status() - -def start_radar_measurements(HOSTIP,PORT,PASSWORD,MDF_FILENAME): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.start_radar_measurements(MDF_FILENAME) - -def start_radar_measurements_local_mdf(HOSTIP,PORT,PASSWORD,MDF_FILENAME): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.start_radar_measurements_local_mdf(MDF_FILENAME) - -def terminate_radar_measurements(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.terminate_radar_measurements() - -def get_mdf_list(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.get_mdf_list() - -def get_radar_id(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.get_radar_id() - -def install_local_mdf(HOSTIP,PORT,PASSWORD,MDF_FILENAME): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD) - X.install_local_mdf(MDF_FILENAME) - -def get_last_sample(HOSTIP,PORT,PASSWORD): - - PORT = int(PORT) - - X = Client(HOSTIP,PORT,PASSWORD,SuppressOutput = True) - X.get_last_sample() - -if __name__ == '__main__': - - status = False - - if len(sys.argv) > 1: - if sys.argv[1] == "get_radar_status": - if len(sys.argv) == 3: - if sys.argv[2] == "help": - print('\nCommand template:\n') - print('python RadarControl.py get_radar_status HOSTIP PORT PASSWORD') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') - status = True - - if sys.argv[1] == "start_radar_measurements": - if len(sys.argv) == 3: - if sys.argv[2] == "help": - print('\nCommand template:\n') - print('python RadarControl.py start_radar_measurements HOSTIP PORT PASSWORD MDF_FILENAME') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') - print('MDF_FILENAME is a string with a MDF/MBF filename on the host PC in the format *.MDF/*.MBF. The string can also contain the full path if the needed file is not in the default MDF / MBF directory.') - print('In order to get the list of available MDF/MBF files in the default MDF / MBF directory, refer to the get_mdf_list command.') - status = True - - if sys.argv[1] == "start_radar_measurements_local_mdf": - if len(sys.argv) == 3: - if sys.argv[2] == "help": - print('\nCommand template:\n') - print('python RadarControl.py start_radar_measurements_local_mdf HOSTIP PORT PASSWORD MDF_FILENAME') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') - print('MDF_FILENAME is a string with a MDF/MBF filename on the local PC in the format path/*.MDF (path/*.MBF). The string must contain the full path. The MDF/MBF file will be transferred to the host PC and launched. Please note, that the file will NOT be stored in the default MDF/MBF folder on the host PC.') - status = True - - if sys.argv[1] == "terminate_radar_measurements": - if len(sys.argv) == 3: - if sys.argv[2] == "help": - print('\nCommand template:\n') - print('python RadarControl.py terminate_radar_measurements HOSTIP PORT PASSWORD') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') - status = True - - if sys.argv[1] == "get_mdf_list": - if len(sys.argv) == 3: - if sys.argv[2] == "help": - print('\nCommand template:\n') - print('python RadarControl.py get_mdf_list HOSTIP PORT PASSWORD') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') - status = True - - if sys.argv[1] == "get_radar_id": - if len(sys.argv) == 3: - if sys.argv[2] == "help": - print('\nCommand template:\n') - print('python RadarControl.py get_radar_id HOSTIP PORT PASSWORD') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') - status = True - - if sys.argv[1] == "install_local_mdf": - if len(sys.argv) == 3: - if sys.argv[2] == "help": - print('\nCommand template:\n') - print('python RadarControl.py install_local_mdf HOSTIP PORT PASSWORD MDF_FILENAME') - print('\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') - print('PORT is the port of the Data Server on the host PC. Must be a positive integer number.') - print('PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') - print('MDF_FILENAME is a string with a MDF/MBF filename on the local PC in the format path/*.MDF (path/*.MBF). The string must contain the full path. The MDF/MBF file will be transferred and stores in the default MDF/MBF folder on the host PC. Please note, that the measurements will NOT be started. Please refer to the start_radar_measurements command.') - status = True - - if len(sys.argv) == 5: - globals()[sys.argv[1]](sys.argv[2],sys.argv[3],sys.argv[4]) - status = True - - if len(sys.argv) == 6: - globals()[sys.argv[1]](sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5]) - status = True - - if status == False: - print('\nError: Wrong input variables.\n') - print('The command line interface of the RadarControl module supports the following commands:\n') - print('get_radar_status') - print('start_radar_measurements') - print('start_radar_measurements_local_mdf') - print('terminate_radar_measurements') - print('get_mdf_list') - print('get_radar_id') - print('install_local_mdf') - - print('\nIn order to get help for a certain command use the following command:') - print('python RadarControl.py CommandName "help"') \ No newline at end of file +# This module contains a Python API class that provides a platform independent interface +# to control RPG FMCW cloud radars. The class can be used for a development of adaptive +# observation strategies. The class provides an access to the latest measured sample +# (both integrated and spectral measured quantities). Based on user anaysis of the last +# sample, scanning and measurement settings can be switched in near-real time. The class +# can also be used to access housekeeping data and to monitor the radar status. +# In addition to the functions which can be used in the users scripts, the module also +# provides command line interface to control RPG FMCW radars directly from a command prompt. +# +# LICENSE +# +# Copyright (c) Radiometer Physics GmbH. All rights reserved. +# +# The software is licensed under GNU Lesser General Public License v2.1. +# +# ACKNOWLEDGEMENT +# +# If you find the provided modules useful for you study, we kindly ask +# to acknowledge in publications our efforts to provide *free* and +# *open-source* solutions for the scientific community. These acknowledgements +# will help us to fulfil requirements for a publication in the Journal +# of Open Source Software. +# +# Found a bug? Please contact us via email: +# +# Interested in contributing to the development of modules? Have an idea how to further +# improve functionality? Please send us your ideas and suggestions to +# + +import socket +import sys +import os +import mmap +import struct +import numpy as np +import math + +from array import array +from pathlib import Path + + +class ByteReader: + + def __init__(self, byte_input, start_idx=1): + + # index starts from 1 because the 0th byte + # in the host response repeats the requested command + # This repeated byte is checked + # in the __check_first_byte_of_response fucntion + # of the Client class (see below in this module) + self.__i = start_idx + self.__input = byte_input + + def __get_byte_num(self, value_type): + + # float, integer, unsigned integer + if value_type in ('f', 'i', 'I'): + return 4 # [bytes] + + # byte + if value_type == 'b': + return 1 # [byte] + + # short integer, unsigned short integer + if value_type in ('h', 'H'): + return 2 # [bytes] + + if value_type in ('q', 'Q'): + return 8 # [bytes] + + raise Exception("Unknown variable type") + + def __read_single_value(self, value_type, endian='<'): + + V = self.__read_vector(value_type, 1, endian=endian) + return V[0] + + def __read_vector(self, value_type, value_num, endian='<'): + + size = self.__get_byte_num(value_type) + firstidx = self.__i + lastidx = self.__i + value_num * size + + F = struct.unpack(value_type * value_num, + self.__input[firstidx:lastidx]) + + self.__i += value_num * size + return F + + def _read_float(self): + + try: + return self.__read_single_value('f') + except: + raise Exception('ByteReader: Cannot read a float') + + def _read_unsigned_short(self): + + try: + return self.__read_single_value('H') + except: + raise Exception('ByteReader: Cannot read a unsigned short int') + + def _read_signed_short(self): + + try: + return self.__read_single_value('h', '<') + except: + raise Exception('ByteReader: Cannot read a signed short int ') + + def _read_unsigned_int(self): + + try: + return self.__read_single_value('I') + except: + raise Exception('ByteReader: Cannot read a unsigned int') + + def _read_int(self): + + try: + return self.__read_single_value('i') + except: + raise Exception('ByteReader: Cannot read an signed integer') + + def _read_int_big_endian(self): + + try: + return self.__read_single_value('i', '>') + except: + raise Exception('ByteReader: Cannot read an integer') + + def _read_int_little_endian(self): + + try: + return self.__read_single_value('i', '<') + except: + raise Exception('ByteReader: Cannot read an integer') + + def _read_long_long(self): + + try: + return self.__read_single_value('q') + except: + raise Exception('ByteReader: Cannot read long long') + + def _read_byte(self): + + try: + return self.__read_single_value('b') + except: + raise Exception('ByteReader: Cannot read a byte') + + def _read_string(self): + + try: + for j in range(self.__i, len(self.__input)): + if self.__input[j] == 0: + break + + s = self.__input[self.__i:j].decode('UTF-8') + self.__i = j + 1 + return s + except: + raise Exception('ByteReader: Cannot read a string') + + def _read_null_separated_strings_to_list(self): + + try: + a = self.__input[self.__i:].decode('UTF-8') + b = a.split('\x00') + return b + except: + raise Exception('ByteReader: Cannot read strings') + + def _read_float_vector(self, value_num): + + try: + return self.__read_vector('f', value_num) + except: + raise Exception('ByteReader: Cannot read float vector') + + def _read_int_vector(self, value_num): + + try: + return self.__read_vector('i', value_num) + except: + raise Exception('ByteReader: Cannot read int vector') + + def _read_byte_vector(self, value_num): + + try: + return self.__read_vector('b', value_num) + except: + raise Exception('ByteReader: Cannot read byte vector') + + def _read_short_int_vector(self, value_num): + + try: + return self.__read_vector('h', value_num) + except: + raise Exception('ByteReader: Cannot read short int vector') + + +class MDFList(ByteReader): + + def __init__(self, byte_input, SuppressOutput=False): + + self.__suppressout = SuppressOutput + ByteReader.__init__(self, byte_input) + self.__read_mdf_list() + + def __output(self): + + if self.__suppressout: + return + + print('\nList of MDF files on the host PC: \n') + + for mdf in self.mdf_list: + print(mdf) + + def __read_mdf_list(self): + + self.mdf_num = ByteReader._read_int(self) + self.mdf_list = ByteReader._read_null_separated_strings_to_list(self) + self.__output() + + +class Status(ByteReader): + + def __init__(self, byte_input, SuppressOutput=False): + + self.__suppressout = SuppressOutput + ByteReader.__init__(self, byte_input) + self.__read_radar_status() + + def __check_connection(self): + + self.connection = ByteReader._read_byte(self) + + if self.connection == 0: + if self.__suppressout == False: + print('The HOST is not connected to the radar') + return False + + if self.connection != 1: + if self.__suppressout == False: + print('Invalid response: ', self.connection) + return False + + if self.__suppressout == False: + print('The HOST is connected to the radar') + return True + + def __output_status(self): + + if self.__suppressout: + return + + if self.connection == 0: + print('The HOST is not connected to the radar') + return + + if self.connection != 1: + print('Invalid response') + return + + if self.status == 1: + print('The radar is in STANDBY mode') + + if self.status == 2: + print('Measurement running') + + if self.status == 3: + print('Zero calibration running') + + if self.status == 4: + print('Absolute calibration running') + + if self.status == 5: + print('Transmitter calibration running') + + if self.status == 2 or self.status == 3 or self.status == 5: + + print('Number of MDFs in current measurement: {}'.format(self.mdf_num)) + + for i in range(self.mdf_num): + + print('MDF {}: '.format(i+1) + self.mdf_name[i]) + + if self.mdf_num > 1: + + print('MBF: ' + self.mbf_name) + print('Number of repetitions in batch: {}'.format(self.RepN)) + print('Current batch repetition index: {}'.format(self.CurRep)) + print('Current MDF index: {}'.format(self.CurMDF)) + + if self.hierarchy == 0: + print('Single radar') + + if self.hierarchy == 1: + print('Dual radar, radar is in Master mode') + + if self.hierarchy == 2: + print('Dual radar, radar is in Slave mode') + + def __read_radar_status(self): + + if self.__check_connection() == False: + return + + self.status = ByteReader._read_byte(self) + + if self.status == 2 or self.status == 3 or self.status == 5: + + self.mdf_num = ByteReader._read_int(self) + + self.mdf_name = [] + + for i in range(self.mdf_num): + + self.mdf_name.append(ByteReader._read_string(self)) + + if self.mdf_num > 1: + + self.mbf_name = ByteReader._read_string(self) + self.RepN = ByteReader._read_int(self) + self.CurRep = ByteReader._read_int(self) + self.CurMDF = ByteReader._read_int(self) + + self.hierarchy = ByteReader._read_byte(self) + + self.__output_status() + + +class RadarID(ByteReader): + + def __init__(self, byte_input, SuppressOutput=False): + + self.__suppressout = SuppressOutput + ByteReader.__init__(self, byte_input) + self.__read_radar_id() + + def __output(self): + + if self.__suppressout: + return + + if self.connection == 0: + print('The HOST is not connected to the radar') + return + + if self.connection != 1: + print('Invalid response: ', self.connection) + return + + print('Software version: {0:.2f}'.format(self.swversion)) + print('Subversion: {}'.format(self.sbversion)) + + if self.model == 0: + print('Radar model: RPG-FMCW-94-SP') + if self.model == 1: + print('Radar model: RPG-FMCW-94-DP') + if self.model == 2: + print('Radar model: RPG-FMCW-35-SP') + if self.model == 3: + print('Radar model: RPG-FMCW-35-DP') + + print('Fabrication year: {}'.format(self.year)) + print('Fabrication number: {}'.format(self.number)) + print('Customer: ' + self.customer) + print('License: {}'.format(self.license)) + + if self.scanner == 0: + print('No positioner found') + if self.scanner == 1: + print('Positioner found') + + if self.polmode == 0: + print('Single pol. radar') + if self.polmode == 1: + print('Dual pol. radar: LDR mode') + if self.polmode == 2: + print('Dual pol. radar: STSR mode') + + print('IF range min. frequency [Hz]: {0:.1f}'.format(self.IFmin)) + print('IF range max. frequency [Hz]: {0:.1f}'.format(self.IFmax)) + print('Wavelength [m]: {0:.4f}'.format(self.wavelen)) + print('Antenna diameter [m]: {0:.3f}'.format(self.ant_d)) + print('Antenna separation [m]: {0:.3f}'.format(self.ant_sep)) + print('Antenna gain [linear]: {0:.3f}'.format(self.ant_g)) + print('Half-power-beam-width [deg]: {0:.3f}'.format(self.hpbw)) + print('Subreflector blockage [%]: {0:.3f}'.format(self.ant_block)) + print('Receiver gain V-pol. [linear]: {0:.3f}'.format(self.vrec_g)) + print('Receiver gain H-pol. [linear]: {0:.3f}'.format(self.hrec_g)) + + if self.fft_win == 0: + print('FFT window: Rectangular') + if self.fft_win == 1: + print('FFT window: Parzen') + if self.fft_win == 2: + print('FFT window: Blackman') + if self.fft_win == 3: + print('FFT window: Welch') + if self.fft_win == 4: + print('FFT window: Slepian2') + if self.fft_win == 5: + print('FFT window: Slepian3') + + if self.recovery == 0: + print('Recovery after power failure: Disabled') + if self.recovery == 1: + print('Recovery after power failure: Enabled') + + if self.calibrat == 0: + print('Absolute calibration: Not available') + if self.calibrat == 1: + print('Absolute calibration: Available') + + if self.pow_diag == 0: + print('Power supply diagnostic: Not installed') + if self.pow_diag == 1: + print('Power supply diagnostic: Installed') + + print('Positioner azimuth offset [deg]: {0:.3f}'.format(self.scan_off)) + + if self.interlan == 0: + print('InterLAN status: Detection disabled') + if self.interlan == 1: + print('InterLAN status: Autodetection') + + print('Radar IP: ' + self.radar_ip) + + def __read_radar_id(self): + + if self.__check_connection() == False: + return + + self.swversion = ByteReader._read_float(self) + self.sbversion = ByteReader._read_int(self) + self.model = ByteReader._read_int(self) + self.year = ByteReader._read_int(self) + self.number = ByteReader._read_int(self) + self.customer = ByteReader._read_string(self) + self.license = ByteReader._read_int(self) + self.scanner = ByteReader._read_byte(self) + self.polmode = ByteReader._read_byte(self) + self.IFmin = ByteReader._read_float(self) + self.IFmax = ByteReader._read_float(self) + self.wavelen = ByteReader._read_float(self) + self.ant_d = ByteReader._read_float(self) + self.ant_sep = ByteReader._read_float(self) + self.ant_g = ByteReader._read_float(self) + self.hpbw = ByteReader._read_float(self) + self.ant_block = ByteReader._read_float(self) + self.vrec_g = ByteReader._read_float(self) + self.hrec_g = ByteReader._read_float(self) + self.fft_win = ByteReader._read_int(self) + self.recovery = ByteReader._read_byte(self) + self.calibrat = ByteReader._read_byte(self) + self.pow_diag = ByteReader._read_byte(self) + self.scan_off = ByteReader._read_float(self) + self.interlan = ByteReader._read_byte(self) + self.radar_ip = ByteReader._read_string(self) + + self.__output() + + def __check_connection(self): + + self.connection = ByteReader._read_byte(self) + + if self.connection == 0: + if self.__suppressout == False: + print('The HOST is not connected to the radar') + return False + + if self.connection != 1: + if self.__suppressout == False: + print('Invalid response') + return False + + if self.__suppressout == False: + print('The HOST is connected to the radar') + return True + + +class HouseKeeping(ByteReader): + + def __init__(self, byte_input, SuppressOutput=False): + + self.__suppressout = SuppressOutput + ByteReader.__init__(self, byte_input) + + self.__read() + + def __read(self): + + self.gps_flag = ByteReader._read_byte(self) + + if self.gps_flag == 1: + + self.pos_stat = ByteReader._read_byte(self) + self.time_stat = ByteReader._read_byte(self) + + if self.pos_stat == 1: + + self.longitude = ByteReader._read_float(self) + self.latitude = ByteReader._read_float(self) + self.gps_pos_time = ByteReader._read_int(self) + + if self.time_stat == 1: + + self.gps_sync_time = ByteReader._read_int(self) + + self.radar_time = ByteReader._read_int(self) + self.met_found = ByteReader._read_byte(self) + + if self.met_found == 1: + + self.env_temp = ByteReader._read_float(self) + self.pressure = ByteReader._read_float(self) + self.rel_hum = ByteReader._read_float(self) + self.wind_sp = ByteReader._read_float(self) + self.wind_dir = ByteReader._read_float(self) + + self.scanner_found = ByteReader._read_byte(self) + + if self.met_found == 1: + + self.elev = ByteReader._read_float(self) + self.azm = ByteReader._read_float(self) + + self.rec_temp = ByteReader._read_float(self) + self.trans_temp = ByteReader._read_float(self) + self.pc_temp = ByteReader._read_float(self) + self.rain_stat = ByteReader._read_byte(self) + + self.heat_switch = ByteReader._read_int(self) + self.blow_switch = ByteReader._read_int(self) + self.rad_srive_cnt = ByteReader._read_int(self) + + self.free_mem = [] + self.tot_mem = [] + + for i in range(self.rad_srive_cnt): + + self.free_mem.append(ByteReader._read_int(self)) + self.tot_mem.append(ByteReader._read_int(self)) + + self.inc_el = ByteReader._read_float(self) + self.inc_el_ax = ByteReader._read_float(self) + self.meas_mode = ByteReader._read_byte(self) + self.hirarchy = ByteReader._read_byte(self) + + if self.hirarchy == 1: + + self.sl_model_no = ByteReader._read_int(self) + + self.sl_error = ByteReader._read_byte(self) + self.meas_run = ByteReader._read_byte(self) + self.hdd_overflow = ByteReader._read_byte(self) + self.alarm_code = ByteReader._read_byte(self) + + +class LastSample(ByteReader): + + def __init__(self, byte_input, SuppressOutput=False): + + self.__suppressout = SuppressOutput + ByteReader.__init__(self, byte_input) + + self.__default_dr = 30 # [m] + self.__defauls_dv = 0.04 # [m/s] + + self.__read_last_sample() + + def __check_running_measurements(self): + + self.meas_run = ByteReader._read_byte(self) + + if self.meas_run == 0: + if self.__suppressout == False: + print('Measurements are not running') + return False + + if self.meas_run != 1: + if self.__suppressout == False: + print('Invalid response') + return False + + if self.__suppressout == False: + print('Measurements are running') + return True + + def __read_last_sample(self): + + if self.__check_running_measurements() == False: + return + + self.samp_idx = ByteReader._read_int(self) + self.head_len = ByteReader._read_int(self) + self.cgprog = ByteReader._read_int(self) + self.model = ByteReader._read_int(self) +# self.number = ByteReader._read_int_big_endian(self) + + self.progname = ByteReader._read_string(self) + self.customer = ByteReader._read_string(self) + + self.freq = ByteReader._read_float(self) + self.ant_sep = ByteReader._read_float(self) + self.ant_d = ByteReader._read_float(self) + self.ant_g = ByteReader._read_float(self) + self.hpbw = ByteReader._read_float(self) + self.radar_c = ByteReader._read_float(self) + self.pol_mode = ByteReader._read_byte(self) + self.comp_ena = ByteReader._read_byte(self) + self.antialia = ByteReader._read_byte(self) + self.samp_dur = ByteReader._read_float(self) + self.gps_lat = ByteReader._read_float(self) + self.gps_lon = ByteReader._read_float(self) + self.call_int = ByteReader._read_int(self) + self.raltn = ByteReader._read_int(self) + self.taltn = ByteReader._read_int(self) + self.haltn = ByteReader._read_int(self) + self.sequn = ByteReader._read_int(self) + self.ralts = ByteReader._read_float_vector(self, self.raltn) + self.talts = ByteReader._read_float_vector(self, self.taltn) + self.halts = ByteReader._read_float_vector(self, self.haltn) + self.nfft = ByteReader._read_int_vector(self, self.sequn) + self.rng_off = ByteReader._read_int_vector(self, self.sequn) + self.c_rep = ByteReader._read_int_vector(self, self.sequn) + self.seqint = ByteReader._read_float_vector(self, self.sequn) + self.dr = ByteReader._read_float_vector(self, self.sequn) + self.max_vel = ByteReader._read_float_vector(self, self.sequn) + self.center_f = ByteReader._read_float_vector(self, self.sequn) + self.cal_par = ByteReader._read_int(self) + self.samp_rate = ByteReader._read_int(self) + self.max_range = ByteReader._read_int(self) + +# range_bin_num = math.ceil(self.max_range / self.__default_range_res) +# vel_bin_num = math.ceil(max(self.max_vel) / self.__defaulf_vel_res) + +# self.spectrum = np.empty((vel_bin_num,range_bin_num)) +# self.spectrum[:] = np.nan + + self.sup_pow_lev = ByteReader._read_byte(self) + self.spk_filter = ByteReader._read_byte(self) + self.phase_corr = ByteReader._read_byte(self) + self.rel_pow_cor = ByteReader._read_byte(self) + self.fft_window = ByteReader._read_byte(self) + + self.fft_input_range = ByteReader._read_int(self) + self.noise_filter = ByteReader._read_float(self) + self.chirp_table_prog = ByteReader._read_int(self) + self.lim_meas = ByteReader._read_byte(self) + + if self.lim_meas == 1: + self.end_of_meas = ByteReader._read_int(self) + self.scan_type = ByteReader._read_byte(self) + self.scan_mode = ByteReader._read_byte(self) + self.scan_dur = ByteReader._read_int(self) + self.base_fn = ByteReader._read_string(self) + + self.arch_data = ByteReader._read_byte(self) + self.disk_full = ByteReader._read_byte(self) + self.sup_pow_lev = ByteReader._read_byte(self) + self.meas_trig = ByteReader._read_byte(self) + self.meas_start = ByteReader._read_int(self) + self.meas_class = ByteReader._read_byte(self) + self.store_lv0 = ByteReader._read_byte(self) + self.store_lv1 = ByteReader._read_byte(self) + + self.rep_idx = ByteReader._read_int(self) + self.mdf_idx = ByteReader._read_int(self) + self.rep_num = ByteReader._read_int(self) + self.mdf_num = ByteReader._read_int(self) + self.mbf_name = ByteReader._read_string(self) + + self.mdf_list = [] + + for i in range(self.mdf_num): + self.mdf_list.append(ByteReader._read_string(self)) + + # LV0 Data + self.samp_t = ByteReader._read_unsigned_int(self) + self.samp_ms = ByteReader._read_int(self) + self.qf = ByteReader._read_byte(self) + + self.rr = ByteReader._read_float(self) + self.rel_hum = ByteReader._read_float(self) + self.env_t = ByteReader._read_float(self) + self.baro_p = ByteReader._read_float(self) + self.ws = ByteReader._read_float(self) + self.wd = ByteReader._read_float(self) + self.dd_volt = ByteReader._read_float(self) + self.dd_tb = ByteReader._read_float(self) + self.lwp = ByteReader._read_float(self) + self.pow_if = ByteReader._read_float(self) + self.elv = ByteReader._read_float(self) + self.azm = ByteReader._read_float(self) + self.status = ByteReader._read_float(self) + self.t_pow = ByteReader._read_float(self) + self.t_temp = ByteReader._read_float(self) + self.r_temp = ByteReader._read_float(self) + self.pc_temp = ByteReader._read_float(self) + self.sky_tb = ByteReader._read_float(self) + self.inc_el = ByteReader._read_float(self) + self.inc_elax = ByteReader._read_float(self) + + self.t_prof = ByteReader._read_float_vector(self, self.taltn) + self.ah_prof = ByteReader._read_float_vector(self, self.haltn) + self.rh_prof = ByteReader._read_float_vector(self, self.haltn) + + self.s_lev = ByteReader._read_float_vector(self, self.raltn) + + prof_msk = ByteReader._read_byte_vector(self, self.raltn) + + for msk in prof_msk: + + if msk == 0: + continue + + block_n = ByteReader._read_byte(self) + + min_block_idx = ByteReader._read_short_int_vector(self, block_n) + max_block_idx = ByteReader._read_short_int_vector(self, block_n) + + for i in range(block_n): + + block_width = max_block_idx[i] - min_block_idx[i] + 1 + + Spec = ByteReader._read_float_vector(self, block_width) + + if self.antialia == 1: + alias_msk = ByteReader._read_byte(self) + min_vel = ByteReader._read_float(self) + + # LV1 data + + self.ze = self.__init_nan_vector(self.raltn) + self.mv = self.__init_nan_vector(self.raltn) + self.sw = self.__init_nan_vector(self.raltn) + self.sk = self.__init_nan_vector(self.raltn) + self.kt = self.__init_nan_vector(self.raltn) + self.ref_rat = self.__init_nan_vector(self.raltn) + self.corr = self.__init_nan_vector(self.raltn) + self.phi = self.__init_nan_vector(self.raltn) + self.ze45 = self.__init_nan_vector(self.raltn) + self.sldr = self.__init_nan_vector(self.raltn) + self.scorr = self.__init_nan_vector(self.raltn) + self.kdp = self.__init_nan_vector(self.raltn) + self.diff_at = self.__init_nan_vector(self.raltn) + + for i in range(self.raltn): + + if prof_msk[i] == 0: + continue + + self.ze[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.mv[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.sw[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.sk[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.kt[i] = float(ByteReader._read_long_long(self)) / 10**7 + + if self.pol_mode == 0: + continue + + self.ref_rat[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.corr[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.phi[i] = float(ByteReader._read_long_long(self)) / 10**7 + + if self.pol_mode != 2: + continue + + self.ze45[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.sldr[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.scorr[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.kdp[i] = float(ByteReader._read_long_long(self)) / 10**7 + self.diff_at[i] = float(ByteReader._read_long_long(self)) / 10**7 + + def __init_nan_vector(self, n): + + v = np.empty(n) + v[:] = np.nan + return v + + +class Client(Status): + + def __init__(self, HostIP, Port, Password, SuppressOutput=False): + + self.__HostIP = "" + self.__Port = 0 + self.__PWC = 0 + + self.__suppressoutput = SuppressOutput + + try: + socket.inet_aton(HostIP) + except socket.error: + raise Exception('Not valid IP4 address') + + if not isinstance(Port, int): + raise Exception('The Port input variable must be an integer') + + if Port < 0 or Port > 65535: + raise Exception( + 'The HostIP input variable must be in the range from 0 to 65535') + + self.__HostIP = HostIP + self.__Port = Port + self.__PWC = self.__get_password_code(Password) + + def __get_password_code(self, password): + + passcode = 0 + password = password.upper() + + for i in range(len(password)): + passcode = passcode + ord(password[i]) * (i+1)**2 + + return (passcode) + + def __send_receive(self, msgcode, filename=None, length=None, content=None): + + MESSAGE = self.__form_request_message( + msgcode, filename=filename, length=length, content=content) + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + + try: + s.connect((self.__HostIP, self.__Port)) + except socket.error: + raise Exception('Cannot connect to the server') + + s.sendall(MESSAGE) + + data = bytearray() + + while True: + packet = s.recv(1024) + if not packet: + break + data.extend(packet) + + if self.__check_first_byte_of_response(data, msgcode) == False: + return None + + return data + + def __check_first_byte_of_response(self, response, msgcode): + + if response[0] == 253: + if self.__suppressoutput == False: + print('The requested command is not known') + return False + + if response[0] == 255: + if self.__suppressoutput == False: + print('The password is wrong') + return False + + if response[0] is not msgcode: + if self.__suppressoutput == False: + print('Invalid response') + return False + + return True + + def __form_request_message(self, msgcode, filename=None, length=None, content=None): + + MESSAGE = (msgcode).to_bytes(1, byteorder='little') + \ + (self.__PWC).to_bytes(4, byteorder='little') + + if filename is not None: + MESSAGE += bytearray(filename + chr(0), 'utf-8') + + if length is not None: + MESSAGE += (length).to_bytes(4, byteorder='little') + \ + bytearray(content) + + return MESSAGE + + def get_radar_status(self): + + try: + # Form the request message, 173 is the command code from the software manual + d = self.__send_receive(173) + S = Status(d, self.__suppressoutput) + return S + except: + print('Cannot get radar status') + return None + + def start_radar_measurements(self, mdf_name): + + try: + # Form the request message, 171 is the command code from the software manual + d = self.__send_receive(171, filename=mdf_name) + + if self.__suppressoutput == False: + + if d[1] == 0: + print('Host is not connected to radar') + + if d[1] == 1: + print('radar in STANDBY mode: starting measurement') + + if d[1] == 2: + print('radar not in STANDBY mode') + + if d[1] == 3: + print('Specified MDF/MBF path not valid') + + if d[1] == 4: + print( + 'Host is connected to Slave radar. Slave cannot start measurement') + + return d[1] + except: + print('Failed to start radar measurements') + return None + + def terminate_radar_measurements(self): + + try: + # Form the request message, 170 is the command code from the software manual + d = self.__send_receive(170) + + if self.__suppressoutput == False: + + if d[1] == 0: + print('Host is not connected to radar') + + if d[1] == 1: + print('no measurement running, STANDBY mode') + + if d[1] == 2: + print('running measurement will be terminated') + + if d[1] == 3: + print('cannot terminate: zero calibration running') + + if d[1] == 4: + print('no measurement: absolute calibration running') + + if d[1] == 5: + print('cannot terminate: transmitter power calibration running') + + if d[1] == 6: + print( + 'Host is connected to Slave radar! Slave cannot terminate measurement') + + return d[1] + except: + print('Failed to terminate radar measurements') + return None + + def start_radar_measurements_local_mdf(self, filename): + + try: + if not isinstance(filename, str): + print('mdf_name must be a string') + + if not Path(filename).is_file(): + print('MDF file is not found') + + f = open(filename, 'r') + read_data = f.read() + m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + f.close() + + d = self.__send_receive( + 172, filename=filename, length=len(read_data), content=m) + + if d[1] == 0: + print('Host is not connected to radar') + + if d[1] == 1: + print('radar in STANDBY mode: starting measurement') + + if d[1] == 2: + print('radar not in STANDBY mode') + + if d[1] == 3: + print('Host is connected to Slave radar. Slave cannot start measurement') + + return d[1] + except: + print('Failed to start radar measurements from local mdf') + return None + + def get_mdf_list(self): + + try: + d = self.__send_receive(174) + R = MDFList(d, self.__suppressoutput) + return R + except: + print('Failed to get MDF list') + return None + + def get_radar_id(self): + + try: + d = self.__send_receive(175) + R = RadarID(d, self.__suppressoutput) + return R + except: + print('Failed to get radar id') + return None + + def install_local_mdf(self, filename): + + try: + if not isinstance(filename, str): + print('mdf_name must be a string') + + if not Path(filename).is_file(): + print('MDF file is not found') + + f = open(filename, 'r') + read_data = f.read() + m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + f.close() + + a = filename.split('\\') + + d = self.__send_receive( + 176, filename=a[-1] + chr(0), length=len(read_data), content=m) + + if d is None: + return False + + if self.__suppressoutput == False: + print('The file has been successfully transferred') + + return True + except: + print('Failed to install mdf on the radar') + return False + + def get_last_sample(self): + + d = self.__send_receive(177) + S = LastSample(d, self.__suppressoutput) + return S + + +class Scan: + + def __init__(self, + elv=90, + azm=0, + elv_target=None, + azm_target=None, + elv_spd=1.0, + azm_spd=1.0, + align_with_wind=0, + ): + + if elv < 0.0 or elv > 180.0: + + raise Exception('Wrong elevation') + + if azm < 0.0 or azm > 360.0: + + raise Exception('Wrong azimuth') + + if elv_target is not None: + + if elv_target < 0.0 or elv_target > 180.0: + + raise Exception('Wrong target elevation') + + if azm_target is not None: + + if azm_target < 0.0 or azm_target > 360.0: + + raise Exception('Wrong target azimuth') + + if elv_spd < 0.0 or elv_spd > 5.0: + + raise Exception('Wrong elevation speed') + + if azm_spd < 0.0 or azm_spd > 5.0: + + raise Exception('Wrong azimuth speed') + + # constant angle + if elv_target is None and azm_target is None: + + self.ScanType = 0 + self.ConstEl = elv + self.ConstAz = azm + + return + + if elv_target is None or azm_target is None: + + raise Exception('Wrong targets') + + self.ScanType = 1 + self.ScanMode = 0 + + self.ScanStartEl = elv + self.ScanStopEl = elv_target + self.ScanIncEl = 0 + self.ScanSpeedEl = elv_spd + + self.ScanStartAz = azm + self.ScanStopAz = azm_target + self.ScanIncAz = 0 + self.ScanSpeedAz = azm_spd + + self.AzWindAlign = align_with_wind + + +class MeasDefFile: + + def __init__(self): + + self.__filecode = 48856 + + def __write_to_file(self, file_id, var_type, val_list): + + A = array(var_type, val_list) + A.tofile(file_id) + + def create(self, + filename, + chirp_prg, + scan_or_list_of_scans, + # the default of None makes a frame that just goes through + # all the defines scans once without repetion + frames=None, + cal_int=3600, + timing=False, + duration=3600, + filelen=3600, + # comment spirrobe: start = False (=0) means immediately + start=False, + start_time=0, + # unclear how to support the ignore date, ignore hour + trig=0, + LV0=True, + LV1=True, + NoiseT=6.0, + windfunc=4, + basename='rpgfmcwscan_'): + + SpecCmp = True + + _windfuncs = {'Rectangle': 0, + 'Parzen': 1, + 'Blackman': 2, + 'Welch': 3, + 'Slepian2': 4, + 'Slepian3': 5, + } + + if isinstance(windfunc, str): + windfunc = _windfuncs.get(windfunc.lower(), False) + if windfunc is False or windfunc not in list(range(6)): + raise ValueError('Windowfunction has to be either integer or', + f'a known name ({_windfuncs.keys()})') + + if isinstance(scan_or_list_of_scans, list): + scans = scan_or_list_of_scans + else: + scans = [scan_or_list_of_scans] + + # Comment spirrobe: + # assert that all scans are of the same type, albeit it is unclear + # if this would not be supported in principle but made impossible + # by the MDF structure + if all(scans[0].ScanType == scan.ScanType for scan in scans): + scantype = scans[0].ScanType + if (scantype == 1 + and all(scans[0].ScanMode == scan.ScanMode for scan in scans)): + scanmode = scans[0].ScanMode + else: + raise ValueError("Mixing of different scanmodes", + " is not possible with the MDF file structure") + else: + raise ValueError("Mixing of different scantypes", + " is not possible with the MDF file structure") + + # the framenumbers, in order + # the framecount (default 1) as the len of the nested list + # the start scans (default 0) + # the stop scans (default 0) + # the repetitions per frame (default 1) + if frames is None: + frames = [[0, len(scans), 1]] + framecount = len(frames) + framestarts = [frame[0] for frame in frames] + # warn that it never includes the scan 0 indicating an issue + if min(framestarts) > 0: + print('Scan numbering starts at index 0, which was not found', + 'in the frames definition, are you sure you defined it right?') + + framestops = [frame[1] for frame in frames] + # warn that it goes over the number of defined scans + if min(framestarts) > 0: + print('Scan numbering exceeds the number of available scans', + ', are you sure you defined it right?') + + # ensure MDF file is always uppercase like in GUI + filename = filename.replace(os.path.basename(filename), + os.path.basename(filename).upper() + ) + try: + output_file = open(filename, 'wb') + except OSError: + print('Could not open file location, check if path exists') + return + + self.__write_to_file(output_file, 'i', [ + self.__filecode, chirp_prg, cal_int]) + self.__write_to_file(output_file, 'b', [LV0, LV1, SpecCmp, 0, 0, 0, 0]) + self.__write_to_file(output_file, 'f', [NoiseT]) + + # the passing in of scans makes the most sense as a list + # but for the MDF format the parameters of the scans are in parameter + # order so we have to adjust this accordingly here. + self.__write_to_file(output_file, + 'b', + [scantype]) + # scan.ScanType for scan in scans]) + if scantype == 0: + self.__write_to_file(output_file, 'f', + [scan.ConstEl for scan in scans]) + + self.__write_to_file(output_file, 'f', + [scan.ConstAz for scan in scans]) + elif scantype == 1: + # header type for scantype 1, scanmode refers to continuous + self.__write_to_file(output_file, 'b', [scanmode]) + # this is the ScanCnt + self.__write_to_file(output_file, 'i', [len(scans)]) + + # elevation related 4 params + self.__write_to_file(output_file, 'f', + [scan.ScanStartEl for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanStopEl for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanIncEl for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanSpeedEl for scan in scans]) + + # azimuth related 4 params + self.__write_to_file(output_file, 'f', + [scan.ScanStartAz for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanStopAz for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanIncAz for scan in scans]) + self.__write_to_file(output_file, 'f', + [scan.ScanSpeedAz for scan in scans]) + + # elevation related 4 params + self.__write_to_file(output_file, 'b', + [scan.AzWindAlign for scan in scans]) + + self.__write_to_file(output_file, 'i', [framecount]) + self.__write_to_file(output_file, 'i', framestarts) + self.__write_to_file(output_file, 'i', framestops) + self.__write_to_file(output_file, 'i', [ + frame[2] for frame in frames]) + + # general scans must be limited in time, this is + if scantype == 1 and timing is True: + timing = False + + self.__write_to_file(output_file, 'b', [timing]) + + # timing can either be False or True + if timing == 0: + self.__write_to_file(output_file, 'i', [duration]) + # add one byte to account for the null terminated byte + self.__write_to_file(output_file, 'i', [len(basename)+1]) + # add the null terminated byte + self.__write_to_file(output_file, 'b', + [ord(i) for i in basename] + [0], + ) + elif timing == 1: + self.__write_to_file(output_file, 'i', [filelen]) + + self.__write_to_file(output_file, 'b', [start]) + + if start == 1: + self.__write_to_file(output_file, 'i', [start_time, trig]) + + self.__write_to_file(output_file, 'i', [windfunc, 1000]) + + output_file.close() + + def output(self): + + if self.filecode != self.__filecode: + print("No loaded MDF file") + return + print('#'*20, '\nGeneral parameters', '\n'+'#'*20) + print("Chirp generator Program Index: ", self.CGProgNo) + print("Calibration interval [s]: ", self.ZeroCallInt) + print("Store LV0: ", self.Lev0Ena) + print("Store LV1: ", self.Lev1Ena) + print("Spectral compression: ", self.SpecCompEna) + print("Store polarimetric spectral parameters in LV0: ", self.SpecParLV0Ena) + print("File Backup: ", self.FileBackupEna) + print("Antialiasing: ", self.AntiAliasEna) + print("Suppress Power Leveling: ", self.PowLevSupEna) + print("Noise Threshold Factor: ", self.NoiseThresh) + + print('\n'+'#'*20, '\nOperational parameters', '\n'+'#'*20) + if self.ScanType == 0: + print('Radar operates at elevation {0:.1f} and azimuth {0:.1f} deg'.format( + self.ConstEl, self.ConstAz)) + + elif self.ScanType == 1: + print("Radar performs a general scan") + + if self.ScanMode == 0: + print("Scan Mode: Continuous ") + + if self.ScanMode == 1: + print("Scan Mode: Discrete, stop at each sample ") + + for i in range(self.ScanCnt): + + print('-'*20, '\nScan index: ', i) + print('Elevation from {0:.1f} to {1:.1f} deg'.format( + self.ScanStartEl[i], self.ScanStopEl[i])) + print('Elevation increment angle [deg]: ', self.ScanIncEl[i]) + print('Elevation speed [deg/s]: ', self.ScanSpeedEl[i]) + print('Azimuth from {0:.1f} to {1:.1f} deg'.format( + self.ScanStartAz[i], self.ScanStopAz[i])) + print('Azimuth increment angle [deg]: ', self.ScanIncAz[i]) + print('Azimuth speed [deg/s]: ', self.ScanSpeedAz[i]) + print('Align to wind [deg/s]: ', self.AzWindAlign[i]) + + print('\n'+'#'*20, '\nRepetion and order of scans', '\n'+'#'*20) + for i in range(self.FrameCnt): + + print('Frame index: ', i) + print('Frame start scan: ', self.FrameStartScn[i]) + print('Frame stop scan: ', self.FrameStopScn[i]) + print('Frame repetition number: ', self.FrameRep[i]) + + if self.Timing == 0: + + print('Limited measurement') + print('Duration [s]: ', self.Duration) + + if self.Timing == 1: + + print('Unlimited measurement') + print('File Length [s]', self.FileLen) + + if self.MeasStart == 1: + + print('Start time: ', self.StartTime) + print('Trigger condition: ', self.TrigCond) + + if self.WindFunc == 0: + + print('Window Function: Rectangle') + + if self.WindFunc == 1: + + print('Window Function: Parzen') + + if self.WindFunc == 2: + + print('Window Function: Blackman') + + if self.WindFunc == 3: + + print('Window Function: Welch') + + if self.WindFunc == 4: + + print('Window Function: Slepian2') + + if self.WindFunc == 5: + + print('Window Function: Slepian3') + + print('ADC voltage range [mV]: ', self.ADCVoltRng/1000) + + def read(self, filepath): + + with open(filepath, mode='rb') as file: + # can close the file here already .. + data = file.read() + + R = ByteReader(data, 0) + + self.filecode = R._read_int() + + if self.filecode != 48856: + raise Exception("Filecode does not correspond to MDF format") + + self.CGProgNo = R._read_int() + self.ZeroCallInt = R._read_int() + self.Lev0Ena = R._read_byte() + self.Lev1Ena = R._read_byte() + self.SpecCompEna = R._read_byte() + self.SpecParLV0Ena = R._read_byte() + self.FileBackupEna = R._read_byte() + self.AntiAliasEna = R._read_byte() + self.PowLevSupEna = R._read_byte() + self.NoiseThresh = R._read_float() + self.ScanType = R._read_byte() + + if self.ScanType == 0: + self.ConstEl = R._read_float() + self.ConstAz = R._read_float() + + if self.ScanType == 1: + self.ScanMode = R._read_byte() + self.ScanCnt = R._read_int() + + self.ScanStartEl = [] + self.ScanStopEl = [] + self.ScanIncEl = [] + self.ScanSpeedEl = [] + self.ScanStartAz = [] + self.ScanStopAz = [] + self.ScanIncAz = [] + self.ScanSpeedAz = [] + self.AzWindAlign = [] + # 8 vars for each scan to be read, 4 elv, 4 az + scandata = [[R._read_float() for j in range(self.ScanCnt)] + for i in range(8)] + + # elevation related params + self.ScanStartEl.extend(scandata[0]) + self.ScanStopEl.extend(scandata[1]) + self.ScanIncEl.extend(scandata[2]) + self.ScanSpeedEl.extend(scandata[3]) + + # azimuth related params + self.ScanStartAz.extend(scandata[4]) + self.ScanStopAz.extend(scandata[5]) + self.ScanIncAz.extend(scandata[6]) + self.ScanSpeedAz.extend(scandata[7]) + + # whether to align the azimuth into the wind, + # for each scan a byte + self.AzWindAlign.extend([R._read_byte() + for j in range(self.ScanCnt)]) + + # comment spirrobe + # maybe this needs to be refactored to in case there are + # several frames. requires testing. + self.FrameCnt = R._read_int() + self.FrameStartScn = [] + self.FrameStopScn = [] + self.FrameRep = [] + + self.FrameStartScn.extend([R._read_int() + for j in range(self.FrameCnt)]) + self.FrameStopScn.extend([R._read_int() + for j in range(self.FrameCnt)]) + self.FrameRep.extend([R._read_int() + for j in range(self.FrameCnt)]) + + self.Timing = R._read_byte() + if self.Timing == 0: + self.Duration = R._read_int() + self.BaseNmLen = R._read_int() + self.BaseNm = ''.join([chr(R._read_byte()) + for j in range(self.BaseNmLen)]) + + elif self.Timing == 1: + self.FileLen = R._read_int() + + self.MeasStart = R._read_byte() + if self.MeasStart == 1: + + self.StartTime = R._read_int() + self.TrigCond = R._read_int() + + self.WindFunc = R._read_int() + # comment spirrobe: + # is this really the ADCVoltRng in an RPG FMCW? + # here in the RPG FMCW it is always 1000 (see also .create method) + self.ADCVoltRng = R._read_int() + + if R._ByteReader__i == R._ByteReader__input.__len__(): + print(f'Finished processing {filepath}') + else: + # this is pure guesswork based on making different MDF files via + # the gui + self.extra = R._read_signed_short() + + # comment spirrobe: + # this was the number I found during testing, as I do not know + # what the 256 stands for in this case there could be other cases + # where something else is defined in the MDF. + if self.extra == 256: + self.BaseNmLen, self.BaseNm = R._read_int(), R._read_string() + + print(f'{R._ByteReader__i} bytes of', + f'{R._ByteReader__input.__len__()} bytes read') + print(f'{R._ByteReader__input[R._ByteReader__i:]} remains') + + +class MeasBatchFile: + + def __init__(self): + + self.__filecode = 58856 + + def __write_to_file(self, file_id, var_type, val_list): + + A = array(var_type, val_list) + A.tofile(file_id) + + def create(self, + filename, + mdflist, + repetitions=1, + ): + + if isinstance(mdflist, list): + mdflist = mdflist + else: + mdflist = [mdflist] + + # ensure MBF filename is always uppercase + filename = filename.replace(os.path.basename(filename), + os.path.basename(filename).upper() + ) + try: + output_file = open(filename, 'wb') + except OSError: + print('Could not open file location, check if path exists') + return + + self.__write_to_file(output_file, 'i', [self.__filecode,]) + self.__write_to_file(output_file, 'i', [len(mdflist)]) + + for mdffile in mdflist: + shortname = mdffile.replace('\\', '/').split('/')[-1] + fullname = mdffile.replace('/', '\\') + # add one byte to account for the null terminated byte + self.__write_to_file(output_file, 'i', [len(fullname)+1]) + # add the null terminated byte + self.__write_to_file(output_file, 'b', + [ord(i) for i in fullname] + [0], + ) + # add one byte to account for the null terminated byte + self.__write_to_file(output_file, 'i', [len(shortname)+1]) + # add the null terminated byte + print(shortname, [len(shortname)+1]) + self.__write_to_file(output_file, 'b', + [ord(i) for i in shortname] + [0], + ) + # self.__write_to_file(output_file, 'b', + # [0], + # ) + self.__write_to_file(output_file, 'i', [repetitions,]) + output_file.close() + + def output(self): + + if self.filecode != self.__filecode: + print("No loaded MBF file") + return + + print('#'*20, '\nList of MDFs', '\n'+'#'*20) + + for i in range(self.nMDFs): + print(f'MDF file {i+1}', self.MDFlist[i]) + + print(f'Batch will be repeated: {self.Repetitions}') + + def read(self, filepath): + + with open(filepath, mode='rb') as file: + # can close the file here already .. + data = file.read() + + R = ByteReader(data, 0) + + self.filecode = R._read_int() + + if self.filecode != 58856: + raise Exception("Filecode does not correspond to MBF format") + + self.nMDFs = R._read_int() + print(self.nMDFs) + if self.nMDFs: + self.MDFlist = [] + + for i in range(self.nMDFs): + FullNameLen = R._read_int() + FullName = ''.join([chr(R._read_byte()) + for j in range(FullNameLen)][:-1]) + + ShortNameLen = R._read_int() + ShortName = ''.join([chr(R._read_byte()) + for j in range(ShortNameLen)][:-1]) + self.MDFlist += [[FullName, ShortName]] + + self.Repetitions = R._read_int() + + if R._ByteReader__i == R._ByteReader__input.__len__(): + print(f'Finished processing {filepath}') + else: + print(f'{R._ByteReader__i} bytes of', + f'{R._ByteReader__input.__len__()} bytes read') + print(f'{R._ByteReader__input[R._ByteReader__i:]} remains') + + +def get_radar_status(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.get_radar_status() + + +def start_radar_measurements(HOSTIP, PORT, PASSWORD, MDF_FILENAME): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.start_radar_measurements(MDF_FILENAME) + + +def start_radar_measurements_local_mdf(HOSTIP, PORT, PASSWORD, MDF_FILENAME): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.start_radar_measurements_local_mdf(MDF_FILENAME) + + +def terminate_radar_measurements(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.terminate_radar_measurements() + + +def get_mdf_list(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.get_mdf_list() + + +def get_radar_id(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.get_radar_id() + + +def install_local_mdf(HOSTIP, PORT, PASSWORD, MDF_FILENAME): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD) + X.install_local_mdf(MDF_FILENAME) + + +def get_last_sample(HOSTIP, PORT, PASSWORD): + + PORT = int(PORT) + + X = Client(HOSTIP, PORT, PASSWORD, SuppressOutput=True) + X.get_last_sample() + + +if __name__ == '__main__': + + status = False + + if len(sys.argv) > 1: + if sys.argv[1] == "get_radar_status": + if len(sys.argv) == 3: + if sys.argv[2] == "help": + print('\nCommand template:\n') + print( + 'python RadarControl.py get_radar_status HOSTIP PORT PASSWORD') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + status = True + + if sys.argv[1] == "start_radar_measurements": + if len(sys.argv) == 3: + if sys.argv[2] == "help": + print('\nCommand template:\n') + print( + 'python RadarControl.py start_radar_measurements HOSTIP PORT PASSWORD MDF_FILENAME') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print('MDF_FILENAME is a string with a MDF/MBF filename on the host PC in the format *.MDF/*.MBF. The string can also contain the full path if the needed file is not in the default MDF / MBF directory.') + print( + 'In order to get the list of available MDF/MBF files in the default MDF / MBF directory, refer to the get_mdf_list command.') + status = True + + if sys.argv[1] == "start_radar_measurements_local_mdf": + if len(sys.argv) == 3: + if sys.argv[2] == "help": + print('\nCommand template:\n') + print( + 'python RadarControl.py start_radar_measurements_local_mdf HOSTIP PORT PASSWORD MDF_FILENAME') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print('MDF_FILENAME is a string with a MDF/MBF filename on the local PC in the format path/*.MDF (path/*.MBF). The string must contain the full path. The MDF/MBF file will be transferred to the host PC and launched. Please note, that the file will NOT be stored in the default MDF/MBF folder on the host PC.') + status = True + + if sys.argv[1] == "terminate_radar_measurements": + if len(sys.argv) == 3: + if sys.argv[2] == "help": + print('\nCommand template:\n') + print( + 'python RadarControl.py terminate_radar_measurements HOSTIP PORT PASSWORD') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + status = True + + if sys.argv[1] == "get_mdf_list": + if len(sys.argv) == 3: + if sys.argv[2] == "help": + print('\nCommand template:\n') + print('python RadarControl.py get_mdf_list HOSTIP PORT PASSWORD') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + status = True + + if sys.argv[1] == "get_radar_id": + if len(sys.argv) == 3: + if sys.argv[2] == "help": + print('\nCommand template:\n') + print('python RadarControl.py get_radar_id HOSTIP PORT PASSWORD') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + status = True + + if sys.argv[1] == "install_local_mdf": + if len(sys.argv) == 3: + if sys.argv[2] == "help": + print('\nCommand template:\n') + print( + 'python RadarControl.py install_local_mdf HOSTIP PORT PASSWORD MDF_FILENAME') + print( + '\nHOSTIP is the IP address of the host PC in format a.b.c.d, where a,b,c,d are numbers from 0 to 255.') + print( + 'PORT is the port of the Data Server on the host PC. Must be a positive integer number.') + print( + 'PASSWORD is a string with the User Password. If the user password is not set, type any symbol.') + print('MDF_FILENAME is a string with a MDF/MBF filename on the local PC in the format path/*.MDF (path/*.MBF). The string must contain the full path. The MDF/MBF file will be transferred and stores in the default MDF/MBF folder on the host PC. Please note, that the measurements will NOT be started. Please refer to the start_radar_measurements command.') + status = True + + if len(sys.argv) == 5: + globals()[sys.argv[1]](sys.argv[2], sys.argv[3], sys.argv[4]) + status = True + + if len(sys.argv) == 6: + globals()[sys.argv[1]](sys.argv[2], + sys.argv[3], sys.argv[4], sys.argv[5]) + status = True + + if status == False: + print('\nError: Wrong input variables.\n') + print('The command line interface of the RadarControl module supports the following commands:\n') + print('get_radar_status') + print('start_radar_measurements') + print('start_radar_measurements_local_mdf') + print('terminate_radar_measurements') + print('get_mdf_list') + print('get_radar_id') + print('install_local_mdf') + + print('\nIn order to get help for a certain command use the following command:') + print('python RadarControl.py CommandName "help"') diff --git a/scan_rpgfmcw.py b/scan_rpgfmcw.py new file mode 100644 index 0000000..aed6e4d --- /dev/null +++ b/scan_rpgfmcw.py @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 6 13:44:33 2023 + +@author: Toshiba +""" +import os +import time +import sys +import glob +import datetime +import numpy as np + +from RPGtools_RemoteSensingInstruments.RadarControl import (Scan, + Client, + MeasDefFile, + MeasBatchFile, + install_local_mdf, + get_radar_status, + start_radar_measurements_local_mdf, + start_radar_measurements, + install_local_mdf) + +WORKDIR = os.path.abspath('.') +WORKDIR = WORKDIR if WORKDIR.endswith(os.sep) else WORKDIR + os.sep + +################################## +### RADAR SPECIFIC SETTINGS ###### +################################## +IP = '192.168.0.2' +PORT = 7000 +PW = '' +CONFIG = [IP, PORT, PW] + +################################## +### SCAN SPECIFIC SETTINGS ### +################################## +SCANSPEED = 1 +FASTSPEED = 5 + +################################## +### CHIRP SPECIFIC SETTINGS ###### +################################## +CHIRPPRG = 7 # RMBLCHIRP, the inface number starts at 1, the real list at 0 + + +################################## +### LOCATION SPECIFIC SETTINGS ### +################################## +NORTHOFFSET = 22.3 + + +################################## +### CLOUDLAB SPECIFIC SETTINGS ### +################################## +# default duration is in s +DURATION = 20 * 60 +# EXTRAWAIT = 10 # an extra number of seconds to wait for the scan to be done +# at the end of a scan, wait this amount in seconds before sending a new scan cmd +AFTERSCANWAIT = 5 + +################################## +### RPG SPECIFIC SETTINGS ### +################################## +t0 = datetime.datetime(2001, 1, 1) + + +def report(client, duration, sleeptime=1): + for i in np.arange((duration/sleeptime) + 1): + _sample = client.get_last_sample() + dts = (datetime.timedelta(seconds=_sample.samp_t), + datetime.timedelta(seconds=_sample.samp_ms/10**6)) + st = t0 + dts[0] + dts[1] + print('Time of sample:', st) + print('Current elevation:', + _sample.elv, _sample.inc_el, _sample.inc_elax) + print('Current azimuth:', + _sample.azm, _sample.inc_elax, ) + time.sleep(sleeptime) + + +def ensure_termination(client, + timeout=20, + retrytime=0.5, + quiet=True): + res = -1 + cnt = 0 + while res != 1: + if not quiet: + print(f'Trying to terminate measurements (try {cnt+1})') + res = client.terminate_radar_measurements() + + if res in [3, 4]: + print('Zero calibration cannot be determinated, wait longer...') + try: + time.sleep(1) + except KeyboardInterrupt: + print('Waiting cancelled') + return + elif res in [5]: + print('Transmitter calibration cannot be determinated, wait longer...') + time.sleep(10) + + time.sleep(retrytime) + cnt += 1 + _status = client.get_radar_status() + # print('Current status after termination command:', _status.__dict__) + if cnt * retrytime >= timeout: + print(f'Measurement could not be terminated in { + cnt*retrytime} seconds, use GUI') + return + + # to ensure there is enough time for the radar to react + time.sleep(2) + + +def ensure_start(client, + file, + timeout=30, + retrytime=2, + quiet=True): + + res = -1 + cnt = 0 + client.terminate_radar_measurements() + while res != 1: + if not quiet: + print(f'Trying to start measurements (try {cnt+1})') + + # if the file exists we assume it is a local file that we do once + # else we assume it is on the radar in the default MDF/MBF directory + if os.path.exists(file): + # assume its local and we need to send it + res = client.start_radar_measurements_local_mdf(file) + else: + # assume its on the radar + res = client.start_radar_measurements(file) + + # if res == 2: + # ensure_termination(client, quiet=quiet) + + time.sleep(retrytime) + cnt += 1 + _status = client.get_radar_status() + if 'mdf_name' in _status.__dict__: + curmdf = _status.mdf_name + + if isinstance(curmdf, list): + curmdf = curmdf[0] + + if file.lower().endswith(curmdf.lower()): + print('Radar reports matching MDF') + return + + # print('Current status after termination command:', _status.__dict__) + if cnt * retrytime >= timeout: + print(f'Measurement could not be started in { + cnt} seconds, use GUI') + return + + # to ensure there is enough time for the radar to react + time.sleep(2) + + +def scan_rhi(elevation_init, + elevation_end, + azimuth, + **kwargs): + scan_generic(elevation_init, elevation_end, azimuth, azimuth, + once=True, + **kwargs) + + +def scan_ppi(elevation, + **kwargs): + azimuth_init, azimuth_end = 0 + NORTHOFFSET, 359.99 + NORTHOFFSET + scan_generic(elevation, elevation, azimuth_init, azimuth_end, + once=True, + **kwargs) + + +def scan_elevation(elevation_init, + elevation_end, + azimuth, + **kwargs): + + scan_generic(elevation_init, elevation_end, azimuth, azimuth, **kwargs) + + +def scan_azimuth(azimuth_init, + azimuth_end, + elevation, + **kwargs): + scan_generic(elevation, elevation, azimuth_init, azimuth_end, **kwargs) + + +def scan_generic(elevation_init, + elevation_end, + azimuth_init, + azimuth_end, + # at which speed to scan + scanspeed=SCANSPEED, + # at which speed to move + fastspeed=FASTSPEED, + # this is the LOWER scantime, as it will be updated + # to match an even number of scans with the given speed/angle + duration=DURATION, + # the calibration interval (abs. cal.). be aware that once + # this is running the scan cannot be aborted + calibration_interval=1, + once=False, + quiet=True, + dryrun=True): + try: + c = Client(*CONFIG, SuppressOutput=quiet) + except: + print('Error connecting to data server, returning ...') + print('Is the CLIENT running (on this computer).', + 'We need the data server to forward commands ...') + return + +# + if (azimuth_init == azimuth_end or elevation_init == elevation_end): + if azimuth_init == azimuth_end: + # rhi like scan, movement in azi is fast + movementtime = abs(90 - elevation_init) / scanspeed + movementtime += abs(azimuth_init-NORTHOFFSET) / fastspeed + elif elevation_init == elevation_end: + # sector scan/ppi like scan, movement in elv is fast + movementtime = abs(90 - elevation_init) / fastspeed + movementtime += abs(azimuth_init - NORTHOFFSET) / scanspeed + + movementtime = int(np.ceil(movementtime)) + print(movementtime, '**') + else: + print('Scanning in both azimuth and elevation is not supported by', + 'this script. exiting....') + return + + if azimuth_init == azimuth_end: + onescanduration = (abs(elevation_end - elevation_init)/scanspeed) + onescanduration = int(onescanduration) + elif elevation_init == elevation_end: + onescanduration = (abs(azimuth_end - azimuth_init)/scanspeed) + onescanduration = int(onescanduration) + else: + print('Scanning in both azimuth and elevation is not supported by', + 'this script. exiting....') + return + + if once: + duration = onescanduration + movementtime + nscans = 1 + else: + nscans = duration / onescanduration + nscans = int(np.ceil(nscans)) + # these have to be symmetrical, so always needs to be an even number + if nscans % 2 != 0: + print('Adding another scancycle to return radar to zenith') + nscans += 1 + + duration = int(nscans * onescanduration) + movementtime + + print(f'The scanrange of {elevation_init}° to {elevation_end}° with', + f'a speed of {scanspeed} results in', + f' {nscans} scans for {duration} seconds', + '(This was rounded up to match the next higher duration for n scans)') + + if azimuth_init == azimuth_end: + mdffilename = 'ELEVATION_SCAN.MDF' if not once else 'RHI_SCAN.MDF' + # elevation scan + # the first scan going down + SCAN_FORTH = Scan(elv=elevation_init, + azm=((azimuth_init-NORTHOFFSET)+360) % 360, + elv_target=elevation_end, + azm_target=((azimuth_init-NORTHOFFSET)+360) % 360, + elv_spd=scanspeed, + azm_spd=fastspeed, + ) + # the second scan (e.g. going back up) + SCAN_BACK = Scan(elv=elevation_end, + azm=((azimuth_init-NORTHOFFSET)+360) % 360, + elv_target=elevation_init, + azm_target=((azimuth_init-NORTHOFFSET)+360) % 360, + elv_spd=scanspeed, + azm_spd=fastspeed, + ) + elif elevation_init == elevation_end: + # maybe a bit of a misnomer but follows the CLOUDLAB nomenclature + # from the miras + mdffilename = 'SECTOR_SCAN.MDF' if not once else 'PPI_SCAN.MDF' + # azimuth/sector scan + # the first scan going down + SCAN_FORTH = Scan(elv=elevation_init, + azm=((azimuth_init-NORTHOFFSET)+360) % 360, + elv_target=elevation_init, + azm_target=((azimuth_end-NORTHOFFSET)+360) % 360, + elv_spd=fastspeed, + azm_spd=scanspeed, + ) + # the second scan (e.g. going back up) + SCAN_BACK = Scan(elv=elevation_init, + azm=((azimuth_end-NORTHOFFSET)+360) % 360, + elv_target=elevation_init, + azm_target=((azimuth_init-NORTHOFFSET)+360) % 360, + elv_spd=fastspeed, + azm_spd=scanspeed, + ) + + SCANS = [SCAN_FORTH, SCAN_BACK] + # once means a RHI or PPI scan + if once: + SCANS = SCANS[:1] + + m = MeasDefFile() + + if once: + frames = [[0, 0, 1]] + else: + frames = [[0, 1, int(np.ceil(nscans/2))]] + + if dryrun: + m.create(WORKDIR + mdffilename, + CHIRPPRG, + SCANS, + frames=frames, + duration=duration, + filelen=onescanduration, + cal_int=calibration_interval, + ) + m.read(WORKDIR + mdffilename) + print(f'Made {WORKDIR+mdffilename}:') + m.output() + # os.remove(WORKDIR + f) + else: + radar_id = c.get_radar_id() + radar_status = c.get_radar_status() + if 'mdf_name' in radar_status.__dict__: + oldmdf = radar_status.mdf_name + else: + oldmdf = None + + if isinstance(oldmdf, list): + oldmdf = oldmdf[0] + + print(f'Radar is currently running {oldmdf}\n') + try: + print(f'A Running {WORKDIR+mdffilename}:') + # m.create(WORKDIR + f, CHIRPPRG, SCAN_FORTH, duration=duration) + m.create(WORKDIR + mdffilename, + CHIRPPRG, + SCANS, + frames=frames, + duration=duration, + filelen=onescanduration, + cal_int=calibration_interval, + ) + m.read(WORKDIR + mdffilename) + ensure_start(c, WORKDIR+mdffilename) + time.sleep(max([5, movementtime])) + time.sleep(10) + report(c, duration + 1, sleeptime=10) + time.sleep(AFTERSCANWAIT) + except KeyboardInterrupt: + print('Stopping scanning operation...') + finally: + c.terminate_radar_measurements() + + print('Scan finished (see above if successful).') + + if oldmdf is not None: + print(f'Installing previous MDF: {oldmdf}') + # time.sleep(15) + # ensure_termination(c) + time.sleep(10) + # c.terminate_radar_measurements() + ensure_start(c, oldmdf) + + # ensure some wait before killing the client when not in dryrun + time.sleep(5) + return c + # del c + + +if __name__ == '__main__': + pass + # a half rhi at positioner 0° to avoid unneccessary movement for testing + client = scan_elevation(90, 60, + NORTHOFFSET, + duration=20, + # dryrun=False, + dryrun=True, + quiet=True) + + client = scan_ppi(85, + dryrun=False, + # dryrun=True, + quiet=False) + + client = scan_rhi(10, 170, NORTHOFFSET, + # dryrun=False, + dryrun=True, + quiet=True) + + # make some MBF file from all MDFs you have. + # mbf = MeasBatchFile() + # mdflist = [WORKDIR + i for i in os.listdir(WORKDIR) if 'MDF' in i] + # mbf.create(WORKDIR+'test.mbf', mdflist, repetitions=3) From 44bc2f37f953ea048789bbb1439fee260debd94e Mon Sep 17 00:00:00 2001 From: spirrobe Date: Tue, 2 Jan 2024 15:54:09 +0100 Subject: [PATCH 05/11] Update README.md extended readme.md --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index e18ac08..aa7f6cf 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,47 @@ Currently implemented functionality for the command prompt use is listed below: `get_radar_id` provides information about the radar. `install_local_mdf` copies an MDF from the user PC to the host PC. The MDF is not started. + +### scan_rpgfmcw.py + +This module contains utilities/examples to conduct scans. The following scanpatterns are implements/available: +- PPI scan with a mandatory elevation input +- RHI scan with mandatory azimuth, and elevation (init and end) +- repeated RHI scans similar to the above RHI scan with a duration keyword. Generally, a forth and a back RHI are conducted, i.e. the number of scans is even +- repeated partial PPI (sector/azimuth) scans with a given elevation and azimuth (init and end) for a selectable duration + +The former two are standard scans and should not require additional information. The latter two are meant to be repeatable (and reduced for the sector scans) RHI/PPI scans to better track changes over time. Generally, the `scan_generic` function is the central function and all other functions are shallow wrappers around it as the logic is similar for all scans it is located in `scan_generic` rather than the single scans. Each scan returns the original Client from RadarControl.py to allow changes afterwards. + +**At the start of the file several configuration parameters are available that should be checked and adjusted, especially the scan speeds and the north angle** + +The working principle is to create a MDF files with a set number of frame repetitions (number of scans that fit in duration / 2) which is then sent to the data server (which communicates with the radar). After a scan has been conducted, the previous MDF file is reinstalled (no support for MBF reinstallation yet). Dryrun creates MDF files without starting/sending them, e.g. for error checking. Dryrun is set to True per default to allow checking of the approach before. + +#### Example +``` +# set your own for the specific location, this should be set +NORTHOFFSET = 0 +# Does a rhi scan of 30° (for illustration) for 20 seconds +# (this gets rounded up to 30 seconds to fit the anglerange (30° movement) with +# a default scanspeed of 1°/s) +client = scan_elevation(90, 60, + NORTHOFFSET, + duration=20, + dryrun=True, + quiet=True) + +# PPI scan, simply choose the elevation at which to conduct +client = scan_ppi(85, + dryrun=True, + quiet=False) + +# RHI scan from 10 to 170° elevation at the NORTH offset +client = scan_rhi(10, 170, NORTHOFFSET, + dryrun=True, + quiet=True) +``` + +#### Issues +Currently, the termination/ensuring start of the scan sometimes faces issues, when +- the original MDF has been restarted and has a long zero calibration value a repetition of the scan won't work +- the reporting of the values during the scanning sometimes indicates weird time stamps, mainly when for some reason the same MDF gets restarted. + From 9aca71dcf7cb5177fb0183ea17c3e4b23b509cf5 Mon Sep 17 00:00:00 2001 From: Robert Spirig Date: Tue, 2 Jan 2024 15:54:25 +0100 Subject: [PATCH 06/11] added reporting option --- scan_rpgfmcw.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scan_rpgfmcw.py b/scan_rpgfmcw.py index aed6e4d..401f9b1 100644 --- a/scan_rpgfmcw.py +++ b/scan_rpgfmcw.py @@ -208,6 +208,8 @@ def scan_generic(elevation_init, # the calibration interval (abs. cal.). be aware that once # this is running the scan cannot be aborted calibration_interval=1, + reporting=True + reportinterval=5, once=False, quiet=True, dryrun=True): @@ -359,7 +361,11 @@ def scan_generic(elevation_init, ensure_start(c, WORKDIR+mdffilename) time.sleep(max([5, movementtime])) time.sleep(10) - report(c, duration + 1, sleeptime=10) + if reporting: + report(c, duration + 1, sleeptime=reportinterval) + else: + time.sleep(duration + 1) + time.sleep(AFTERSCANWAIT) except KeyboardInterrupt: print('Stopping scanning operation...') From b118219833e2b72cec7a8ff2919b839674a58266 Mon Sep 17 00:00:00 2001 From: Robert Spirig Date: Tue, 2 Jan 2024 15:55:28 +0100 Subject: [PATCH 07/11] missing comma fix --- scan_rpgfmcw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scan_rpgfmcw.py b/scan_rpgfmcw.py index 401f9b1..40e07df 100644 --- a/scan_rpgfmcw.py +++ b/scan_rpgfmcw.py @@ -208,7 +208,7 @@ def scan_generic(elevation_init, # the calibration interval (abs. cal.). be aware that once # this is running the scan cannot be aborted calibration_interval=1, - reporting=True + reporting=True, reportinterval=5, once=False, quiet=True, From 080a321a2fbe387c01e37c81bdcbc4c0f838bbbc Mon Sep 17 00:00:00 2001 From: Robert Spirig Date: Wed, 3 Jan 2024 14:01:52 +0100 Subject: [PATCH 08/11] refactored slightly to seperate mdf creation of scans and actual scanning as well as reporting --- scan_rpgfmcw.py | 208 +++++++++++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 92 deletions(-) diff --git a/scan_rpgfmcw.py b/scan_rpgfmcw.py index 40e07df..b84cc52 100644 --- a/scan_rpgfmcw.py +++ b/scan_rpgfmcw.py @@ -65,18 +65,30 @@ t0 = datetime.datetime(2001, 1, 1) -def report(client, duration, sleeptime=1): - for i in np.arange((duration/sleeptime) + 1): - _sample = client.get_last_sample() - dts = (datetime.timedelta(seconds=_sample.samp_t), - datetime.timedelta(seconds=_sample.samp_ms/10**6)) - st = t0 + dts[0] + dts[1] - print('Time of sample:', st) - print('Current elevation:', - _sample.elv, _sample.inc_el, _sample.inc_elax) - print('Current azimuth:', - _sample.azm, _sample.inc_elax, ) - time.sleep(sleeptime) +def report(client, duration=None, sleeptime=1): + start = datetime.datetime.now(datetime.UTC) + if isinstance(duration, float) or isinstance(duration, int): + duration = datetime.timedelta(seconds=duration) + + try: + while True: + _sample = client.get_last_sample() + dts = (datetime.timedelta(seconds=_sample.samp_t), + datetime.timedelta(seconds=_sample.samp_ms/10**6)) + st = t0 + dts[0] + dts[1] + print('Time of sample:', st) + print('Current elevation:', + _sample.elv, _sample.inc_el, _sample.inc_elax) + print('Current azimuth:', + _sample.azm, _sample.inc_elax, ) + + now = datetime.datetime.utcnow() + if duration is not None and (now - start) > duration: + break + time.sleep(sleeptime) + + except KeyboardInterrupt: + return def ensure_termination(client, @@ -106,8 +118,8 @@ def ensure_termination(client, _status = client.get_radar_status() # print('Current status after termination command:', _status.__dict__) if cnt * retrytime >= timeout: - print(f'Measurement could not be terminated in { - cnt*retrytime} seconds, use GUI') + print('Measurement could not be terminated in ', + f'{cnt*retrytime} seconds, use GUI') return # to ensure there is enough time for the radar to react @@ -154,8 +166,8 @@ def ensure_start(client, # print('Current status after termination command:', _status.__dict__) if cnt * retrytime >= timeout: - print(f'Measurement could not be started in { - cnt} seconds, use GUI') + print(f'Measurement could not be started in ', + f'{cnt} seconds, use GUI') return # to ensure there is enough time for the radar to react @@ -166,17 +178,19 @@ def scan_rhi(elevation_init, elevation_end, azimuth, **kwargs): - scan_generic(elevation_init, elevation_end, azimuth, azimuth, - once=True, - **kwargs) + mdffile = make_scan_mdf(elevation_init, elevation_end, azimuth, azimuth, + once=True, + **kwargs) + return scan(mdffile, **kwargs) def scan_ppi(elevation, **kwargs): azimuth_init, azimuth_end = 0 + NORTHOFFSET, 359.99 + NORTHOFFSET - scan_generic(elevation, elevation, azimuth_init, azimuth_end, - once=True, - **kwargs) + mdffile = make_scan_mdf(elevation, elevation, azimuth_init, azimuth_end, + once=True, + **kwargs) + return scan(mdffile, **kwargs) def scan_elevation(elevation_init, @@ -184,44 +198,38 @@ def scan_elevation(elevation_init, azimuth, **kwargs): - scan_generic(elevation_init, elevation_end, azimuth, azimuth, **kwargs) + mdffile = make_scan_mdf(elevation_init, elevation_end, + azimuth, azimuth, **kwargs) + return scan(mdffile, **kwargs) def scan_azimuth(azimuth_init, azimuth_end, elevation, **kwargs): - scan_generic(elevation, elevation, azimuth_init, azimuth_end, **kwargs) + mdffile = make_scan_mdf(elevation, elevation, + azimuth_init, azimuth_end, + **kwargs) + return scan(mdffile, **kwargs) + + +def make_scan_mdf(elevation_init, + elevation_end, + azimuth_init, + azimuth_end, + # at which speed to scan + scanspeed=SCANSPEED, + # at which speed to move + fastspeed=FASTSPEED, + # this is the LOWER scantime, as it will be updated + # to match an even number of scans with the given speed/angle + duration=DURATION, + # the calibration interval (abs. cal.). be aware that once + # this is running the scan cannot be aborted + calibration_interval=1, + once=False, + **kwargs): - -def scan_generic(elevation_init, - elevation_end, - azimuth_init, - azimuth_end, - # at which speed to scan - scanspeed=SCANSPEED, - # at which speed to move - fastspeed=FASTSPEED, - # this is the LOWER scantime, as it will be updated - # to match an even number of scans with the given speed/angle - duration=DURATION, - # the calibration interval (abs. cal.). be aware that once - # this is running the scan cannot be aborted - calibration_interval=1, - reporting=True, - reportinterval=5, - once=False, - quiet=True, - dryrun=True): - try: - c = Client(*CONFIG, SuppressOutput=quiet) - except: - print('Error connecting to data server, returning ...') - print('Is the CLIENT running (on this computer).', - 'We need the data server to forward commands ...') - return - -# if (azimuth_init == azimuth_end or elevation_init == elevation_end): if azimuth_init == azimuth_end: # rhi like scan, movement in azi is fast @@ -314,29 +322,52 @@ def scan_generic(elevation_init, if once: SCANS = SCANS[:1] - m = MeasDefFile() - if once: frames = [[0, 0, 1]] else: frames = [[0, 1, int(np.ceil(nscans/2))]] + m = MeasDefFile() + + m.create(WORKDIR + mdffilename, + CHIRPPRG, + SCANS, + frames=frames, + duration=duration, + filelen=onescanduration, + cal_int=calibration_interval, + ) + m.read(WORKDIR + mdffilename) + print(f'Made {WORKDIR+mdffilename}:') + m.output() + return WORKDIR + mdffilename + + +def scan(mdffile, + reporting=True, + reportinterval=5, + quiet=True, + dryrun=True, + **kwargs): + + m = MeasDefFile() + m.read(mdffile) + if dryrun: - m.create(WORKDIR + mdffilename, - CHIRPPRG, - SCANS, - frames=frames, - duration=duration, - filelen=onescanduration, - cal_int=calibration_interval, - ) - m.read(WORKDIR + mdffilename) - print(f'Made {WORKDIR+mdffilename}:') m.output() # os.remove(WORKDIR + f) + return mdffile else: - radar_id = c.get_radar_id() - radar_status = c.get_radar_status() + try: + client = Client(*CONFIG, SuppressOutput=quiet) + except: + print('Error connecting to data server, returning ...') + print('Is the CLIENT running (on this computer).', + 'We need the data server to forward commands ...') + return + + radar_id = client.get_radar_id() + radar_status = client.get_radar_status() if 'mdf_name' in radar_status.__dict__: oldmdf = radar_status.mdf_name else: @@ -347,30 +378,22 @@ def scan_generic(elevation_init, print(f'Radar is currently running {oldmdf}\n') try: - print(f'A Running {WORKDIR+mdffilename}:') + print(f'A Running {mdffile}:') # m.create(WORKDIR + f, CHIRPPRG, SCAN_FORTH, duration=duration) - m.create(WORKDIR + mdffilename, - CHIRPPRG, - SCANS, - frames=frames, - duration=duration, - filelen=onescanduration, - cal_int=calibration_interval, - ) - m.read(WORKDIR + mdffilename) - ensure_start(c, WORKDIR+mdffilename) - time.sleep(max([5, movementtime])) - time.sleep(10) + ensure_start(client, mdffile) + if reporting: - report(c, duration + 1, sleeptime=reportinterval) + report(client, + duration=m.Duration + 1, + sleeptime=reportinterval) else: - time.sleep(duration + 1) + time.sleep(m.Duration + 1) time.sleep(AFTERSCANWAIT) except KeyboardInterrupt: print('Stopping scanning operation...') finally: - c.terminate_radar_measurements() + client.terminate_radar_measurements() print('Scan finished (see above if successful).') @@ -380,33 +403,34 @@ def scan_generic(elevation_init, # ensure_termination(c) time.sleep(10) # c.terminate_radar_measurements() - ensure_start(c, oldmdf) + ensure_start(client, oldmdf) # ensure some wait before killing the client when not in dryrun time.sleep(5) - return c + return client # del c if __name__ == '__main__': pass # a half rhi at positioner 0° to avoid unneccessary movement for testing - client = scan_elevation(90, 60, + result = scan_elevation(90, 60, NORTHOFFSET, duration=20, # dryrun=False, dryrun=True, quiet=True) - client = scan_ppi(85, - dryrun=False, + result = scan_ppi(85, + # dryrun=False, # dryrun=True, quiet=False) - client = scan_rhi(10, 170, NORTHOFFSET, - # dryrun=False, - dryrun=True, - quiet=True) + # result = scan_rhi(10, 170, + # NORTHOFFSET, + # # dryrun=False, + # dryrun=True, + # quiet=True) # make some MBF file from all MDFs you have. # mbf = MeasBatchFile() From fa996cde8c544ce0f41f20dee26f73bce9411b2b Mon Sep 17 00:00:00 2001 From: Robert Spirig Date: Wed, 3 Jan 2024 16:43:02 +0100 Subject: [PATCH 09/11] some typos and minor bugfixing in reporting/some refactoring with helper variables --- scan_rpgfmcw.py | 90 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/scan_rpgfmcw.py b/scan_rpgfmcw.py index b84cc52..1f2edb6 100644 --- a/scan_rpgfmcw.py +++ b/scan_rpgfmcw.py @@ -62,30 +62,67 @@ ################################## ### RPG SPECIFIC SETTINGS ### ################################## -t0 = datetime.datetime(2001, 1, 1) +t0 = datetime.datetime(2001, 1, 1, tzinfo=datetime.UTC) +asec = datetime.timedelta(seconds=1) +amin = 60 * asec -def report(client, duration=None, sleeptime=1): +def _ts2dt(seconds, milliseconds=0): + if seconds is None: + return None + dts = (datetime.timedelta(seconds=seconds), + datetime.timedelta(seconds=milliseconds/10**6)) + return t0 + dts[0] + dts[1] + + +# get current measurement time from sample +def _get_cmt(sample): + return _ts2dt(sample.samp_t, milliseconds=sample.samp_ms) + + +# get end of measurement from sample +def _get_eom(sample): + eom = getattr(sample, 'end_of_meas', None) + if eom is None: + return None + return _ts2dt(eom) + + +def report(client, duration=None, reportfrequency=1): start = datetime.datetime.now(datetime.UTC) if isinstance(duration, float) or isinstance(duration, int): duration = datetime.timedelta(seconds=duration) + cnt = 0 try: while True: _sample = client.get_last_sample() - dts = (datetime.timedelta(seconds=_sample.samp_t), - datetime.timedelta(seconds=_sample.samp_ms/10**6)) - st = t0 + dts[0] + dts[1] - print('Time of sample:', st) - print('Current elevation:', - _sample.elv, _sample.inc_el, _sample.inc_elax) - print('Current azimuth:', - _sample.azm, _sample.inc_elax, ) - - now = datetime.datetime.utcnow() + cmt = _get_cmt(_sample) + print('Time of sample:', cmt) + print('Current elevation/elv speed:', + f'{_sample.elv:4.3}°, {_sample.inc_el:3.3}°/s' + ) + print('Current azimuth/az speed:', + f'{_sample.azm:4.3}°, {_sample.inc_elax:3.3}°/s' + ) + print('Current min, mean and max ZE:', + f'{np.nanmin(_sample.ze):3.3} dBZ', + f'{np.nanmean(_sample.ze):3.3} dBZ', + f'{np.nanmax(_sample.ze):3.3} dBZ' + ) + + now = datetime.datetime.now(datetime.UTC) if duration is not None and (now - start) > duration: break - time.sleep(sleeptime) + + eom = _get_eom(_sample) + + if eom is not None and now > (eom + 10 * asec): + print(f'Ending reporting as {eom} has passed {now}') + break + + time.sleep(reportfrequency) + cnt += 1 except KeyboardInterrupt: return @@ -266,7 +303,7 @@ def make_scan_mdf(elevation_init, nscans = int(np.ceil(nscans)) # these have to be symmetrical, so always needs to be an even number if nscans % 2 != 0: - print('Adding another scancycle to return radar to zenith') + print('Adding another scancycle to achieve even number of scans') nscans += 1 duration = int(nscans * onescanduration) + movementtime @@ -277,7 +314,7 @@ def make_scan_mdf(elevation_init, '(This was rounded up to match the next higher duration for n scans)') if azimuth_init == azimuth_end: - mdffilename = 'ELEVATION_SCAN.MDF' if not once else 'RHI_SCAN.MDF' + mdffilename = 'SCAN_ELEVATION.MDF' if not once else 'SCAN_RHI.MDF' # elevation scan # the first scan going down SCAN_FORTH = Scan(elv=elevation_init, @@ -298,7 +335,7 @@ def make_scan_mdf(elevation_init, elif elevation_init == elevation_end: # maybe a bit of a misnomer but follows the CLOUDLAB nomenclature # from the miras - mdffilename = 'SECTOR_SCAN.MDF' if not once else 'PPI_SCAN.MDF' + mdffilename = 'SCAN_SECTOR.MDF' if not once else 'SCAN_PPI.MDF' # azimuth/sector scan # the first scan going down SCAN_FORTH = Scan(elv=elevation_init, @@ -380,18 +417,23 @@ def scan(mdffile, try: print(f'A Running {mdffile}:') # m.create(WORKDIR + f, CHIRPPRG, SCAN_FORTH, duration=duration) - ensure_start(client, mdffile) + # ensure_start(client, mdffile) + client.terminate_radar_measurements() + time.sleep(3) + client.start_radar_measurements_local_mdf(mdffile) if reporting: report(client, duration=m.Duration + 1, - sleeptime=reportinterval) + reportfrequency=reportinterval) else: time.sleep(m.Duration + 1) - time.sleep(AFTERSCANWAIT) + client.start_radar_measurements(oldmdf) + # time.sleep(AFTERSCANWAIT) + except KeyboardInterrupt: - print('Stopping scanning operation...') + print('Stopping scanning operation manually...') finally: client.terminate_radar_measurements() @@ -401,14 +443,12 @@ def scan(mdffile, print(f'Installing previous MDF: {oldmdf}') # time.sleep(15) # ensure_termination(c) - time.sleep(10) + # time.sleep(10) # c.terminate_radar_measurements() - ensure_start(client, oldmdf) + # ensure_start(client, oldmdf) + client.start_radar_measurements(oldmdf) - # ensure some wait before killing the client when not in dryrun - time.sleep(5) return client - # del c if __name__ == '__main__': From 2cbbcf7cdb6b3498be46a34008fbb53d3cd4fda2 Mon Sep 17 00:00:00 2001 From: Robert Spirig Date: Thu, 4 Jan 2024 16:57:34 +0100 Subject: [PATCH 10/11] moved away from single mdf files containing x scans to instead run the same mdfs in order to have seperate datafiles for easier postprocessing --- scan_rpgfmcw.py | 169 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 116 insertions(+), 53 deletions(-) diff --git a/scan_rpgfmcw.py b/scan_rpgfmcw.py index 1f2edb6..2a5d1e0 100644 --- a/scan_rpgfmcw.py +++ b/scan_rpgfmcw.py @@ -9,6 +9,7 @@ import sys import glob import datetime +import warnings import numpy as np from RPGtools_RemoteSensingInstruments.RadarControl import (Scan, @@ -22,6 +23,7 @@ install_local_mdf) WORKDIR = os.path.abspath('.') +WORKDIR = r'C:\RPG-FMCW-H\MDF_MBF' WORKDIR = WORKDIR if WORKDIR.endswith(os.sep) else WORKDIR + os.sep ################################## @@ -88,7 +90,10 @@ def _get_eom(sample): return _ts2dt(eom) -def report(client, duration=None, reportfrequency=1): +def report(client, + duration=None, + reportfrequency=1, + break_on_eom=False): start = datetime.datetime.now(datetime.UTC) if isinstance(duration, float) or isinstance(duration, int): duration = datetime.timedelta(seconds=duration) @@ -105,11 +110,19 @@ def report(client, duration=None, reportfrequency=1): print('Current azimuth/az speed:', f'{_sample.azm:4.3}°, {_sample.inc_elax:3.3}°/s' ) - print('Current min, mean and max ZE:', - f'{np.nanmin(_sample.ze):3.3} dBZ', - f'{np.nanmean(_sample.ze):3.3} dBZ', - f'{np.nanmax(_sample.ze):3.3} dBZ' - ) + # so we do not have to see the empty slice warnings for nans + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + print('Current min, mean and max ZE:', + f'{np.nanmin(_sample.ze):5.3} dBZ', + f'{np.nanmean(_sample.ze):5.3} dBZ', + f'{np.nanmax(_sample.ze):5.3} dBZ' + ) + print('Current min, mean and max SLDR:', + f'{np.nanmin(_sample.sldr):5.3} dB', + f'{np.nanmean(_sample.sldr):5.3} dB', + f'{np.nanmax(_sample.sldr):5.3} dB' + ) now = datetime.datetime.now(datetime.UTC) if duration is not None and (now - start) > duration: @@ -117,7 +130,7 @@ def report(client, duration=None, reportfrequency=1): eom = _get_eom(_sample) - if eom is not None and now > (eom + 10 * asec): + if eom is not None and break_on_eom and now > (eom + 10 * asec): print(f'Ending reporting as {eom} has passed {now}') break @@ -235,19 +248,19 @@ def scan_elevation(elevation_init, azimuth, **kwargs): - mdffile = make_scan_mdf(elevation_init, elevation_end, - azimuth, azimuth, **kwargs) - return scan(mdffile, **kwargs) + mdffiles = make_scan_mdf(elevation_init, elevation_end, + azimuth, azimuth, **kwargs) + return scan(mdffiles, **kwargs) def scan_azimuth(azimuth_init, azimuth_end, elevation, **kwargs): - mdffile = make_scan_mdf(elevation, elevation, - azimuth_init, azimuth_end, - **kwargs) - return scan(mdffile, **kwargs) + mdffiles = make_scan_mdf(elevation, elevation, + azimuth_init, azimuth_end, + **kwargs) + return scan(mdffiles, **kwargs) def make_scan_mdf(elevation_init, @@ -265,6 +278,13 @@ def make_scan_mdf(elevation_init, # this is running the scan cannot be aborted calibration_interval=1, once=False, + # make a mdffile for each unique scanpattern to allow + # for easier postprocessing + seperatemdffiles=True, + # overwrite the basename in the beginning of the file + # TODO: Test if we can pass in \..\ or similar to get to + # another directory on the radar PC :-) + basename=None, **kwargs): if (azimuth_init == azimuth_end or elevation_init == elevation_end): @@ -308,13 +328,26 @@ def make_scan_mdf(elevation_init, duration = int(nscans * onescanduration) + movementtime - print(f'The scanrange of {elevation_init}° to {elevation_end}° with', - f'a speed of {scanspeed} results in', - f' {nscans} scans for {duration} seconds', - '(This was rounded up to match the next higher duration for n scans)') + if once: + print(f'The scanrange of {elevation_init}° to {elevation_end}° with', + f'a speed of {scanspeed} results in a duration of {duration} seconds', + ) + else: + if azimuth_init == azimuth_end: + print(f'The scanrange of {elevation_init}° to {elevation_end}° with', + f'a speed of {scanspeed} results in', + f' {nscans} scans for {duration} seconds', + '(This may have been rounded up to create an even scan number)') + elif elevation_init == elevation_end: + print(f'The scanrange of {azimuth_init}° to {azimuth_end}° with', + f'a speed of {scanspeed} results in', + f' {nscans} scans for {duration} seconds', + '(This may have been rounded up to create an even scan number)') if azimuth_init == azimuth_end: mdffilename = 'SCAN_ELEVATION.MDF' if not once else 'SCAN_RHI.MDF' + if basename is None: + basename = 'ELEVATIONSCAN' if not once else 'RHISCAN' # elevation scan # the first scan going down SCAN_FORTH = Scan(elv=elevation_init, @@ -336,6 +369,8 @@ def make_scan_mdf(elevation_init, # maybe a bit of a misnomer but follows the CLOUDLAB nomenclature # from the miras mdffilename = 'SCAN_SECTOR.MDF' if not once else 'SCAN_PPI.MDF' + if basename is None: + basename = 'SECTORSCAN' if not once else 'PPISCAN' # azimuth/sector scan # the first scan going down SCAN_FORTH = Scan(elv=elevation_init, @@ -359,41 +394,71 @@ def make_scan_mdf(elevation_init, if once: SCANS = SCANS[:1] - if once: + if once or seperatemdffiles: frames = [[0, 0, 1]] else: frames = [[0, 1, int(np.ceil(nscans/2))]] m = MeasDefFile() + if seperatemdffiles: + mdffiles = [] + + for SCANNO, SCAN in enumerate(SCANS): + _mdffilename = mdffilename.replace('.MDF', f'{SCANNO}.MDF') + m.create(WORKDIR + _mdffilename, + CHIRPPRG, + SCAN, + frames=frames, + duration=onescanduration, + filelen=onescanduration, + cal_int=calibration_interval, + basename=basename, + ) + m.read(WORKDIR + _mdffilename) + print(f'Made {WORKDIR+_mdffilename}:') + m.output() + mdffiles.append(WORKDIR + _mdffilename) + # simply repeat the list entry of mdf files the + # number of scans/2 so we can just use the same mdffiles again + # and know how many times they should be made. + mdffiles = mdffiles * int(np.ceil(nscans/2)) + return mdffiles + else: + m.create(WORKDIR + mdffilename, + CHIRPPRG, + SCANS, + frames=frames, + duration=duration, + filelen=onescanduration, + cal_int=calibration_interval, + basename=basename, + ) + m.read(WORKDIR + mdffilename) + print(f'Made {WORKDIR+mdffilename}:') + m.output() + return WORKDIR + mdffilename - m.create(WORKDIR + mdffilename, - CHIRPPRG, - SCANS, - frames=frames, - duration=duration, - filelen=onescanduration, - cal_int=calibration_interval, - ) - m.read(WORKDIR + mdffilename) - print(f'Made {WORKDIR+mdffilename}:') - m.output() - return WORKDIR + mdffilename - - -def scan(mdffile, + +def scan(mdffile_or_list_of_mdffiles, reporting=True, reportinterval=5, quiet=True, dryrun=True, **kwargs): + if isinstance(mdffile_or_list_of_mdffiles, str): + mdffiles = [mdffile_or_list_of_mdffiles] + else: + mdffiles = mdffile_or_list_of_mdffiles + m = MeasDefFile() - m.read(mdffile) if dryrun: - m.output() + for mdffile in mdffiles: + m.read(mdffile) + m.output() # os.remove(WORKDIR + f) - return mdffile + return mdffiles else: try: client = Client(*CONFIG, SuppressOutput=quiet) @@ -415,19 +480,21 @@ def scan(mdffile, print(f'Radar is currently running {oldmdf}\n') try: - print(f'A Running {mdffile}:') # m.create(WORKDIR + f, CHIRPPRG, SCAN_FORTH, duration=duration) - # ensure_start(client, mdffile) - client.terminate_radar_measurements() - time.sleep(3) - client.start_radar_measurements_local_mdf(mdffile) - - if reporting: - report(client, - duration=m.Duration + 1, - reportfrequency=reportinterval) - else: - time.sleep(m.Duration + 1) + # made this way so that each scan of a mdflist has a + # unique data file + for mdfno, mdffile in enumerate(mdffiles): + print(f'Running {mdffile}, {mdfno} of {len(mdffiles)}') + m.read(mdffile) + ensure_start(client, mdffile) + if reporting: + report(client, + duration=m.Duration + 3, + reportfrequency=reportinterval) + else: + time.sleep(m.Duration + 3) + + # client.terminate_radar_measurements() client.start_radar_measurements(oldmdf) # time.sleep(AFTERSCANWAIT) @@ -441,10 +508,6 @@ def scan(mdffile, if oldmdf is not None: print(f'Installing previous MDF: {oldmdf}') - # time.sleep(15) - # ensure_termination(c) - # time.sleep(10) - # c.terminate_radar_measurements() # ensure_start(client, oldmdf) client.start_radar_measurements(oldmdf) From 33186535dba4fc43be6d7d43449fb2b6a5427ac5 Mon Sep 17 00:00:00 2001 From: Robert Spirig Date: Thu, 4 Jan 2024 23:42:25 +0100 Subject: [PATCH 11/11] added gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__