diff --git a/src/pyfaradaycup/pipeline/ccsds_reader_pipeline.py b/src/pyfaradaycup/pipeline/ccsds_reader_pipeline.py index 72eba78..59aed89 100644 --- a/src/pyfaradaycup/pipeline/ccsds_reader_pipeline.py +++ b/src/pyfaradaycup/pipeline/ccsds_reader_pipeline.py @@ -35,12 +35,41 @@ ######################################### + + def read_stdin(ptp=False, verbose=False): # ruff:ignore[ANN001, ANN201, FBT002] """Parse binary stream on stdin""" # ruff:ignore[D400] ######################################### -def file2bytestr(path="", verbose=False, gzip=False): # ruff:ignore[ANN001, ANN201, ARG001, D103, FBT002] + + +def file2bytestr(path="", verbose=False, gzip=False): # ruff:ignore[ANN001, ANN201, ARG001, FBT002] + """ + Read the entire contents of a file into a bytes object. + + Parameters + ---------- + path : str, optional + Path to the file to read. + + verbose : bool, optional + Not currently used. + + gzip : bool, optional + If `True`, read the file as gzip-compressed. + + Returns + ------- + bytes + The raw contents of the file. + + Notes + ----- + If the file cannot be read, this function prints the error, opens a + ``pdb`` debugging session, and then exits the program. + @namurphy - should we start to replace these ``pdb`` so we don't heavy over use ``ruff:ignore`` + """ try: if gzip: import gzip # ruff:ignore[PLC0415] @@ -62,8 +91,36 @@ def file2bytestr(path="", verbose=False, gzip=False): # ruff:ignore[ANN001, ANN ######################################### -def choose_file(path="", ptp=False, verbose=False): # ruff:ignore[ANN001, ANN201, ARG001, D103, FBT002] + + +def choose_file(path="", ptp=False, verbose=False): # ruff:ignore[ANN001, ANN201, ARG001, FBT002] # make sure file exists + """ + Check that a file can be opened and return its path. + + Parameters + ---------- + path : str, optional + Path to the file to check. + + ptp : bool, optional + Not currently used. + + verbose : bool, optional + Not currently used. + + Returns + ------- + str + The input ``path`` if the file can be opened, or an empty string + if it cannot be opened or no path was given. + + Notes + ----- + If the file cannot be opened, an error message is printed instead of + raising an exception. An interactive file dialog was used here + previously but is currently disabled. + """ try: open(path).close() # ruff:ignore[PTH123] except: # ruff:ignore[E722] @@ -82,8 +139,43 @@ def choose_file(path="", ptp=False, verbose=False): # ruff:ignore[ANN001, ANN20 ######################################### -def wrapper_status(path="", verbose=False, gzip=False, spconly=False): # ruff:ignore[ANN001, ANN201, ARG001, D103, FBT002] + +def wrapper_status(path="", verbose=False, gzip=False, spconly=False): # ruff:ignore[ANN001, ANN201, ARG001, FBT002] + """ + Read CCSDS headers from SWEM wrapper packets and the packets inside them. + + Parameters + ---------- + path : str, optional + Path to the CCSDS file to read. + + verbose : bool, optional + Not currently used. + + gzip : bool, optional + If `True`, read the file as gzip-compressed. + + spconly : bool, optional + If `True`, only match SPC instrument APIDs (0x351-0x354, 0x35E, + 0x35F). If `False`, match any instrument APID from 0x351 to 0x39F. + + Returns + ------- + dict of str to list + Header values for each matched packet pair. The keys are + ``"wrap_met"``, ``"wrap_apid"``, and ``"wrap_seq"`` for the + wrapper packet, and ``"data_met"``, ``"data_apid"``, and + ``"data_seq"`` for the instrument packet inside it. MET is the + mission elapsed time and seq is the CCSDS sequence count. If no + packets are found, each list is empty. + + Notes + ----- + Packets are found by searching the raw bytes for a SWEM wrapper + header (APIDs 0x348-0x350) followed by an instrument header. Only + the headers are decoded, not the packet data. + """ # get a filename if not specified path = choose_file(path) @@ -146,6 +238,8 @@ def wrapper_status(path="", verbose=False, gzip=False, spconly=False): # ruff:i ######################################### + + def read_file(path="", verbose=False, gzip=False): # ruff:ignore[ANN001, ANN201, C901, FBT002] """Read a CCSDS File and return data structure""" # ruff:ignore[D400] # get a filename if not specified @@ -238,6 +332,8 @@ def read_file(path="", verbose=False, gzip=False): # ruff:ignore[ANN001, ANN201 ######################################### + + def read_file_sc(path="", verbose=False, ptp=False, gzip=False): # ruff:ignore[ANN001, ANN201, C901, FBT002, PLR0912, PLR0915] """Read a CCSDS File and return data structure""" # ruff:ignore[D400] # get a filename if not specified @@ -434,6 +530,8 @@ def read_file_sc(path="", verbose=False, ptp=False, gzip=False): # ruff:ignore[ ######################################### + + def read_bytestr(bytestr, pointer, data, apidformat, pktcnt, verbose=False): # ruff:ignore[ANN001, ANN201, C901, FBT002, PLR0912, PLR0913, RET503] """Take a hex string and find packets""" # ruff:ignore[D400] # Parse the CCSDS header @@ -495,7 +593,39 @@ def read_bytestr(bytestr, pointer, data, apidformat, pktcnt, verbose=False): # ######################################### -def parse_ccsds_head(bytestr, verbose=False): # ruff:ignore[ANN001, ANN201, ARG001, D103, FBT002] + + +def parse_ccsds_head(bytestr, verbose=False): # ruff:ignore[ANN001, ANN201, ARG001, FBT002] + """ + Decode a 10-byte CCSDS packet header into its fields. + + Parameters + ---------- + bytestr : bytes + The header bytes. Only the first 10 bytes are used. + + verbose : bool, optional + Not currently used. + + Returns + ------- + dict of str to int + The header fields, with keys ``"CCSDS_Version"``, + ``"CCSDS_PacketType"``, ``"CCSDS_SecHdrFlag"``, ``"CCSDS_ApID"``, + ``"CCSDS_GroupFlags"``, ``"CCSDS_SeqCnt"``, ``"CCSDS_PacketLen"``, + and ``"CCSDS_MET"``. + + Raises + ------ + ValueError + If ``bytestr`` is shorter than 10 bytes. + + Notes + ----- + The first 6 bytes are the standard CCSDS primary header. The next + 4 bytes are read as the mission elapsed time (MET), in seconds, from + the secondary header. + """ bytearr = struct.unpack("B" * len(bytestr), bytestr) exp_length = 10 @@ -519,6 +649,8 @@ def parse_ccsds_head(bytestr, verbose=False): # ruff:ignore[ANN001, ANN201, ARG ######################################### + + def parse_pkt(bytestr, data, apidformat, apid, ccsds_head, verbose=False): # ruff:ignore[ANN001, ANN201, ARG001, C901, FBT002, PLR0912, PLR0913] """Parse one CCSDS packet""" # ruff:ignore[D400] # The format for this APIDs packet list @@ -623,8 +755,46 @@ def parse_pkt(bytestr, data, apidformat, apid, ccsds_head, verbose=False): # ru thisdat[key].append(newdat[key]) -######################################### -class apid_obj: # ruff:ignore[D101, N801] +class apid_obj: # ruff:ignore[ N801] + """ + Store the bit layout of one packet type (APID). + + An empty instance is created by `get_layout` or `get_layout_sc`, + which then fill in the attributes from a telemetry definition file. + + Attributes + ---------- + names : list of str + Mnemonic (field name) of each field in the packet. + + bits : list of int + Length of each field, in bits. + + bytestart, bitstart : list or numpy.ndarray of int + Byte and bit position where each field starts. Set by + `get_layout`. + + byteend, bitend : list or numpy.ndarray of int + Byte and bit position where each field ends. Set by + `get_layout`. + + startbyte, startbit : list of int + Byte and bit position where each field starts, as listed in the + spacecraft housekeeping definition file. Set by `get_layout_sc`. + + data : dict of str to list + An empty list for each mnemonic. Set by `get_layout`. + + Notes + ----- + `get_layout` and `get_layout_sc` also add an ``apid`` attribute + (the APID as an int). `get_layout` adds a ``sw_data_vars`` + attribute (a list of mnemonics in the science data block) for + packets that have one. + """ + + ######################################### + def __init__(self): # ruff:ignore[ANN204] self.names = [] self.bits = [] @@ -638,7 +808,33 @@ def __init__(self): # ruff:ignore[ANN204] ######################################### -def get_layout(apid, verbose=False): # ruff:ignore[ANN001, ANN201, C901, D103, FBT002] + + +def get_layout(apid, verbose=False): # ruff:ignore[ANN001, ANN201, C901, FBT002] + """ + Read the bit layout for one SWEAP APID from ``sweap_tlm.blk``. + + Parameters + ---------- + apid : int + The APID to look up, such as ``0x352``. + + verbose : bool, optional + If `True`, print status messages. + + Returns + ------- + apid_obj or None + The layout of each field in the packet, or `None` if the APID + is not found in the file. + + Notes + ----- + The file ``sweap_tlm.blk`` is looked for first in the current + working directory and then in the directory containing this module. + The second lookup builds the path with Windows-style backslashes, + so it only works on Windows. + """ try: file = open("sweap_tlm.blk") # ruff:ignore[PTH123, SIM115] except: # ruff:ignore[E722] @@ -708,7 +904,36 @@ def get_layout(apid, verbose=False): # ruff:ignore[ANN001, ANN201, C901, D103, ######################################### -def get_layout_sc(apid, verbose=False, filename=""): # ruff:ignore[ANN001, ANN201, C901, D103, FBT002] +def get_layout_sc(apid, verbose=False, filename=""): # ruff:ignore[ANN001, ANN201, C901, FBT002] + """ + Read the bit layout for one spacecraft housekeeping APID. + + Parameters + ---------- + apid : int + The APID to look up. + + verbose : bool, optional + If `True`, print a message when the APID is found. + + filename : str, optional + Path to the spacecraft housekeeping telemetry definition + (``.blk``) file. + + Returns + ------- + tuple of (apid_obj, int) or None + The layout of each field in the packet and the packet length + from the file's ``Block[...]`` line, or `None` if the APID is + not found in the file. + + Notes + ----- + The APID section in the file starts with a line like + ``SC_HK_0x``. Fields written as ``mnemonic[N]`` are treated + as ``N`` bytes long (``8 * N`` bits). If the file cannot be opened, + a ``pdb`` debugging session is started. + """ try: file = open(filename) # ruff:ignore[PTH123, SIM115] print(f"using sc_hk file: {filename}") # ruff:ignore[T201] diff --git a/src/pyfaradaycup/pipeline/swp_spc_l02l1.py b/src/pyfaradaycup/pipeline/swp_spc_l02l1.py index f9254f5..434d0c1 100644 --- a/src/pyfaradaycup/pipeline/swp_spc_l02l1.py +++ b/src/pyfaradaycup/pipeline/swp_spc_l02l1.py @@ -75,7 +75,47 @@ def main( # ruff:ignore[ANN201, C901, PLR0912, PLR0913, PLR0915, PLR0917] overwrite=False, # ruff:ignore[ANN001, FBT002] verbose=False, # ruff:ignore[ANN001, FBT002] ): - """Convert a single L0 file to L1""" # ruff:ignore[D400] + """ + Convert one SPC L0 file into L1 CDF files, one per APID. + + Parameters + ---------- + l0file : str, optional + Path to the L0 file to convert. + + l1dir : str, optional + Directory for the L1 CDF files. If empty, the directory of + ``l0file`` is used. + + logdir : str, optional + Directory for the log file. If empty, ``l1dir`` is used. It is + created if it does not exist. + + spacecraft : bool, optional + If `True`, read spacecraft housekeeping packets with + `~pyfaradaycup.pipeline.ccsds_reader_pipeline.read_file_sc`. + If `False`, read SWEAP instrument packets with + `~pyfaradaycup.pipeline.ccsds_reader_pipeline.read_file`. + + ptp : bool, optional + If `True`, the L0 file is a PTP file. Only used when + ``spacecraft`` is `True`. + + gzip : bool, optional + If `True`, read the L0 file as gzip-compressed. + + apidreq : int, optional + Only create a CDF for this APID. If ``0``, create a CDF for + every supported APID found in the file. + + overwrite : bool, optional + If `True`, replace L1 CDF files that already exist. If `False` + and a file already exists, the program exits. + + verbose : bool, optional + If `True`, print messages to the screen as well as to the log + file. + """ # Try to create a filename for the new CDF that we're going to create l0dirname = os.path.dirname(l0file) # ruff:ignore[PTH120] l0basename = os.path.basename(l0file) # ruff:ignore[PTH119] @@ -317,7 +357,42 @@ def main( # ruff:ignore[ANN201, C901, PLR0912, PLR0913, PLR0915, PLR0917] def cdf35e_35f(cdf, dat, verbose=False) -> None: # ruff:ignore[ANN001, C901, FBT002] - """Fill up a CDF with data from an SPC HSK (0x35E or 0x35F) packet or S/C HSK packet""" # ruff:ignore[D400] + """ + Fill a CDF with housekeeping data, one row per packet. + + This handles SPC housekeeping packets (APIDs 0x35E and 0x35F) and + the spacecraft housekeeping packets that `main` sends here + (APIDs 0x081, 0x1DE, 0x254, 0x256, 0x257, and 0x262). + + Parameters + ---------- + cdf : spacepy.pycdf.CDF + The L1 CDF file to write the data into. + + dat : dict of str to list + Decoded L0 data for one APID, with one entry per packet for + each mnemonic. + + verbose : bool, optional + If `True`, print error messages to the screen as well as to the + log file. + + Notes + ----- + Unlike `cdf352` and `cdf351_353_354`, the data is not expanded: + each packet becomes one row in the CDF. + + ``"Epoch"`` (nanoseconds past J2000) is calculated from whichever + MET fields ``dat`` contains: ``"CCSDS_MET"`` for SPC packets, or + one of several ``*_TPSH_MET_SEC`` fields for spacecraft packets. + If none are found, an error is logged and nothing is written. + ``"Epoch"`` is also added to ``dat``. + + Each variable in the CDF is filled from the matching key in + ``dat``. Variables with no matching key are filled with the + variable's ``FILLVAL``. If an unexpected error occurs, a ``pdb`` + debugging session is started. + """ # Calculate MET from the variables in the L0 data # MET of each NYS if "CCSDS_MET" in dat.keys(): # ruff:ignore[SIM118] @@ -382,8 +457,51 @@ def cdf35e_35f(cdf, dat, verbose=False) -> None: # ruff:ignore[ANN001, C901, FB ##################################################### ## ##################################################### + + def cdf351_353_354(cdf, dat, nocdf=False, verbose=False): # ruff:ignore[ANN001, ANN201, C901, FBT002, PLR0912, PLR0915, RET503] - """Fill up a CDF with SCI, ALL, or RSS data.""" + """ + Expand SPC science packets (APIDs 0x351, 0x353, 0x354) and write them to a CDF. + + Each packet holds all the measurements from one NY second. This + function gives every measurement its own timestamp and puts each + variable into a flat array. APID 0x351 holds AllGain (ALL) data, + 0x353 holds SCI data, and 0x354 holds RSS data. + + Parameters + ---------- + cdf : spacepy.pycdf.CDF + The L1 CDF file to write the data into. Not used if ``nocdf`` + is `True`. + + dat : dict of str to list + Decoded L0 data for one of these APIDs, with one entry per + packet for each mnemonic. Must include ``"CCSDS_ApID"``, + ``"CCSDS_MET"``, ``"SW_SPCSUBSEC"``, ``"SW_SPC_INTTIME"``, + ``"SW_SPC_SERVTIME"``, ``"WINDOW"``, and the variable that + sets the number of measurements (``"A1S"``, ``"ASIN"``, or + ``"ARSS"``). APID 0x351 also needs ``"SW_SPC_PKTNUM"``. + + nocdf : bool, optional + If `True`, return the expanded data instead of writing it to + ``cdf``. + + verbose : bool, optional + If `True`, print warnings and errors to the screen as well as + to the log file. + + Returns + ------- + dict of str to list or None + If ``nocdf`` is `True`, the expanded data, with one value per + measurement for each key. Otherwise, `None`. + + Notes + ----- + ``"Epoch"`` is in nanoseconds past J2000. Measurements are spaced + by the integration time plus the settling time (IT + ST), in ticks + of 1/1171.875 seconds (1024 ticks per NY second). + """ # Take data sorted by NYS, and produce one long variable with all data # APID of this packet @@ -539,7 +657,54 @@ def cdf351_353_354(cdf, dat, nocdf=False, verbose=False): # ruff:ignore[ANN001, pdb.set_trace() # ruff:ignore[T100] -def cdf352(cdf, dat, nocdf=False, verbose=False): # ruff:ignore[ANN001, ANN201, C901, D103, FBT002, PLR0912, PLR0915] +def cdf352(cdf, dat, nocdf=False, verbose=False): # ruff:ignore[ANN001, ANN201, C901, FBT002, PLR0912, PLR0915] + """ + Expand SPC time series (APID 0x352) packets into L1 data and write them to a CDF. + + Each 0x352 packet holds many fast measurements from one NY second, + for four channels at a time. This function gives every measurement + its own timestamp and puts each channel into a flat array. + + Parameters + ---------- + cdf : spacepy.pycdf.CDF + The L1 CDF file to write the data into. Not used if ``nocdf`` + is `True`. + + dat : dict of str to list + Decoded L0 data for APID 0x352, with one entry per packet for + each mnemonic. Must include ``"CCSDS_MET"``, ``"SW_SPCSUBSEC"``, + ``"SPC_TIMESERCOLL"``, ``"SPC_TIMESERTICK"``, and the + measurement arrays ``"G0_000"`` through ``"G3_000"``. + + nocdf : bool, optional + If `True`, return the expanded data instead of writing it to + ``cdf``. + + verbose : bool, optional + If `True`, print error messages to the screen as well as to the + log file. + + Returns + ------- + dict of str to list or tuple + If ``nocdf`` is `True`, the expanded data, with one value per + measurement for each key. Otherwise, an empty tuple. + + Notes + ----- + ``"SPC_TIMESERCOLL"`` sets which four channels a packet contains: + ``1``, ``2``, ``4``, and ``8`` for the A, B, C, and D collectors + (channels 0-3), and ``16`` and ``32`` for two sets of housekeeping + voltages. The values go into ``"VAR0"`` through ``"VAR3"``, and the + channel names go into ``"VAR0_NAME"`` through ``"VAR3_NAME"``. + Packets with any other value are logged as errors and skipped. + + ``"Epoch"`` is in nanoseconds past J2000. Each measurement is + spaced ``1 / (32 * 1171.875)`` seconds apart, starting at the + packet's start tick. Values that appear once per packet are + repeated for every measurement in that packet. + """ try: # Calculate SCET from the variables in the L0 data dt = secsubsec2scet(dat["CCSDS_MET"], dat["SW_SPCSUBSEC"]) @@ -674,7 +839,41 @@ def cdf352(cdf, dat, nocdf=False, verbose=False): # ruff:ignore[ANN001, ANN201, def secsubsec2scet(sec, subsec, spacecraft=False, verbose=False): # ruff:ignore[ANN001, ANN201, ARG001, FBT002] - """Parse a fairly standard CCSDS time structure into decimal MET: first 4 bytes=MET seconds, second 2 bytes = MET subseconds""" # ruff:ignore[D400] + """ + Convert MET seconds and subseconds to ephemeris time in nanoseconds. + + The conversion uses the PSP spacecraft clock through SPICE. + + Parameters + ---------- + sec : list of int + MET whole seconds, from the first 4 bytes of the CCSDS time + field. + + subsec : list of int + MET subseconds, from the next 2 bytes of the CCSDS time field. + + spacecraft : bool, optional + If `True`, ``subsec`` is in units of 1/256 second, as used in + spacecraft packets. If `False`, ``subsec`` is in units of + 1/65536 second, as used in SWEAP packets. + + verbose : bool, optional + Not currently used. + + Returns + ------- + list of float + Ephemeris time for each input, in nanoseconds past J2000, + rounded to the nearest nanosecond. + + Notes + ----- + The subseconds are rescaled to the 1/50000 second ticks used by + the PSP clock kernel, and each time is converted with + ``spiceypy.scs2e`` using NAIF ID -96 (PSP). The PSP clock (SCLK) + and leap second kernels must already be loaded. + """ sec_str = [f"{i:1.0f}" for i in sec] subsec_str_base50000 = [ f"{int(i * 50000 / 65536):05.0f}" for i in subsec @@ -694,7 +893,31 @@ def secsubsec2scet(sec, subsec, spacecraft=False, verbose=False): # ruff:ignore def statusmsg(string, screen=False, file=True, verbose=False): # ruff:ignore[ANN001, ANN201, FBT002] - """Output status message to screen or logfile (default to file, but not screen)""" # ruff:ignore[D400] + """ + Write a timestamped status message to the log file and/or the screen. + + Parameters + ---------- + string : str + The message to write. + + screen : bool, optional + If `True`, also print the message to the screen, but only when + ``verbose`` is also `True`. + + file : bool, optional + If `True`, write the message to the log file. + + verbose : bool, optional + Must be `True` for ``screen`` to have any effect. + + Notes + ----- + Messages written to the log file start with the current local time + in ISO format, followed by a comma. The log file is the global + ``logfile`` opened by `main`, so this function only works after + `main` has opened it. + """ nowdtstr = datetime.datetime.now().isoformat() # ruff:ignore[DTZ005] if file: logfile.write(nowdtstr + ", " + string + "\n") @@ -704,7 +927,37 @@ def statusmsg(string, screen=False, file=True, verbose=False): # ruff:ignore[AN def get_newest_kernel(tls=False, sclk=False, verbose=False): # ruff:ignore[ANN001, ANN201, ARG001, FBT002] - """Find the path to the newest NAIF TLS (leap second) kernel file""" # ruff:ignore[D400] + """ + Find the newest NAIF leap second or PSP clock (SCLK) kernel file. + + Exactly one of ``tls`` or ``sclk`` must be `True`. + + Parameters + ---------- + tls : bool, optional + If `True`, find the newest leap second kernel + (``naif00NN.tls``). + + sclk : bool, optional + If `True`, find the newest PSP clock kernel + (``spp_sclk_NNNN.tsc``). + + verbose : bool, optional + Not currently used. + + Returns + ------- + str or bool + Path to the kernel file with the highest version number, or + `False` if both or neither of ``tls`` and ``sclk`` are `True`. + + Notes + ----- + The kernels are searched for in fixed directories under + ``/psp/data/moc_data_products/``, so this only works on a system + with that directory layout. The version number is read from the + digits at the end of the file name. + """ # Make sure we chose exactly one of the options if tls + sclk != 1: return False @@ -740,7 +993,30 @@ def get_newest_kernel(tls=False, sclk=False, verbose=False): # ruff:ignore[ANN0 def get_newest_skeleton(apid, verbose=False): # ruff:ignore[ANN001, ANN201, ARG001, FBT002] - """Find the path to the newest skeleton CDF file""" # ruff:ignore[D400] + """ + Return the path to the skeleton CDF file for an APID. + + Parameters + ---------- + apid : int + The APID of the skeleton file, such as ``0x352``. + + verbose : bool, optional + Not currently used. + + Returns + ------- + str + The path ``cdf_skeletons/psp_swp_spc_l1__skeleton.cdf``, + with the APID as three lowercase hexadecimal digits. + + Notes + ----- + The path is relative to the current working directory. The + function does not check that the file exists. Earlier versions + searched for the newest versioned skeleton file; that code is + kept below as comments. + """ return f"cdf_skeletons/psp_swp_spc_l1_{hex(apid)[2:].zfill(3)}_skeleton.cdf" # ruff:ignore[FURB116] # The remaining code in this function is from when we used skeleton file numbers with a version # in them @@ -770,8 +1046,26 @@ def get_newest_skeleton(apid, verbose=False): # ruff:ignore[ANN001, ANN201, ARG ##################################################### ### ##################################################### + + def setup(): # ruff:ignore[ANN201] - """Get user command-line input and set things up""" # ruff:ignore[D400] + """ + Read the command-line arguments for running this module as a script. + + Returns + ------- + argparse.Namespace + The parsed arguments. ``apid`` is converted to an integer, and + a ``version`` attribute (the data product version) is added. + + Raises + ------ + KeyError + If the ``PSP_DATA_DIR`` environment variable is not set. + + ValueError + If the directory in ``PSP_DATA_DIR`` does not exist. + """ # defaults l0file_default = "" l0dir_default = "" @@ -939,6 +1233,8 @@ def setup(): # ruff:ignore[ANN201] ############################################ #### ############################################ + + if __name__ == "__main__": args = setup() main( @@ -952,3 +1248,44 @@ def setup(): # ruff:ignore[ANN201] overwrite=args.overwrite, verbose=args.verbose, ) + """ + Convert one SPC L0 file into L1 CDF files, one per APID. + + Parameters + ---------- + l0file : str, optional + Path to the L0 file to convert. + + l1dir : str, optional + Directory for the L1 CDF files. If empty, the directory of + ``l0file`` is used. + + logdir : str, optional + Directory for the log file. If empty, ``l1dir`` is used. It is + created if it does not exist. + + spacecraft : bool, optional + If `True`, read spacecraft housekeeping packets with + `~pyfaradaycup.pipeline.ccsds_reader_pipeline.read_file_sc`. + If `False`, read SWEAP instrument packets with + `~pyfaradaycup.pipeline.ccsds_reader_pipeline.read_file`. + + ptp : bool, optional + If `True`, the L0 file is a PTP file. Only used when + ``spacecraft`` is `True`. + + gzip : bool, optional + If `True`, read the L0 file as gzip-compressed. + + apidreq : int, optional + Only create a CDF for this APID. If ``0``, create a CDF for + every supported APID found in the file. + + overwrite : bool, optional + If `True`, replace L1 CDF files that already exist. If `False` + and a file already exists, the program exits. + + verbose : bool, optional + If `True`, print messages to the screen as well as to the log + file. + """