10 from io
import BytesIO
14 from telemetry.derive_orbitparams
import derive_orbitparams
15 from telemetry
import ccsdspy
17 __version__ =
'1.5.4 (2024-05-28)'
31 good_apids = [att, orb, tilt]
36 pktypes[
'SC'] = {
'maxapid': 549,
'maxlen': 2048,
'name':
'Spacecraft'}
37 pktypes[
'OCI'] = {
'maxapid': 749,
'maxlen': 1618,
'name':
'OCI'}
38 pktypes[
'HARP'] = {
'maxapid': 799,
'maxlen': 40,
'name':
'HARP2'}
39 pktypes[
'SPEX'] = {
'maxapid': 849,
'maxlen': 298,
'name':
'SPEXone'}
42 MAX_SECONDS_OF_DAY = 172800.0
46 print(
"hktgen_pace", __version__)
49 parser = argparse.ArgumentParser(
50 formatter_class=argparse.RawTextHelpFormatter,
51 description=
'Convert S-band housekeeping telemetry to NetCDF4',
56 101 : Non-fatal file open error
57 102 : Invalid file or instrument from CFE header
58 103 : Invalid packet header
59 104 : Invalid packet [header/datatype/length]
60 110 : No valid packets found
61 120 : Multiple warnings; see log
64 parser.add_argument(
'ifile', type=str,
65 help=
'path to S-band telemetry (HSK) file OR list of input files, '
66 'one per line, in chronological order')
67 parser.add_argument(
"-o",
"--output", metavar=
"ofile", dest=
"ofile",
68 help=
"output NetCDF4 file; defaults to PACE.yyyymmddThhmmss.HKT.nc")
69 parser.add_argument(
'-s',
'--skip_raw', action=
'store_true',
70 default=
False, help=
"don't save raw housekeeping data")
71 parser.add_argument(
'-v',
'--verbose', action=
'store_true',
72 default=
False, help=
"print status messages")
73 parser.add_argument(
"--pversion", metavar=
"pversion", dest=
"pversion",
74 help=
"Processing version of the program")
75 parser.add_argument(
"--doi", metavar=
"doi", dest=
"doi",
76 help=
"Identifier_product_doi")
78 args = parser.parse_args()
81 loglevel = logging.INFO
if args.verbose
else logging.WARNING
82 logging.basicConfig(format=
'%(levelname)s: %(message)s', level=loglevel)
85 pktDir = os.path.join(os.getenv(
'OCDATAROOT'),
'telemetry',
'pace',
'hkt')
86 if not os.path.exists(pktDir):
87 logging.error(f
'ERROR: The directory {pktDir} does not exist.')
90 for apid
in good_apids:
92 pktDict[apid][
'recordlist'] = []
93 csvfile = os.path.join(f
'{pktDir}',f
'APID{apid}.csv')
94 if not os.path.exists(csvfile):
95 logging.error(f
'ERROR: The file {csvfile} does not exist.')
97 pktDict[apid][
'packetdef'] = ccsdspy.FixedLength.from_file(csvfile)
101 pktDict[tilt][
'packetdef'].add_converted_field(
'TILT_ENCPOS',
'TILT_ENCPOS',
106 infile = os.path.expandvars(args.ifile)
109 with open(infile, mode=
'rt')
as flist:
113 filelist.append(os.path.expandvars(ifile.rstrip()))
114 except UnicodeDecodeError:
118 logging.error(f
'{e}; exiting')
121 if not len(filelist):
122 filelist.append(infile)
125 for val
in pktypes.values():
127 oversize_packets = bytearray()
131 for filename
in filelist:
132 logging.info(f
'Reading {filename}')
135 ifile = open(filename, mode=
'rb')
137 status = 120
if status > 0
else 101
138 logging.warning(f
'{e}; continuing')
142 filehdr = readFileHeader(ifile)
144 logging.info(filehdr)
148 desired = (filehdr[
'subtype'] == 101
and
149 filehdr[
'length'] == 64
and
150 filehdr[
'SCID'] == b
'PACE' and
151 filehdr[
'processorid']
in (1, 2, 30))
153 if not filehdr
or not desired:
154 status = 120
if status > 0
else 102
156 logging.warning(f
'File {filename} has invalid header; continuing')
159 logging.error(f
'File {filename} has invalid header; returning')
167 for k, v
in header.items():
172 if header[
'CCSDS_PACKET_LENGTH'] > 16378:
173 status = 120
if status > 0
else 103
175 f
'File {filename} contains invalid CCSDS packet header: {header}')
179 if len(data) < header[
'CCSDS_PACKET_LENGTH'] + 1:
180 status = 120
if status > 0
else 104
181 logging.warning(f
'File {filename} has unexpected EOF: '
182 f
'expected {header["CCSDS_PACKET_LENGTH"]+1} more bytes, '
187 if (header[
'CCSDS_SECONDARY_FLAG'] == 1
192 header[
'timestamp'] = ts
196 apid = header[
'CCSDS_APID']
197 if apid
in good_apids:
198 myDict = (pktDict[apid][
'packetdef']).
load(BytesIO(packet))
199 for key, val
in myDict.items():
204 pktDict[apid][
'recordlist'].append(myDict)
205 logging.info(f
"taiTime={myDict['taiTime']} seconds={myDict['seconds']} "
206 f
"subsecs={myDict['subsecs']} timestamp={myDict['timestamp']}")
209 if not args.skip_raw
and apid
not in ignored_apids:
210 for val
in pktypes.values():
211 if apid <= val[
'maxapid']:
212 if len(packet) > val[
'maxlen']:
213 oversize_packets += packet
216 val[
'data'].append(packet)
227 if len(alltimes) == 0:
228 logging.warning(
'No input packets with valid times')
233 daystart = stime.replace(hour=0, minute=0, second=0, microsecond=0)
234 timeunits = f
"seconds since {daystart.strftime('%Y-%m-%d')}"
237 prod_name = stime.strftime(
'PACE.%Y%m%dT%H%M%S.HKT.nc')
238 if args.ofile
is None:
239 args.ofile = prod_name
243 logging.info(f
'Writing {args.ofile}')
244 ofile = netCDF4.Dataset(args.ofile,
'w')
245 except BaseException:
246 logging.error(
"Cannot write file \"%s\": exiting." % args.ofile)
250 ofile.createDimension(
'vector_elements', 3)
251 ofile.createDimension(
'quaternion_elements', 4)
254 if not args.skip_raw:
255 group = ofile.createGroup(
'housekeeping_data')
256 for key, val
in pktypes.items():
257 npkts = len(val[
'data'])
259 maxlen = val[
'maxlen']
261 f
"{npkts}\t{key}_HKT_packets\tx {maxlen}\t= {npkts*maxlen}\tbytes")
262 ofile.createDimension(f
'{key}_hkt_pkts',
None)
263 ofile.createDimension(f
'max_{key}_packet', maxlen)
264 var = group.createVariable(f
'{key}_HKT_packets',
'u1',
265 (f
'{key}_hkt_pkts', f
'max_{key}_packet'))
266 var.long_name = f
"{val['name']} housekeeping telemetry packets"
268 if len(oversize_packets) > 0:
269 logging.info(f
'{len(oversize_packets)}\toversize_packets')
270 ofile.createDimension(
'os_pkts',
None)
271 var = group.createVariable(
'oversize_packets',
'u1', (
'os_pkts'))
272 var.long_name =
'Buffer for packets exceeding maximum size'
273 var[:] = oversize_packets
276 group = ofile.createGroup(
'navigation_data')
278 if len(pktDict[att][
'recordlist']) > 0:
279 ofile.createDimension(
'att_records',
None)
281 var = group.createVariable(
282 'att_time',
'f8', (
'att_records'), fill_value=-32767.)
283 var.long_name =
"Attitude sample time (seconds of day)"
285 var.valid_max = MAX_SECONDS_OF_DAY
286 var.units = timeunits
288 for rec
in pktDict[att][
'recordlist']]
290 var = group.createVariable(
291 'att_quat',
'f4', (
'att_records',
'quaternion_elements'), fill_value=-32767.)
292 var.long_name =
"Attitude quaternions (J2000 to spacecraft)"
295 var.units =
"seconds"
296 var[:] = [rec[
'q_EciToBrf_Est']
for rec
in pktDict[att][
'recordlist']]
298 var = group.createVariable(
299 'att_rate',
'f4', (
'att_records',
'vector_elements'), fill_value=-32767.)
300 var.long_name =
"Attitude angular rates in spacecraft frame"
301 var.valid_min = np.array((-0.004),
'f4')
302 var.valid_max = np.array(( 0.004),
'f4')
303 var.units =
"radians/second"
304 var[:] = [rec[
'w_EciToBrf_Brf_Est']
305 for rec
in pktDict[att][
'recordlist']]
307 if len(pktDict[orb][
'recordlist']) > 0:
308 ofile.createDimension(
'orb_records',
None)
311 posr = [np.matmul(rec[
'DCM_ecef2eci'], rec[
'scPosJ2000'])
312 for rec
in pktDict[orb][
'recordlist']]
313 velr = [np.matmul(rec[
'DCM_ecef2eci'], rec[
'scVelJ2000'])
314 for rec
in pktDict[orb][
'recordlist']]
317 var = group.createVariable(
318 'orb_time',
'f8', (
'orb_records'), fill_value=-32767.)
319 var.long_name =
"Orbit vector time (seconds of day)"
321 var.valid_max = MAX_SECONDS_OF_DAY
322 var.units = timeunits
324 for rec
in pktDict[orb][
'recordlist']]
326 var = group.createVariable(
327 'orb_pos',
'f4', (
'orb_records',
'vector_elements'), fill_value=-9999999)
328 var.long_name =
"Orbit position vectors (ECR)"
329 var.valid_min = -7200000
330 var.valid_max = 7200000
332 var[:] = orbitparams[
'posr']
334 var = group.createVariable(
335 'orb_vel',
'f4', (
'orb_records',
'vector_elements'), fill_value=-32767.)
336 var.long_name =
"Orbit velocity vectors (ECR)"
337 var.valid_min = -7600
339 var.units =
"meters/second"
340 var[:] = orbitparams[
'velr']
342 var = group.createVariable(
343 'orb_lon',
'f8', (
'orb_records'), fill_value=-32767.)
344 var.long_name =
"Orbit longitude (degrees East)"
347 var.units =
"degrees_east"
348 var[:] = orbitparams[
'lon']
350 var = group.createVariable(
351 'orb_lat',
'f8', (
'orb_records'), fill_value=-32767.)
352 var.long_name =
"Orbit latitude (degrees North)"
355 var.units =
"degrees_north"
356 var[:] = orbitparams[
'lat']
358 var = group.createVariable(
359 'orb_alt',
'f8', (
'orb_records'), fill_value=-32767.)
360 var.long_name =
"Orbit altitude"
361 var.valid_min = 670000
362 var.valid_max = 710000
364 var[:] = orbitparams[
'alt']
366 if len(pktDict[tilt][
'recordlist']) > 0:
367 ofile.createDimension(
'tilt_records',
None)
369 var = group.createVariable(
370 'tilt_time',
'f8', (
'tilt_records'), fill_value=-32767.)
371 var.long_name =
"Tilt sample time (seconds of day)"
373 var.valid_max = MAX_SECONDS_OF_DAY
374 var.units = timeunits
376 for rec
in pktDict[tilt][
'recordlist']]
378 var = group.createVariable(
379 'tilt',
'f4', (
'tilt_records'), fill_value=-32767.)
380 var.long_name =
"Tilt angle"
381 var.valid_min = np.array((-20.1),
'f4')
382 var.valid_max = np.array(( 20.1),
'f4')
383 var.units =
"degrees"
384 var[:] = [rec[
'TILT_ENCPOS']
for rec
in pktDict[tilt][
'recordlist']]
386 var = group.createVariable(
387 'tilt_flag',
'u1', (
'tilt_records'), fill_value=255)
388 var.long_name =
"Tilt quality flag"
389 var.flag_values = np.array([0, 1],
'u1')
390 var.flag_meanings =
"Valid Not_initialized"
391 var[:] = [1 - rec[
'TILT_HOME_INIT']
392 for rec
in pktDict[tilt][
'recordlist']]
397 ofile.title =
"PACE HKT Data"
398 ofile.instrument =
"Observatory"
399 ofile.processing_version =
"V1.0"
400 ofile.institution =
"NASA Goddard Space Flight Center, Ocean Biology Processing Group"
401 ofile.license =
"https://science.nasa.gov/earth-science/earth-science-data/data-information-policy/"
402 ofile.naming_authority =
"gov.nasa.gsfc.oceancolor"
403 ofile.creator_name =
"NASA/GSFC/OBPG"
404 ofile.creator_email =
"data@oceancolor.gsfc.nasa.gov"
405 ofile.creator_url =
"https://oceancolor.gsfc.nasa.gov"
406 ofile.project =
"Ocean Biology Processing Group"
407 ofile.publisher_name =
"NASA/GSFC/OB.DAAC"
408 ofile.publisher_email =
"data@oceancolor.gsfc.nasa.gov"
409 ofile.publisher_url =
"https://oceancolor.gsfc.nasa.gov"
410 ofile.processing_level =
"L0"
411 ofile.Conventions =
"CF-1.8 ACDD-1.3"
412 ofile.standard_name_vocabulary =
'CF Standard Name Table v79'
413 ofile.keywords_vocabulary =
"NASA Global Change Master Directory (GCMD) Science Keywords"
415 ofile.product_name = args.ofile
418 ofile.history =
' '.join([v
for v
in sys.argv])
420 ofile.source =
','.join([os.path.basename(f)
for f
in filelist])
421 ofile.date_created = datetime.datetime.utcnow().strftime(
'%Y-%m-%dT%H:%M:%SZ')
424 if args.pversion !=
None:
425 ofile.processing_version = args.pversion
428 ofile.identifier_product_doi_authority =
"https://dx.doi.org"
429 ofile.identifier_product_doi = args.doi
432 group = ofile.createGroup(
'processing_control')
433 group.setncattr(
"software_name",
"hktgen_pace")
434 group.setncattr(
"software_version", __version__)
435 group.setncattr(
"hsk_files",
",".join(filelist))
439 group1 = group.createGroup(
"input_parameters")
440 group1.setncattr(
"ifile", args.ifile)
441 group1.setncattr(
"ofile", args.ofile)
442 group1.setncattr(
"skip_raw",
"True" if args.skip_raw
else "False")
448 logging.warning(
'Exiting with status code %s' % status)
452 if __name__ ==
'__main__':