#!/usr/bin/env python3
#
# This file is part of pyspex
#    https://github.com/rmvanhees/pyspex.git
#
# Copyright (c) 2022-2024 SRON - Netherlands Institute for Space Research
#    All Rights Reserved
#
# License:  BSD-3-Clause
#
"""An amalgamation of the pyspex code needed for SPEXone L0-L1A processing.

Intended for operational processing of SPEXone data at NASA Goddard Space
 Flight Center, Ocean Biology Processing Group.

Notes
-----
- Although pyspex is using features from Python v3.11+, the code of this module
  should run fine with Python v3.8 and the modules listed in requirements-3.8.txt.
- Set environment variable OCVARROOT as '$OCVARROOT/common/tai-utc.dat'


"""

from __future__ import annotations

import argparse
import datetime as dt
import logging
import sys
import warnings
from copy import copy
from dataclasses import dataclass, field
from enum import IntFlag, auto
from logging.config import dictConfig
from os import environ
from pathlib import Path, PurePosixPath
from typing import Any

import h5py
import julian
import numpy as np
import numpy.typing as npt
import xarray as xr
import yaml

# pylint: disable=no-name-in-module
from netCDF4 import Dataset, Variable
from numpy import ma

# - global parameters -----------------------
module_logger = logging.getLogger("pyspex.l1agen_spex")

ORBIT_DURATION = 5904  # seconds

DATE_MIN = dt.datetime(2020, 1, 1, 1, tzinfo=dt.timezone.utc)
TSTAMP_MIN = int(DATE_MIN.timestamp())
TSTAMP_TYPE = np.dtype([("tai_sec", int), ("sub_sec", int), ("dt", "O")])

# reduce extend of navigation data to
TEN_SECONDS = np.timedelta64(10, "s")
# require an extent of navigation data of
SEVEN_SECONDS = np.timedelta64(7, "s")

# define detector constants/settings of the SPEXone instrument
DET_CONSTS = {
    "dimRow": 2048,
    "dimColumn": 2048,
    "dimFullFrame": 2048 * 2048,
    "DEM_frequency": 10,  # [MHz]
    "FTI_science": 1000 / 15,  # [ms]
    "FTI_diagnostic": 240.0,  # [ms]
    "FTI_margin": 212.4,  # [ms]
    "overheadTime": 0.4644,  # [ms]
    "FOT_length": 20,
}
FULLFRAME_BYTES = 2 * DET_CONSTS["dimFullFrame"]


# --------------------------------------------------
# from pyspex.lib import pyspex_version
#
# Remarks:
# - Use hardcoded version!
# --------------------------------------------------
def pyspex_version() -> str:
    """Return the software version of the original pyspex code."""
    return "1.4.13"


# --------------------------------------------------
# from pyspex.argparse_gen_l1a import argparse_gen_l1a
# --------------------------------------------------
# - global parameters ------------------------------
ARG_INPUT_HELP = """provide one or more input files:
- raw: CCSDS packages generated by the SPEXone ICU -- provide name
       of one file with extension '.H'. The files with science and
       housekeeping data are collected using Unix filename pattern
       matching.
- st3: CCSDS packages with ITOS and spacewire headers -- provide name
       of one file with extension '.ST3'.
- dsb: CCSDS packages with PACE headers -- provide list of filenames
       with extension '.spx'.
"""

ARG_YAML_HELP = """provide settings file in YAML format (in-flight example):

# define output directory, default is current working directory
outdir: .
# define name of output file, will be generated automatically when empty
outfile: ''
# compress the dataset /science_data/detector_images
compression: True
# define file-version as nn, neglected when outfile not empty
file_version: 1
# flag to indicate measurements taken in eclipse or day-side
eclipse: True
# provide list, directory, file-glob or empty
hkt_list: <PATH>/PACE.20220617T011*.HKT.nc
# must be a list, directory or glob. Fails when empty
l0_list:
- <PATH>/SPX022000010.spx
- <PATH>/SPX022000011.spx
- <PATH>/SPX022000012.spx
- <PATH>/SPX022000013.spx

"""

_prog_name_ = Path(sys.argv[0]).name
EPILOG_HELP = f"""Usage:
  Generate L1A from OCAL level-0 data directly from the SPEXone instrument:

    {_prog_name_} <Path>/NomSciCal1_20220123T121801.676167.H

    Note that OCAL science & telemetry data is read from the files:
      <Path>/NomSciCal1_20220123T121801.676167.?
      <Path>/NomSciCal1_20220123T121801.676167.??
      <Path>/NomSciCal1_20220123T121801.676167_hk.?

  Generate L1A from OCAL level-0 data via ITOS from the PACE platform:

    {_prog_name_} <Path>/DIAG_20220124_175458_073.ST3

  Generate L1A from in-flight level-0 data, store product in directory L1A:

    {_prog_name_} --outdir L1A <Path>/SPX*.spx

  Generate L1A from in-flight level-0 data read settings from a YAML file:

    {_prog_name_} --yaml config_l1a_gen.yaml

    An example YAML file:
       outdir: L1A
       outfile: ''
       file_version: 1
       compression: False
       eclipse: False
       hkt_list: HKT/PACE.20220617T011*.HKT.nc
       l0_list: <PATH>/SPX0220000??.spx

  Dry-run, be extra verbose without generating data:

    {_prog_name_} --debug <Path>/NomSciCal1_20220123T121801.676167.H

  Read level-0 data and dump CCSDS packet headers in ASCII:

    {_prog_name_} --dump <Path>/NomSciCal1_20220123T121801.676167.H

Return codes:
  2      Failed to parse command-line parameters.
  110    One (or more) SPEXone level-0 files not found.
  115    Failed to generate output directry due to permission error.
  121    Input file not recognized as a SPEXone level-0 product.
  122    Corrupted SPEXone level-0 data.
  131    Failed to generate output file due to netCDF4 library issues.
  132    Incomplete set of navigation data detected
  135    Failed to generate output file due to permission error.

Environment:
   'OCVARROOT'
       The number of leap seconds for the TAI to UTC conversion are determined
       using the file 'tai-utc.dat'. A copy of this file is included in the
       package `pyspex`. The latest version can be obtained from
         `https://maia.usno.navy.mil/ser7/tai-utc.dat`.
       When OCVARROOT is set the path should be '$OCVARROOT/common/tai-utc.dat'.
"""


# - local functions --------------------------------
# pylint: disable=too-many-instance-attributes
@dataclass()
class Config:
    """Initiate class to hold settings for L0->L1a processing."""

    debug: bool = False
    dump: bool = False
    verbose: int = logging.NOTSET
    compression: bool = False
    outdir: Path = Path(".").resolve()
    outfile: str = ""
    file_version: int = 1
    processing_version: str = "1.0"
    eclipse: bool | None = None
    yaml_fl: Path = None
    hkt_list: list[Path] = field(default_factory=list)
    l0_format: str = ""
    l0_list: list[Path] = field(default_factory=list)


def __commandline_settings() -> Config:
    """Parse command-line parameters."""

    class NumericLevel(argparse.Action):
        """Store verbosity level of the logger as a numeric value."""

        def __call__(
            self: NumericLevel,
            parser_local: argparse.ArgumentParser,
            namespace: argparse.Namespace,
            values: str,
            option_string: str | None = None,
        ) -> None:
            numeric_level = getattr(logging, values.upper(), None)
            setattr(namespace, self.dest, numeric_level)

    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawTextHelpFormatter,
        description="Generate PACE level-1A product from SPEXone level-0 data.",
        epilog=EPILOG_HELP,
    )
    parser.add_argument(
        "-v", "--version", action="version", version="%(prog)s v" + pyspex_version()
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="be extra verbose, no output files generated",
    )
    parser.add_argument(
        "--dump", action="store_true", help="dump CCSDS packet headers in ASCII"
    )
    parser.add_argument(
        "--verbose",
        nargs="?",
        const="info",
        default=logging.WARNING,
        action=NumericLevel,
        choices=("debug", "info", "warning", "error"),
        help='set verbosity level, default is "warning"',
    )
    group = parser.add_mutually_exclusive_group(required=False)
    group.add_argument(
        "--eclipse",
        action="store_true",
        default=None,
        help="assume that measurements are perfomed in eclipse",
    )
    group.add_argument(
        "--no_eclipse",
        dest="eclipse",
        action="store_false",
        help="assume that measurements are not perfomed in eclipse",
    )
    parser.add_argument(
        "--outdir",
        type=Path,
        default=None,
        help="directory to store the generated level-1A product(s)",
    )
    # group = parser.add_mutually_exclusive_group(required=True)
    parser.add_argument("--yaml", type=Path, default=None, help=ARG_YAML_HELP)
    parser.add_argument("lv0_list", nargs="*", help=ARG_INPUT_HELP)
    args = parser.parse_args()

    config = Config()
    if args.debug:
        config.debug = True
    if args.dump:
        config.dump = True
    if args.verbose:
        config.verbose = args.verbose
    if args.eclipse is not None:
        config.eclipse = args.eclipse
    if args.outdir is not None:
        config.outdir = args.outdir
    if args.yaml:
        config.yaml_fl = args.yaml
    elif args.lv0_list:
        config.l0_list = [Path(x) for x in args.lv0_list]
    else:
        parser.error("You should provide a YAML file or names of L0 products")

    return config


# pylint: disable=too-many-branches
def __yaml_settings(config: dataclass) -> dataclass:
    """Read YAML configuration file."""
    with open(config.yaml_fl, encoding="ascii") as fid:
        config_yaml = yaml.safe_load(fid)

    if "outdir" in config_yaml and config_yaml["outdir"] is not None:
        config.outdir = Path(config_yaml["outdir"])
    if "outfile" in config_yaml and config_yaml["outfile"]:
        config.outfile = config_yaml["outfile"]
    if "compression" in config_yaml and config_yaml["compression"]:
        config.compression = True
    if "file_version" in config_yaml and config_yaml["file_version"] != 1:
        config.file_version = config_yaml["file_version"]
    if "processing_version" in config_yaml and config_yaml["processing_version"] != "":
        config.processing_version = config_yaml["processing_version"]
    if "eclipse" in config_yaml and config_yaml["eclipse"] is not None:
        config.eclipse = config_yaml["eclipse"]
    if "hkt_list" in config_yaml and config_yaml["hkt_list"]:
        if isinstance(config_yaml["hkt_list"], list):
            config.hkt_list = [Path(x) for x in config_yaml["hkt_list"]]
        else:
            mypath = Path(config_yaml["hkt_list"])
            if mypath.is_dir():
                config.hkt_list = sorted(Path(mypath).glob("*"))
            else:
                config.hkt_list = sorted(Path(mypath.parent).glob(mypath.name))
    if "l0_list" in config_yaml and config_yaml["l0_list"]:
        if isinstance(config_yaml["l0_list"], list):
            config.l0_list = [Path(x) for x in config_yaml["l0_list"]]
        else:
            mypath = Path(config_yaml["l0_list"])
            if mypath.is_dir():
                config.l0_list = sorted(Path(mypath).glob("*"))
            else:
                config.l0_list = sorted(Path(mypath.parent).glob(mypath.name))

    return config


# - main function ----------------------------------
def argparse_gen_l1a() -> dataclass:
    """Obtain settings from both command-line and YAML file (when provided).

    Returns
    -------
    dataclass
       settings from both command-line arguments and YAML config-file

    """
    config = __commandline_settings()
    if config.yaml_fl is None:
        return config

    if not config.yaml_fl.is_file():
        raise FileNotFoundError(config.yaml_fl)

    return __yaml_settings(config)


# --------------------------------------------------
# from pyspex.lib.attrs_def import attrs_def
# --------------------------------------------------
def attrs_def(inflight: bool = True, origin: str | None = None) -> dict:
    """Define global attributes of a SPEXone Level-1A product.

    Parameters
    ----------
    inflight : bool, default=True
       Flag for in-flight or on-ground products
    origin : str
       Product origin: 'SRON' or 'NASA'

    Returns
    -------
    dict
       Global attributes for a Level-1A product

    """
    if origin is None:
        origin = "NASA" if inflight else "SRON"

    res = {
        "title": "PACE SPEXone Level-1A data",
        "platform": "PACE",
        "instrument": "SPEXone",
        "institution": (
            "NASA Goddard Space Flight Center," " Ocean Biology Processing Group"
        ),
        "license": (
            "http://science.nasa.gov/earth-science/"
            "earth-science-data/data-information-policy/"
        ),
        "naming_authority": "gov.nasa.gsfc.sci.oceancolor",
        "keyword_vocabulary": (
            "NASA Global Change Master Directory (GCMD)" " Science Keywords"
        ),
        "stdname_vocabulary": (
            "NetCDF Climate and Forecast (CF)" " Metadata Convention"
        ),
        "standard_name_vocabulary": "CF Standard Name Table v79",
        "conventions": "CF-1.8 ACDD-1.3",
        "identifier_product_doi_authority": "http://dx.doi.org/",
        "identifier_product_doi": "https://doi.org/10.5281/zenodo.5705691",
        "creator_name": "NASA/GSFC",
        "creator_email": "data@oceancolor.gsfc.nasa.gov",
        "creator_url": "http://oceancolor.gsfc.nasa.gov",
        "project": "PACE Project",
        "publisher_name": "NASA/GSFC",
        "publisher_email": "data@oceancolor.gsfc.nasa.gov",
        "publisher_url": "http://oceancolor.gsfc.nasa.gov",
        "cdm_data_type": ("One orbit swath or granule" if inflight else "granule"),
        "cdl_version_date": "2021-09-10",
        "product_name": None,
        "processing_level": "L1A",
        "processing_version": "",
        "date_created": dt.datetime.now(dt.timezone.utc).isoformat(
            timespec="milliseconds"
        ),
        "software_name": "SPEXone L0-L1A processor",
        "software_url": "https://github.com/rmvanhees/pyspex",
        "software_version": pyspex_version(),
        "history": "spx1_level01a.py",
        "start_direction": "Ascending" if inflight else None,
        "end_direction": "Ascending" if inflight else None,
        "time_coverage_start": "yyyy-mm-ddTHH:MM:DD",
        "time_coverage_end": "yyyy-mm-ddTHH:MM:DD",
    }

    if origin == "SRON":
        res["title"] = "SPEXone Level-1A data"
        res["institution"] = "SRON Netherlands Institute for Space Research"
        res["creator_name"] = "SRON/Earth"
        res["creator_email"] = "SPEXone-MPC@sron.nl"
        res["creator_url"] = "https://www.sron.nl/missions-earth/pace-spexone"
        res["publisher_name"] = "SRON/Earth"
        res["publisher_email"] = "SPEXone-MPC@sron.nl"
        res["publisher_url"] = "https://www.sron.nl/missions-earth/pace-spexone"

    return res


# --------------------------------------------------
# from pyspex.lib.ccsds_hdr import CCSDShdr
# --------------------------------------------------
class CCSDShdr:
    """Read CCSDS telemetry packet structure.

    Which consists of the primary header: version, type, apid, grouping flag,
    sequence count and packet length, and the secondary header: tai_sec and
    sub_sec.

    """

    def __init__(self: CCSDShdr, hdr: np.ndarray | None = None) -> None:
        """Initialise the class instance.

        Parameters
        ----------
        hdr :  np.ndarray, optional
           CCSDS primary and secondary headers

        """
        self.__dtype = None
        self.__hdr = None
        if hdr is not None:
            self.__hdr = hdr
            self.__dtype = hdr.dtype

    def _tm_raw_(self: CCSDShdr) -> np.dtype:  # ApID unknown
        """Return data-type of unknown packet, just a header and byte data."""
        return np.dtype(
            [("hdr", self.__hdr.dtype), ("Data", "u1", (self.__hdr["length"] - 5))]
        )

    def _tm_800_(self: CCSDShdr) -> np.dtype:  # ApID = 0x320
        """Return data-type of NomHk packet."""
        return np.dtype([("hdr", self.__hdr.dtype), ("hk", tmtc_dtype(0x320))])

    def _tm_802_(self: CCSDShdr) -> np.dtype:  # ApID = 0x322
        """Return data-type of DemHk packet."""
        return np.dtype([("hdr", self.__hdr.dtype), ("hk", tmtc_dtype(0x322))])

    def _tm_817_(self: CCSDShdr) -> np.dtype:  # ApID = 0x331
        """Return data-type of TcAccept packet."""
        return np.dtype(
            [("hdr", self.__hdr.dtype), ("TcPacketId", ">u2"), ("TcSeqControl", ">u2")]
        )

    def _tm_818_(self: CCSDShdr) -> np.dtype:  # ApID = 0x332
        """Return data-type of TcReject packet."""
        return np.dtype(
            [
                ("hdr", self.__hdr.dtype),
                ("TcPacketId", ">u2"),
                ("TcSeqControl", ">u2"),
                ("TcRejectCode", ">u2"),
                ("RejectParameter1", ">u2"),
                ("RejectParameter2", ">u2"),
            ]
        )

    def _tm_819_(self: CCSDShdr) -> np.dtype:  # ApID = 0x333
        """Return data-type of TcExecute packet."""
        return np.dtype(
            [("hdr", self.__hdr.dtype), ("TcPacketId", ">u2"), ("TcSeqControl", ">u2")]
        )

    def _tm_820_(self: CCSDShdr) -> np.dtype:  # ApID = 0x334
        """Return data-type of TcFail packet."""
        return np.dtype(
            [
                ("hdr", self.__hdr.dtype),
                ("TcPacketId", ">u2"),
                ("TcSeqControl", ">u2"),
                ("TcFailCode", ">u2"),
                ("FailParameter1", ">u2"),
                ("FailParameter2", ">u2"),
            ]
        )

    def _tm_821_(self: CCSDShdr) -> np.dtype:  # ApID = 0x335
        """Return data-type of EventRp packet."""
        return np.dtype(
            [
                ("hdr", self.__hdr.dtype),
                ("Event_ID", "u1"),
                ("Event_Sev", "u1"),
                ("Word1", ">u2"),
                ("Word2", ">u2"),
                ("Word3", ">u2"),
                ("Word4", ">u2"),
                ("Word5", ">u2"),
                ("Word6", ">u2"),
                ("Word7", ">u2"),
                ("Word8", ">u2"),
            ]
        )

    def _tm_832_(self: CCSDShdr) -> np.dtype:  # ApID = 0x340
        """Return data-type of MemDump packet."""
        if self.__hdr["length"] - 15 == 1:
            return np.dtype(
                [
                    ("hdr", self.__hdr.dtype),
                    ("Image_ID", "u1"),
                    ("_FillerByte", "u1"),
                    ("Address32", ">u4"),
                    ("Length", ">u4"),
                    ("Data", "u1"),
                ]
            )

        return np.dtype(
            [
                ("hdr", self.__hdr.dtype),
                ("Image_ID", "u1"),
                ("_FillerByte", "u1"),
                ("Address32", ">u4"),
                ("Length", ">u4"),
                (
                    "Data",
                    "u1",
                    (self.__hdr["length"] - 15),
                ),
            ]
        )

    def _tm_833_(self: CCSDShdr) -> np.dtype:  # ApID = 0x341
        """Return data-type of MemCheckRp packet."""
        return np.dtype(
            [
                ("hdr", self.__hdr.dtype),
                ("Image_ID", "u1"),
                ("_FillerByte", "u1"),
                ("Address32", ">u4"),
                ("Length", ">u4"),
                ("CheckSum", ">u4"),
            ]
        )

    def _tm_826_(self: CCSDShdr) -> np.dtype:  # ApID = 0x33A
        """Return data-type of MonListRp packet."""
        mon_dtype = np.dtype(
            [
                ("Mon1_EnSts", ">u2"),
                ("Mon1_ParID", ">u2"),
                ("Mon1_Int", "u1"),
                ("Mon1_NofSampl", "u1"),
                ("Mon1_CheckType", ">u2"),
                ("Mon1_LowOrExpCheckVal", ">u4"),
                ("Mon1_LowOrExpCheckRpId", ">u4"),
                ("Mon1_UppOrExpCheckVal", ">u4"),
                ("Mon1_UppOrExpCheckRpId", ">u4"),
            ]
        )
        num = (self.__hdr["length"] - 5) // mon_dtype.itemsize
        return np.dtype([("hdr", self.__hdr.dtype), ("Report", mon_dtype, (num,))])

    def _tm_827_(self: CCSDShdr) -> np.dtype:  # ApID = 0x33B
        """Return data-type of EvRpListRp packet."""
        return np.dtype(
            [
                ("hdr", self.__hdr.dtype),
                (
                    "Data",
                    "u1",
                    (self.__hdr["length"] - 5),
                ),
            ]
        )

    def _tm_828_(self: CCSDShdr) -> np.dtype:  # ApID = 0x33C
        """Return data-type of MpsTableRp packet."""
        return np.dtype(
            [
                ("hdr", self.__hdr.dtype),
                ("MPS_ID", "u1"),
                ("MPS_VER", "u1"),
                ("FTO", ">u2"),
                ("FTI", ">u2"),
                ("FTC", ">u2"),
                ("IMRO", ">u2"),
                ("IMRSA_A", ">u4"),
                ("IMRSA_B", ">u4"),
                ("IMRLEN", ">u4"),
                ("PKTLEN", ">u2"),
                ("TMRO", ">u2"),
                ("TMRI", ">u2"),
                ("IMDMODE", "u1"),
                ("I_LVDS", "u1"),
                ("_Filler1", ">u2"),
                ("_Filler2", ">u2"),
                ("_Filler3", ">u2"),
                ("DEM_RST", "u1"),
                ("DEM_CMV_CTRL", "u1"),
                ("COADD", "u1"),
                ("DEM_IGEN", "u1"),
                ("FRAME_MODE", "u1"),
                ("OUTPMODE", "u1"),
                ("BIN_TBL", ">u4"),
                ("COADD_BUF", ">u4"),
                ("COADD_RESA", ">u4"),
                ("COADD_RESB", ">u4"),
                ("FRAME_BUFA", ">u4"),
                ("FRAME_BUFB", ">u4"),
                ("LINE_ENA", ">u4"),
                ("NUMLIN", ">u2"),
                ("STR1", ">u2"),
                ("STR2", ">u2"),
                ("STR3", ">u2"),
                ("STR4", ">u2"),
                ("STR5", ">u2"),
                ("STR6", ">u2"),
                ("STR7", ">u2"),
                ("STR8", ">u2"),
                ("NUMLIN1", ">u2"),
                ("NUMLIN2", ">u2"),
                ("NUMLIN3", ">u2"),
                ("NUMLIN4", ">u2"),
                ("NUMLIN5", ">u2"),
                ("NUMLIN6", ">u2"),
                ("NUMLIN7", ">u2"),
                ("NUMLIN8", ">u2"),
                ("SUBS", ">u2"),
                ("SUBA", ">u2"),
                ("MONO", "u1"),
                ("IMFLP", "u1"),
                ("EXP_CTRL", "u1"),
                ("_FillerByte4", "u1"),
                ("EXP_TIME", ">u4"),
                ("EXP_STEP", ">u4"),
                ("EXP_KP1", ">u4"),
                ("EXP_KP2", ">u4"),
                ("NRSLOPE", "u1"),
                ("EXP_SEQ", "u1"),
                ("EXP_TIME2", ">u4"),
                ("EXP_STEP2", ">u4"),
                ("NUMFR", ">u2"),
                ("FOTLEN", "u1"),
                ("_FillerByte5", "u1"),
                ("ILVDSRCVR", "u1"),
                ("CALIB", "u1"),
                ("TRAINPTRN", ">u2"),
                ("CHENA", ">u4"),
                ("ICOL", "u1"),
                ("ICOLPR", "u1"),
                ("IADC", "u1"),
                ("IAMP", "u1"),
                ("VTFL1", "u1"),
                ("VTFL2", "u1"),
                ("VTFL3", "u1"),
                ("VRSTL", "u1"),
                ("VPRECH", "u1"),
                ("VREF", "u1"),
                ("VRAMP1", "u1"),
                ("VRAMP2", "u1"),
                ("OFFSET", ">u2"),
                ("PGAGAIN", "u1"),
                ("ADCGAIN", "u1"),
                ("TDIG1", "u1"),
                ("TDIG2", "u1"),
                ("BITMODE", "u1"),
                ("ADCRES", "u1"),
                ("PLLENA", "u1"),
                ("PLLINFRE", "u1"),
                ("PLLBYP", "u1"),
                ("PLLRATE", "u1"),
                ("PLLLOAD", "u1"),
                ("DETDUM", "u1"),
                ("BLACKCOL", "u1"),
                ("VBLACKSUN", "u1"),
                ("_Filler6", ">u4"),
                ("_Filler7", ">u4"),
            ]
        )

    def _tm_829_(self: CCSDShdr) -> np.dtype:  # ApID = 0x33D
        """Return data-type of ThemTableRp packet."""
        return np.dtype(
            [
                ("hdr", self.__hdr.dtype),
                ("HTR_1_IsEna", "u1"),
                ("HTR_1_AtcCorMan", "u1"),
                ("HTR_1_THMCH", "u1"),
                ("_FillerByte1", "u1"),
                ("HTR_1_ManOutput", ">u2"),
                ("HTR_1_ATC_SP", ">u4"),
                ("HTR_1_ATC_P", ">u4"),
                ("HTR_1_ATC_I", ">u4"),
                ("HTR_1_ATC_I_INIT", ">u4"),
                ("HTR_2_IsEna", "u1"),
                ("HTR_2_AtcCorMan", "u1"),
                ("HTR_2_THMCH", "u1"),
                ("_FillerByte2", "u1"),
                ("HTR_2_ManOutput", ">u2"),
                ("HTR_2_ATC_SP", ">u4"),
                ("HTR_2_ATC_P", ">u4"),
                ("HTR_2_ATC_I", ">u4"),
                ("HTR_2_ATC_I_INIT", ">u4"),
                ("HTR_3_IsEna", "u1"),
                ("HTR_3_AtcCorMan", "u1"),
                ("HTR_3_THMCH", "u1"),
                ("_FillerByte3", "u1"),
                ("HTR_3_ManOutput", ">u2"),
                ("HTR_3_ATC_SP", ">u4"),
                ("HTR_3_ATC_P", ">u4"),
                ("HTR_3_ATC_I", ">u4"),
                ("HTR_3_ATC_I_INIT", ">u4"),
                ("HTR_4_IsEna", "u1"),
                ("HTR_4_AtcCorMan", "u1"),
                ("HTR_4_THMCH", "u1"),
                ("_FillerByte4", "u1"),
                ("HTR_4_ManOutput", ">u2"),
                ("HTR_4_ATC_SP", ">u4"),
                ("HTR_4_ATC_P", ">u4"),
                ("HTR_4_ATC_I", ">u4"),
                ("HTR_4_ATC_I_INIT", ">u4"),
            ]
        )

    @property
    def hdr(self: CCSDShdr) -> np.ndarray:
        """Structured array holding the CCSDS header."""
        return self.__hdr

    @property
    def dtype(self: CCSDShdr) -> np.dtype:
        """Return numpy date-type of CCSDS headers."""
        return self.__dtype

    @property
    def version(self: CCSDShdr) -> int:
        """Return zero to indicate that this is a version 1 packet."""
        return (self.__hdr["type"] >> 13) & 0x7

    @property
    def type(self: CCSDShdr) -> int:
        """Return zero to indicate that this is a telemetry packet."""
        return (self.__hdr["type"] >> 12) & 0x1

    @property
    def apid(self: CCSDShdr) -> int:
        """Return ApID: an identifier for this telemetry packet.

        Notes
        -----
        SPEXone uses the following APIDs:

        - 0x320:  NomHk
        - 0x322:  DemHk
        - 0x331:  TcAccept
        - 0x332:  TcReject
        - 0x333:  TcExecute
        - 0x334:  TcFail
        - 0x335:  EventRp
        - 0x33A:  MonListRp
        - 0x33B:  EvRpListRp
        - 0x33C:  MpsTableRp
        - 0x33D:  ThermTableRp
        - 0x340:  MemDump
        - 0x341:  MemCheckRp
        - 0x350:  Science

        """
        return self.__hdr["type"] & 0x7FF

    @property
    def grouping_flag(self: CCSDShdr) -> int:
        """Data packages can be segmented.

        Note:
        ----
        This flag is encoded as::

         00 : continuation segment
         01 : first segment
         10 : last segment
         11 : unsegmented

        """
        return (self.__hdr["sequence"] >> 14) & 0x3

    @property
    def sequence(self: CCSDShdr) -> int:
        """Return the sequence counter.

        This counter is incremented with each consecutive packet of a
        particular ApID. This value will roll over to 0 after 0x3FF is reached.
        """
        return self.__hdr["sequence"] & 0x3FFF

    @property
    def packet_size(self: CCSDShdr) -> int:
        """Returns the CCSDS packet-length.

        Which is equal to the number of bytes in the Secondary header plus
        User Data minus 1.
        """
        return self.__hdr["length"]

    @property
    def tai_sec(self: CCSDShdr) -> int:
        """Seconds since 1958 (TAI)."""
        return self.__hdr["tai_sec"]

    @property
    def sub_sec(self: CCSDShdr) -> int:
        """Sub-seconds (1 / 2**16)."""
        return self.__hdr["sub_sec"]

    @property
    def data_dtype(self: CCSDShdr) -> np.dtype:
        """Return numpy data-type of CCSDS User Data."""
        method = getattr(self, f"_tm_{self.apid:d}_", None)
        return self._tm_raw_() if method is None else method()

    def tstamp(self: CCSDShdr, epoch: dt.datetime) -> dt.datetime:
        """Return time of the telemetry packet.

        Parameters
        ----------
        epoch :  datetime
           Provide the UTC epoch of the time (thus corrected for leap seconds)

        """
        return epoch + dt.timedelta(
            seconds=int(self.tai_sec),
            microseconds=100 * int(self.sub_sec / 65536 * 10000),
        )

    def read(self: CCSDShdr, file_format: str, buffer: bytes, offs: int = 0) -> None:
        """Read CCSDS primary and secondary headers from data.

        Parameters
        ----------
        file_format :  {'raw', 'dsb' or 'st3'}
           File format of this level 0 product
        buffer :  buffer_like
           Array of type (unsigned) byte.
        offs :  int, default=0
           Start reading the buffer from this offset (in bytes)

        Notes
        -----
        SPEXone level-0 file-formats:
        'raw'
           data has no file header and standard CCSDS packet headers
        'st3'
           data has no file header and ITOS + spacewire + CCSDS packet headers
        'dsb'
           data has a cFE file-header and spacewire + CCSDS packet headers

        """
        if file_format == "dsb":
            hdr_dtype = np.dtype(
                [
                    ("spacewire", "u1", (2,)),
                    ("type", ">u2"),
                    ("sequence", ">u2"),
                    ("length", ">u2"),
                    ("tai_sec", ">u4"),
                    ("sub_sec", ">u2"),
                ]
            )
        elif file_format == "raw":
            hdr_dtype = np.dtype(
                [
                    ("type", ">u2"),
                    ("sequence", ">u2"),
                    ("length", ">u2"),
                    ("tai_sec", ">u4"),
                    ("sub_sec", ">u2"),
                ]
            )
        elif file_format == "st3":
            hdr_dtype = np.dtype(
                [
                    ("itos_hdr", ">u2", (8,)),
                    ("spacewire", "u1", (2,)),
                    ("type", ">u2"),
                    ("sequence", ">u2"),
                    ("length", ">u2"),
                    ("tai_sec", ">u4"),
                    ("sub_sec", ">u2"),
                ]
            )
        else:
            raise ValueError("Unknown file_format, should be dsb, raw or st3")

        self.__hdr = np.frombuffer(buffer, count=1, offset=offs, dtype=hdr_dtype)[0]
        self.__dtype = hdr_dtype


# --------------------------------------------------
# from pyspex.lib.check_input_files import check_input_files
# --------------------------------------------------
def check_input_files(config: dataclass) -> dataclass:
    """Check SPEXone level-0 files on existence and format.

    Parameters
    ----------
    config :  dataclass
       dataclass that contains the settings for the L0-1A processing

    Returns
    -------
    dataclass
       fields 'l0_format' {'raw', 'st3', 'dsb'} and 'l0_list' are updated.

    Raises
    ------
    FileNotFoundError
       If files are not found on the system.
    TypeError
       If determined file type differs from value supplied by user.

    """
    file_list = config.l0_list
    if file_list[0].suffix == ".H":
        if not file_list[0].is_file():
            raise FileNotFoundError(file_list[0])
        data_dir = file_list[0].parent
        file_stem = file_list[0].stem
        file_list = (
            sorted(data_dir.glob(file_stem + ".[0-9]"))
            + sorted(data_dir.glob(file_stem + ".?[0-9]"))
            + sorted(data_dir.glob(file_stem + "_hk.[0-9]"))
        )
        if not file_list:
            raise FileNotFoundError(file_stem + ".[0-9]")

        config.l0_format = "raw"
        config.l0_list = file_list
    elif file_list[0].suffix == ".ST3":
        if not file_list[0].is_file():
            raise FileNotFoundError(file_list[0])
        config.l0_format = "st3"
        config.l0_list = [file_list[0]]
    elif file_list[0].suffix == ".spx":
        file_list_out = []
        for flname in file_list:
            if not flname.is_file():
                raise FileNotFoundError(flname)

            if flname.suffix == ".spx":
                file_list_out.append(flname)

        if not file_list_out:
            raise FileNotFoundError(file_list)
        config.l0_format = "dsb"
        config.l0_list = file_list_out
    else:
        raise TypeError("Input files not recognized as SPEXone level-0 data")

    return config


# --------------------------------------------------
# from pyspex.lib.l1a_def import init_l1a
# --------------------------------------------------
def attrs_sec_per_day(dset: Variable, ref_date: dt.datetime) -> None:
    """Add CF attributes to a dataset holding 'seconds of day'.

    Parameters
    ----------
    dset : Variable
       NetCDF4 variable containing a timestamp as seconds since reference date
    ref_date : dt.datetime
       Reference date

    Examples
    --------
    Update the attributes of variable 'time':

      ref_date = dt.datetime(2022, 3, 21, tzinfo=dt.timezone.utc)
      dset = sgrp.createVariable('image_time', 'f8', ('number_of_images',),
                                fill_value=-32767)
      dset.long_name = "image time"
      dset.description = "Integration start time in seconds of day."
      attrs_sec_per_day(dset, ref_date)

    In CDL the variable `time` will be defined as::

       double time(number_of_scans) ;
          time:_FillValue = -32767 ;
          time:long_name = "time" ;
          time:units = "seconds since 2022-03-21 00:00:00" ;
          time:description = "Earth view mid-time in seconds of day" ;
          time:year = 2022 ;
          time:month = 3 ;
          time:day = 21 ;
          time:valid_min = 0 ;
          time:valid_max = 86401 ;

    Note that '_FillValue', 'long_name' and 'description' are not set by
    this function.

    """
    dset.units = f"seconds since {ref_date.strftime('%Y-%m-%d %H:%M:%S')}"
    dset.year = f"{ref_date.year}"
    dset.month = f"{ref_date.month}"
    dset.day = f"{ref_date.day}"
    dset.valid_min = 0
    dset.valid_max = 86400 + ORBIT_DURATION


def image_attributes(rootgrp: Dataset, ref_date: dt.datetime) -> None:
    """Define group /image_attributes and its datasets."""
    sgrp = rootgrp.createGroup("/image_attributes")
    dset = sgrp.createVariable("icu_time_sec", "u4", ("number_of_images",))
    dset.long_name = "ICU time stamp (seconds)"
    dset.description = "Science TM parameter ICU_TIME_SEC."
    dset.valid_min = np.uint32(1956528000)  # year 2020
    dset.valid_max = np.uint32(2493072000)  # year 2037
    dset.units = "seconds since 1958-01-01 00:00:00 TAI"
    dset = sgrp.createVariable("icu_time_subsec", "u2", ("number_of_images",))
    dset.long_name = "ICU time stamp (sub-seconds)"
    dset.description = "Science TM parameter ICU_TIME_SUBSEC."
    dset.valid_min = np.uint16(0)
    dset.valid_max = np.uint16(0xFFFF)
    dset.units = "1/65536 s"

    dset = sgrp.createVariable(
        "image_time", "f8", ("number_of_images",), fill_value=-32767
    )
    dset.long_name = "image time"
    dset.description = "Integration start time in seconds of day."
    attrs_sec_per_day(dset, ref_date)
    dset = sgrp.createVariable("image_ID", "i4", ("number_of_images",))
    dset.long_name = "image counter from power-up"
    dset.valid_min = np.int32(0)
    dset.valid_max = np.int32(0x7FFFFFFF)
    dset = sgrp.createVariable("binning_table", "u1", ("number_of_images",))
    dset.long_name = "binning-table ID"
    dset.valid_min = np.uint8(0)
    dset.valid_max = np.uint8(0xFF)
    dset = sgrp.createVariable("digital_offset", "i2", ("number_of_images",))
    dset.long_name = "digital offset"
    dset.units = "1"
    dset = sgrp.createVariable(
        "nr_coadditions", "u2", ("number_of_images",), fill_value=0
    )
    dset.long_name = "number of coadditions"
    dset.valid_min = np.int32(1)
    dset.units = "1"
    dset = sgrp.createVariable(
        "exposure_time", "f8", ("number_of_images",), fill_value=0
    )
    dset.long_name = "exposure time"
    dset.units = "s"


def get_chunksizes(ydim: int, compression: bool) -> tuple[int, int]:
    """Obtain chunksizes for dataset: /science_data/science_data."""
    # I did some extensive testing.
    # - Without compression (chunked vs contiguous):
    #   * Writing to a contiguous dataset is faster (10-20%)
    #   * Reading one image is about as fast for chunked and contiguous storage
    #   * Reading a pixel image is much faster for chunked storage (2-8x)
    # - With compression (always chunked):
    #   * Writing takes 3x as long compared to without compression,
    #     but saves about > 40% on disk storage
    #   * Reading of compressed data is much slower than uncompressed data
    #   * The performance when reading one detector image is acceptable,
    #     however reading one pixel image is really slow (specially full-frame).
    # Therefore, these are the best choices for the variable `chunksizes`.
    return (
        (20, ydim)
        if ydim < 1048576
        else (1, min(512 * 1024, ydim))
        if compression
        else (1, ydim)
    )


def science_data(
    rootgrp: Dataset, compression: bool, chunksizes: tuple[int, int]
) -> None:
    """Define group /science_data and its datasets."""
    sgrp = rootgrp.createGroup("/science_data")
    dset = sgrp.createVariable(
        "detector_images",
        "u2",
        ("number_of_images", "samples_per_image"),
        compression="zlib" if compression else None,
        complevel=1,
        chunksizes=chunksizes,
        fill_value=0xFFFF,
    )
    dset.long_name = "detector pixel values"
    dset.valid_min = np.uint16(0)
    dset.valid_max = np.uint16(0xFFFE)
    dset.units = "counts"
    hk_dtype = rootgrp.createCompoundType(tmtc_dtype(0x350), "science_dtype")
    dset = sgrp.createVariable(
        "detector_telemetry", hk_dtype, dimensions=("number_of_images",)
    )
    dset.long_name = "SPEX science telemetry"
    dset.comment = "A subset of MPS and housekeeping parameters."


def engineering_data(rootgrp: Dataset, ref_date: dt.datetime) -> None:
    """Define group /engineering_data and its datasets."""
    sgrp = rootgrp.createGroup("/engineering_data")
    dset = sgrp.createVariable("HK_tlm_time", "f8", ("hk_packets",), fill_value=-32767)
    dset.long_name = "HK telemetry packet time"
    dset.description = "Packaging time in seconds of day."
    attrs_sec_per_day(dset, ref_date)
    hk_dtype = rootgrp.createCompoundType(tmtc_dtype(0x320), "nomhk_dtype")
    dset = sgrp.createVariable("NomHK_telemetry", hk_dtype, ("hk_packets",))
    dset.long_name = "SPEX nominal-HK telemetry"
    dset.comment = "An extended subset of the housekeeping parameters."
    dset = sgrp.createVariable("temp_detector", "f4", ("hk_packets",))
    dset.long_name = "detector temperature"
    dset.comment = "TS1 DEM Temperature (nominal)."
    dset.valid_min = 260
    dset.valid_max = 300
    dset.units = "K"
    dset = sgrp.createVariable("temp_housing", "f4", ("hk_packets",))
    dset.long_name = "housing temperature"
    dset.comment = "TS2 Housing Temperature (nominal)."
    dset.valid_min = 260
    dset.valid_max = 300
    dset.units = "K"
    dset = sgrp.createVariable("temp_radiator", "f4", ("hk_packets",))
    dset.long_name = "radiator temperature"
    dset.comment = "TS3 Radiator Temperature (nominal)."
    dset.valid_min = 260
    dset.valid_max = 300
    dset.units = "K"
    # hk_dtype = rootgrp.createCompoundType(tmtc_dtype(0x322)), 'demhk_dtype')
    # dset = sgrp.createVariable('DemHK_telemetry', hk_dtype, ('hk_packets',))
    # dset.long_name = "SPEX detector-HK telemetry"
    # dset.comment = "DEM housekeeping parameters."


# - main function ----------------------------------
def init_l1a(
    l1a_flname: str, ref_date: dt.datetime, dims: dict, compression: bool = False
) -> Dataset:
    """Create an empty SPEXone Level-1A product (on-ground or in-flight).

    Parameters
    ----------
    l1a_flname : str
       Name of L1A product
    ref_date :  dt.datetime
       Date of the first detector image (UTC)
    dims :  dict
       Provide length of the Level-1A dimensions. Default values::

          number_of_images : None     # number of image frames
          samples_per_image : None    # depends on binning table
          hk_packets : None           # number of HK tlm-packets (1 Hz)

    compression : bool, default=False
       Use compression on dataset /science_data/detector_images.

    Notes
    -----
    The optional groups '/gse_data' and '/navigation_data' are not created
    by this script.

    Original CDL definition is from F. S. Patt (GSFC), 08-Feb-2019

    """
    # check function parameters
    if not isinstance(dims, dict):
        raise TypeError("dims should be a dictionary")

    # initialize dimensions
    number_img = dims.get("number_of_images")
    img_samples = dims.get("samples_per_image")
    hk_packets = dims.get("hk_packets")

    # create/overwrite netCDF4 product
    try:
        rootgrp = Dataset(l1a_flname, "w")
    except Exception as exc:
        raise Exception(f"Failed to create netCDF4 file {l1a_flname}") from exc

    # - define global dimensions
    _ = rootgrp.createDimension("number_of_images", number_img)
    _ = rootgrp.createDimension("samples_per_image", img_samples)
    _ = rootgrp.createDimension("hk_packets", hk_packets)

    # - define the various HDF54/netCDF4 groups and their datasets
    image_attributes(rootgrp, ref_date)
    chunksizes = get_chunksizes(img_samples, compression)
    science_data(rootgrp, compression, chunksizes)
    engineering_data(rootgrp, ref_date)

    return rootgrp


# --------------------------------------------------
# from pyspex.lib.leap_sec import get_leap_seconds
#
# Remarks:
# - removed use of importlib.resources
# --------------------------------------------------
def get_leap_seconds(taitime: float, epochyear: int = 1958) -> float:
    """Return the number of elapsed leap seconds given a TAI time in seconds.

    The source for the latest version of tai-utc.dat is the
    US Naval Observatory: https://maia.usno.navy.mil/ser7/tai-utc.dat
    """
    # determine location of the file 'tai-utc.dat'
    if "OCVARROOT" not in environ:
        raise ValueError("Environment variable OCVARROOT not set")
    ocvar_dir = Path(environ["OCVARROOT"])
    if (ocvar_dir / "common").is_dir():
        ocvar_dir /= "common"

    epochsecs = dt.datetime(epochyear, 1, 1, tzinfo=dt.timezone.utc).timestamp()
    taidt = dt.datetime.fromtimestamp(taitime + epochsecs, dt.timezone.utc)
    leapsec: float = 0
    with (ocvar_dir / "tai-utc.dat").open("r", encoding="ascii") as fp:
        for line in fp:
            rec = line.rstrip().split(None, 7)
            if julian.from_jd(float(rec[4])).replace(tzinfo=dt.timezone.utc) < taidt:
                leapsec = float(rec[6])
    module_logger.debug("leap seconds: %f", leapsec)

    return leapsec


# --------------------------------------------------
# from pyspex.lib.logger import start_logger
#
# Remarks:
# - Logger config data is hard-coded
# --------------------------------------------------
def start_logger() -> None:
    """Initialize logger for pyspex."""
    config_data = {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "standard": {
                "format": (
                    "[%(asctime)s] {%(name)s:%(lineno)d} %(levelname)s" " - %(message)s"
                ),
                "datefmt": "%H:%M:%S",
            }
        },
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "level": "DEBUG",
                "formatter": "standard",
                "stream": "ext://sys.stdout",
            },
            "file": {
                "class": "logging.handlers.RotatingFileHandler",
                "level": "DEBUG",
                "formatter": "standard",
                "filename": "/tmp/warnings.log",
                "maxBytes": 10485760,
                "backupCount": 10,
                "encoding": "utf8",
            },
        },
        "root": {"level": "WARNING", "handlers": ["console", "file"]},
        "pyspex": {
            "level": "WARNING",
            "handlers": ["console", "file"],
            "propagate": True,
        },
        "pyspex.gen_l1a": {
            "level": "WARNING",
            "handlers": ["console", "file"],
            "propagate": True,
        },
    }

    dictConfig(config_data)


# --------------------------------------------------
# from pyspex.lib.tlm_utils import convert_hk
#
# Remarks:
# - removed all Enum classes and UNIT_DICTS (not needed)
# --------------------------------------------------
# - helper functions ------------------------
def exp_spex_det_t(raw_data: np.ndarray) -> np.ndarray:
    """Convert Detector Temperature Sensor to [K]."""
    res = np.empty(raw_data.size, dtype=float)
    mask = raw_data < 400
    res[mask] = 1.224 * raw_data[mask] - 17.05
    res[~mask] = 0.6426 * raw_data[~mask] - 145.57
    res[res > 325] = np.nan
    return res


def exp_spex_thermistor(raw_data: np.ndarray) -> np.ndarray:
    """Convert readouts of the Temperature Sensors to [K].

    Notes
    -----
    - TS1 DEM Nominal temperature
    - TS2 Housing Nominal Temperature
    - TS3 Radiator Nominal Temperature
    - TS4 DEM Redundant Temperature
    - TS5 Housing Redundant Temperature
    - TS6 Radiator Redundant Temperature*

    """
    coefficients = (294.34, 272589.0, 1.5173e-15, 5.73666e-19, 5.11328e-20)
    buff = ma.masked_array(raw_data / 256, mask=raw_data == 0)
    buff = (
        coefficients[0]
        + coefficients[1] / buff
        - coefficients[2] * buff**4
        + (coefficients[3] - coefficients[4] * ma.log(buff)) * buff**5
    )
    buff[raw_data == 0] = np.nan
    return buff.data


def poly_spex_icuhk_internaltemp(raw_data: np.ndarray) -> np.ndarray:
    """Convert readouts of temperature sensors on ICU power supplies to [K].

    Notes
    -----
    - ICU V5 supply temperature
    - ICU V4 supply temperature
    - ICU HtrG1 supply temperature
    - ICU HtrG2 supply temperature
    - ICU MidBoard temperature
    - ICU MCU-RAM temperature
    - ICU 1V2, 3V3 supply temperature

    """
    coefficients = (273.15, 0.0625)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_icuhk_internaltemp2(raw_data: np.ndarray) -> np.ndarray:
    """Convert readings of ICU bus voltages to Voltages.

    Notes
    -----
    - ICU 4.0 Volt bus voltage
    - ICU 3.3 Volt bus voltage
    - ICU 1.2 Volt bus voltage
    - DEM power supply

    """
    coefficients = (0, 0.001)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_htr_v(raw_data: np.ndarray) -> np.ndarray:
    """Convert Heater Bus voltages to Volt."""
    coefficients = (0, 0.01 / 3)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_htr1_p(raw_data: np.ndarray) -> np.ndarray:
    """Convert Heater1 Current to Watt."""
    resistance = 238.3
    return resistance * (raw_data / 1000) ** 2


def poly_spex_htr2_p(raw_data: np.ndarray) -> np.ndarray:
    """Convert Heater2 Current to Watt."""
    resistance = 212.5
    return resistance * (raw_data / 1000) ** 2


def poly_spex_htr3_p(raw_data: np.ndarray) -> np.ndarray:
    """Convert Heater3 Current to Watt."""
    resistance = 237.7
    return resistance * (raw_data / 1000) ** 2


def poly_spex_htr4_p(raw_data: np.ndarray) -> np.ndarray:
    """Convert Heater4 Current to Watt."""
    resistance = 213.2
    return resistance * (raw_data / 1000) ** 2


def poly_spex_dutycycle(raw_data: np.ndarray) -> np.ndarray:
    """Convert Heater Controller Duty Cycle output to percent."""
    coefficients = (0, 0.1)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_led_anode_v(raw_data: np.ndarray) -> np.ndarray:
    """Convert LED Anode voltage to Volt."""
    coefficients = (0, 0.000000623703003)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_led_cath_v(raw_data: np.ndarray) -> np.ndarray:
    """Convert LED Cathode voltage to Volt."""
    coefficients = (0, 0.000000415802001953)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_led_i(raw_data: np.ndarray) -> np.ndarray:
    """Convert LED current to milli-Ampere."""
    coefficients = (0, 0.0000030307446495961334745762711864407)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_adc_vcc(raw_data: np.ndarray) -> np.ndarray:
    """Convert ADC Analog VCC reading to Volt."""
    coefficients = (0, 0.00000127156575520833333)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_adc_gain(raw_data: np.ndarray) -> np.ndarray:
    """Convert ADC Analog VCC reading to Volt."""
    coefficients = (0, 0.000000127156575520833333)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_adc_t(raw_data: np.ndarray) -> np.ndarray:
    """Convert ADC1 Temperature reading to [K]."""
    coefficients = (-0.25, 0.0007385466842651367)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_adc_offset(raw_data: np.ndarray) -> np.ndarray:
    """Convert ADC offset (?) to Voltage."""
    coefficients = (0, 0.000415802001953)
    return coefficients[0] + coefficients[1] * raw_data


def poly_spex_dem_i(raw_data: np.ndarray) -> np.ndarray:
    """Convert DEM Supply current to milli-Ampere."""
    coefficients = (0, 0.2417)
    return coefficients[0] + coefficients[1] * raw_data


# - exported functions ----------------------
def convert_hk(key: str, raw_data: np.ndarray) -> np.ndarray:
    """Convert a DemHK or NomHK parameter to physical units."""
    conv_dict = {
        "DET_T": exp_spex_det_t,
        "TS1_DEM_N_T": exp_spex_thermistor,
        "TS2_HOUSING_N_T": exp_spex_thermistor,
        "TS3_RADIATOR_N_T": exp_spex_thermistor,
        "TS4_DEM_R_T": exp_spex_thermistor,
        "TS5_HOUSING_R_T": exp_spex_thermistor,
        "TS6_RADIATOR_R_T": exp_spex_thermistor,
        "ADC1_GAIN": poly_spex_adc_gain,
        "ADC2_GAIN": poly_spex_adc_gain,
        "ADC1_OFFSET": poly_spex_adc_offset,
        "ADC2_OFFSET": poly_spex_adc_offset,
        "ADC1_T": poly_spex_adc_t,
        "ADC2_T": poly_spex_adc_t,
        "ADC1_REF": poly_spex_adc_vcc,
        "ADC1_VCC": poly_spex_adc_vcc,
        "ADC2_REF": poly_spex_adc_vcc,
        "ADC2_VCC": poly_spex_adc_vcc,
        "DEM_I": poly_spex_dem_i,
        "HTR1_DUTYCYCL": poly_spex_dutycycle,
        "HTR2_DUTYCYCL": poly_spex_dutycycle,
        "HTR3_DUTYCYCL": poly_spex_dutycycle,
        "HTR4_DUTYCYCL": poly_spex_dutycycle,
        "HTR1_POWER": poly_spex_htr1_p,
        "HTR2_POWER": poly_spex_htr2_p,
        "HTR3_POWER": poly_spex_htr3_p,
        "HTR4_POWER": poly_spex_htr4_p,
        "HTRGRP1_V": poly_spex_htr_v,
        "HTRGRP2_V": poly_spex_htr_v,
        "ICU_4V_T": poly_spex_icuhk_internaltemp,
        "ICU_5V_T": poly_spex_icuhk_internaltemp,
        "ICU_DIGV_T": poly_spex_icuhk_internaltemp,
        "ICU_HG1_T": poly_spex_icuhk_internaltemp,
        "ICU_HG2_T": poly_spex_icuhk_internaltemp,
        "ICU_MCU_T": poly_spex_icuhk_internaltemp,
        "ICU_MID_T": poly_spex_icuhk_internaltemp,
        "DEM_V": poly_spex_icuhk_internaltemp2,
        "ICU_1P2V_V": poly_spex_icuhk_internaltemp2,
        "ICU_3P3V_V": poly_spex_icuhk_internaltemp2,
        "ICU_4P0V_V": poly_spex_icuhk_internaltemp2,
        "ICU_5P0V_V": poly_spex_icuhk_internaltemp2,
        "LED1_ANODE_V": poly_spex_led_anode_v,
        "LED2_ANODE_V": poly_spex_led_anode_v,
        "LED1_CATH_V": poly_spex_led_cath_v,
        "LED2_CATH_V": poly_spex_led_cath_v,
        "LED1_I": poly_spex_led_i,
        "LED2_I": poly_spex_led_i,
    }

    func = conv_dict.get(key)
    if func is not None:
        return func(raw_data)

    return raw_data


# --------------------------------------------------
# from pyspex.lib.tmtc_def import tmtc_dtype
# --------------------------------------------------
def __tmtc_def(apid: int) -> list:
    """Return a list of tuples with the definition os SPEXone telemetry packets.

    Parameters
    ----------
    apid : int
       SPEXone telemetry APID.
       Implemented APIDs: 0x350 (Science), 0x320 (NomHK) and 0x322 (DemHK).

    Returns
    -------
    list of tuples
       Definition of a numpy structured datatype.

    """
    if apid == 0x350:  # *** Science TM ***
        return [  # offs  start in packet
            ("ICUSWVER", ">u2"),  # 0     0x000c
            ("MPS_ID", "u1"),  # 2     0x000e
            ("MPS_VER", "u1"),  # 3     0x000f
            ("TS1_DEM_N_T", ">u4"),  # 4     0x0010
            ("TS2_HOUSING_N_T", ">u4"),  # 8     0x0014
            ("TS3_RADIATOR_N_T", ">u4"),  # 12    0x0018
            ("TS4_DEM_R_T", ">u4"),  # 16    0x001c
            ("TS5_HOUSING_R_T", ">u4"),  # 20    0x0020
            ("TS6_RADIATOR_R_T", ">u4"),  # 24    0x0024
            ("ICU_5V_T", ">i2"),  # 28    0x0028
            ("ICU_4V_T", ">i2"),  # 30    0x002a
            ("ICU_HG1_T", ">i2"),  # 32    0x002c
            ("ICU_HG2_T", ">i2"),  # 34    0x002e
            ("ICU_MID_T", ">i2"),  # 36    0x0030
            ("ICU_MCU_T", ">i2"),  # 38    0x0032
            ("ICU_DIGV_T", ">i2"),  # 40    0x0034
            ("ICU_4P0V_V", ">u2"),  # 42    0x0036
            ("ICU_3P3V_V", ">u2"),  # 44    0x0038
            ("ICU_1P2V_V", ">u2"),  # 46    0x003a
            ("ICU_4P0V_I", ">u2"),  # 48    0x003c
            ("ICU_3P3V_I", ">u2"),  # 50    0x003e
            ("ICU_1P2V_I", ">u2"),  # 52    0x0040
            ("ICU_5P0V_V", ">u2"),  # 54    0x0042
            ("ICU_5P0V_I", ">u2"),  # 56    0x0044
            ("DEM_V", ">u2"),  # 58    0x0046
            ("DEM_I", ">u2"),  # 60    0x0048
            ("LED1_ANODE_V", ">u4"),  # 62    0x004a
            ("LED1_CATH_V", ">u4"),  # 66    0x004e
            ("LED1_I", ">u4"),  # 70    0x0052
            ("LED2_ANODE_V", ">u4"),  # 74    0x0056
            ("LED2_CATH_V", ">u4"),  # 78    0x005a
            ("LED2_I", ">u4"),  # 82    0x005e
            ("ADC1_VCC", ">u4"),  # 86    0x0062
            ("ADC1_REF", ">u4"),  # 90    0x0066
            ("ADC1_T", ">u4"),  # 94    0x006a
            ("ADC2_VCC", ">u4"),  # 98    0x006e
            ("ADC2_REF", ">u4"),  # 102   0x0072
            ("ADC2_T", ">u4"),  # 106   0x0076
            ("REG_FW_VERSION", "u1"),  # 110   0x007a
            ("REG_NCOADDFRAMES", "u1"),  # 111   0x007b
            ("REG_IGEN_SELECT", "u1"),  # 112   0x007c
            ("REG_FULL_FRAME", "u1"),  # 113   0x007d
            ("REG_BINNING_TABLE_START", ">u4"),  # 114  0x007e
            ("REG_CMV_OUTPUTMODE", "u1"),  # 118   0x0082
            ("dummy_01", "u1"),  # 119   0x0083
            ("REG_COADD_BUF_START", ">u4"),  # 120   0x0084
            ("REG_COADD_RESA_START", ">u4"),  # 124   0x0088
            ("REG_COADD_RESB_START", ">u4"),  # 128   0x008c
            ("REG_FRAME_BUFA_START", ">u4"),  # 132   0x0090
            ("REG_FRAME_BUFB_START", ">u4"),  # 136   0x0094
            ("REG_LINE_ENABLE_START", ">u4"),  # 140   0x0098
            ("DET_REG000", "u1"),  # 144   0x009c
            ("dummy_02", "u1"),  # 145   0x009d
            ("DET_NUMLINES", ">u2"),  # 146   0x009e
            ("DET_START1", ">u2"),  # 148   0x00a0
            ("DET_START2", ">u2"),  # 150   0x00a2
            ("DET_START3", ">u2"),  # 152   0x00a4
            ("DET_START4", ">u2"),  # 154   0x00a6
            ("DET_START5", ">u2"),  # 156   0x00a8
            ("DET_START6", ">u2"),  # 158   0x00aa
            ("DET_START7", ">u2"),  # 160   0x00ac
            ("DET_START8", ">u2"),  # 152   0x00ae
            ("DET_NUMLINES1", ">u2"),  # 164   0x00b0
            ("DET_NUMLINES2", ">u2"),  # 166   0x00b2
            ("DET_NUMLINES3", ">u2"),  # 168   0x00b4
            ("DET_NUMLINES4", ">u2"),  # 170   0x00b6
            ("DET_NUMLINES5", ">u2"),  # 172   0x00b8
            ("DET_NUMLINES6", ">u2"),  # 174   0x00ba
            ("DET_NUMLINES7", ">u2"),  # 176   0x00bc
            ("DET_NUMLINES8", ">u2"),  # 178   0x00be
            ("DET_SUBS", ">u2"),  # 180   0x00c0
            ("DET_SUBA", ">u2"),  # 182   0x00c2
            ("DET_MONO", "u1"),  # 184   0x00c4
            ("DET_IMFLIP", "u1"),  # 185   0x00c5
            ("DET_EXPCNTR", "u1"),  # 186   0x00c6
            ("DET_ILVDS", "u1"),  # 187   0x00c7
            ("DET_EXPTIME", ">u4"),  # 188   0x00c8
            ("DET_EXPSTEP", ">u4"),  # 192   0x00cc
            ("DET_KP1", ">u4"),  # 196   0x00d0
            ("DET_KP2", ">u4"),  # 200   0x00d4
            ("DET_NOFSLOPES", "u1"),  # 204   0x00D8
            ("DET_EXPSEQ", "u1"),  # 205   0x00d9
            ("DET_EXPTIME2", ">u4"),  # 206   0x00da
            ("DET_EXPSTEP2", ">u4"),  # 210   0x00de
            ("DET_REG062", "u1"),  # 214   0x00e2
            ("DET_REG063", "u1"),  # 215   0x00e3
            ("DET_REG064", "u1"),  # 216   0x00e4
            ("DET_REG065", "u1"),  # 217   0x00e5
            ("DET_REG066", "u1"),  # 218   0x00e6
            ("DET_REG067", "u1"),  # 219   0x00e7
            ("DET_REG068", "u1"),  # 220   0x00e8
            ("DET_EXP2_SEQ", "u1"),  # 221   0x00e9
            ("DET_NOFFRAMES", ">u2"),  # 222   0x00ea
            ("DET_OUTMODE", "u1"),  # 224   0x00ec
            ("DET_FOTLEN", "u1"),  # 225   0x00ed
            ("DET_ILVDSRCVR", "u1"),  # 226   0x00ee
            ("DET_REG075", "u1"),  # 227   0x00ef
            ("DET_REG076", "u1"),  # 228   0x00f0
            ("DET_CALIB", "u1"),  # 229   0x00f1
            ("DET_TRAINPTRN", ">u2"),  # 230   0x00f2
            ("DET_CHENA", ">u4"),  # 232   0x00f4
            ("DET_ICOL", "u1"),  # 236   0x00F8
            ("DET_ICOLPR", "u1"),  # 237   0x00f9
            ("DET_IADC", "u1"),  # 238   0x00fa
            ("DET_IAMP", "u1"),  # 239   0x00fb
            ("DET_VTFL1", "u1"),  # 240   0x00fc
            ("DET_VTFL2", "u1"),  # 241   0x00fd
            ("DET_VTFL3", "u1"),  # 242   0x00fe
            ("DET_VRSTL", "u1"),  # 243   0x00ff
            ("DET_REG092", "u1"),  # 244   0x0100
            ("DET_REG093", "u1"),  # 245   0x0101
            ("DET_VPRECH", "u1"),  # 246   0x0102
            ("DET_VREF", "u1"),  # 247   0x0103
            ("DET_REG096", "u1"),  # 248   0x0104
            ("DET_REG097", "u1"),  # 249   0x0105
            ("DET_VRAMP1", "u1"),  # 250   0x0106
            ("DET_VRAMP2", "u1"),  # 251   0x0107
            ("DET_OFFSET", ">u2"),  # 252   0x0108
            ("DET_PGAGAIN", "u1"),  # 254   0x010a
            ("DET_ADCGAIN", "u1"),  # 255   0x010b
            ("DET_REG104", "u1"),  # 256   0x010c
            ("DET_REG105", "u1"),  # 257   0x010d
            ("DET_REG106", "u1"),  # 258   0x010e
            ("DET_REG107", "u1"),  # 259   0x010f
            ("DET_TDIG1", "u1"),  # 260   0x0110
            ("DET_TDIG2", "u1"),  # 261   0x0111
            ("DET_REG110", "u1"),  # 262   0x0112
            ("DET_BITMODE", "u1"),  # 263   0x0113
            ("DET_ADCRES", "u1"),  # 264   0x0114
            ("DET_PLLENA", "u1"),  # 265   0x0115
            ("DET_PLLINFRE", "u1"),  # 266   0x0116
            ("DET_PLLBYP", "u1"),  # 267   0x0117
            ("DET_PLLRATE", "u1"),  # 268   0x0118
            ("DET_PLLLOAD", "u1"),  # 269   0x0119
            ("DET_DETDUM", "u1"),  # 270   0x011a
            ("DET_REG119", "u1"),  # 271   0x011b
            ("DET_REG120", "u1"),  # 272   0x011c
            ("DET_BLACKCOL", "u1"),  # 273   0x011d
            ("DET_REG122", "u1"),  # 274   0x011e
            ("DET_VBLACKSUN", "u1"),  # 275   0x011f
            ("DET_REG124", "u1"),  # 276   0x0120
            ("DET_REG125", "u1"),  # 277   0x0121
            ("DET_T", ">u2"),  # 278   0x0122
            ("FTI", ">u2"),  # 280   0x0124  (100 musec)
            ("IMDMODE", "u1"),  # 282   0x0126
            ("dummy_03", "u1"),  # 283   0x0127
            ("IMRLEN", ">u4"),  # 284   0x0128
        ]  # 288

    if apid == 0x320:  # ***** NomHK *****
        return [  # offs  start in packet
            ("SEQCNT", ">u2"),  # 0     0x000c
            ("TCPKTID", ">u2"),  # 2     0x000e
            ("TCPKTSEQCTRL", ">u2"),  # 4     0x0010
            ("TCREJCODE", "u1"),  # 6     0x0012
            ("TCFAILCODE", "u1"),  # 7     0x0013
            ("TCREJPKTID", ">u2"),  # 8     0x0014
            ("TCFAILPKTID", ">u2"),  # 10    0x0016
            ("TCACCCNT", ">u2"),  # 12    0x0018
            ("TCREJCNT", ">u2"),  # 14    0x001a
            ("TCEXECCNT", ">u2"),  # 16    0x001c
            ("TCFAILCNT", ">u2"),  # 18    0x001e
            ("ICUSWVER", ">u2"),  # 20    0x0020
            ("SYSSTATE", ">u4"),  # 22    0x0022
            ("ICUMODE", "u1"),  # 26    0x0026
            ("EXTPPSSTAT", "u1"),  # 27    0x0027
            ("TIMEMSGSTAT", "u1"),  # 28    0x0028
            ("OBTSYNCSTAT", "u1"),  # 29    0x0029
            ("MPS_ID", "u1"),  # 30    0x002a
            ("MPS_VER", "u1"),  # 31    0x002b
            ("EVNTCNT_DEBUG", "u1"),  # 32    0x002c
            ("EVNTCNT_PROG", "u1"),  # 33    0x002d
            ("EVNTCNT_WARN", "u1"),  # 34    0x002e
            ("EVNTCNT_ERR", "u1"),  # 35    0x002f
            ("EVNTCNT_FATAL", "u1"),  # 36    0x0030
            ("BOOTSTATEPREV", "u1"),  # 37    0x0031
            ("BOOTCNTGOOD_IM0", ">u4"),  # 38    0x0032
            ("BOOTCNTGOOD_IM1", ">u4"),  # 42    0x0036
            ("BOOTCNTGOOD_IM2", ">u4"),  # 46    0x003a
            ("BOOTCNTGOOD_IM3", ">u4"),  # 50    0x003e
            ("BOOTATTEMPTS_CURRIM", "u1"),  # 54    0x0042
            ("dummy_01", "u1"),  # 55    0x0043
            ("SWIMG_LOADED", "u1"),  # 56    0x0044
            ("SWIMG_DEFAULT", "u1"),  # 57    0x0045
            ("SWIMG_NXTBOOT", "u1"),  # 58    0x0046
            ("WRITEPROT", "u1"),  # 59    0x0047
            ("BOOTCAUSE", "u1"),  # 60    0x0048
            ("TCVER_STAT", "u1"),  # 61    0x0049
            ("SPW_REG_A", ">u4"),  # 62    0x004a
            ("SPW_REG_B", ">u4"),  # 66    0x004e
            ("LAST_CRC", ">u4"),  # 70    0x0052
            ("SCITM_PKTINTVL", ">u2"),  # 74    0x0056
            ("SCITM_BUFFREE", ">u4"),  # 76    0x0058
            ("SWEXECTIMEWC", ">u8"),  # 80    0x005c
            ("ERRCNT_PLACEHOLDER_03", ">u2"),  # 88    0x0064
            # ('FillerByte', 'u1')              # 90    0x0066
            ("TS1_DEM_N_T", ">u4"),
            # ('FillerByte', 'u1')              # 94    0x006a
            ("TS2_HOUSING_N_T", ">u4"),
            # ('FillerByte', 'u1')              # 98    0x006e
            ("TS3_RADIATOR_N_T", ">u4"),
            # ('FillerByte', 'u1')              # 102   0x0072
            ("TS4_DEM_R_T", ">u4"),
            # ('FillerByte', 'u1')              # 106   0x0076
            ("TS5_HOUSING_R_T", ">u4"),
            # ('FillerByte', 'u1')              # 110   0x007a
            ("TS6_RADIATOR_R_T", ">u4"),
            ("ICU_5V_T", ">u2"),  # 114   0x007e
            ("ICU_4V_T", ">u2"),  # 116   0x0080
            ("ICU_HG1_T", ">u2"),  # 118   0x0082
            ("ICU_HG2_T", ">u2"),  # 120   0x0084
            ("ICU_MID_T", ">u2"),  # 122   0x0086
            ("ICU_MCU_T", ">u2"),  # 124   0x0088
            ("ICU_DIGV_T", ">u2"),  # 126   0x008a
            ("ICU_4P0V_V", ">u2"),  # 128   0x008c
            ("ICU_3P3V_V", ">u2"),  # 130   0x008e
            ("ICU_1P2V_V", ">u2"),  # 132   0x0090
            ("ICU_4P0V_I", ">u2"),  # 134   0x0092
            ("ICU_3P3V_I", ">u2"),  # 136   0x0094
            ("ICU_1P2V_I", ">u2"),  # 138   0x0096
            ("DEM_STATUS", "u1"),  # 140   0x0098
            ("dummy_02", "u1"),  # 141   0x0099
            ("ICU_5P0V_V", ">u2"),  # 142   0x009a
            ("ICU_5P0V_I", ">u2"),  # 144   0x009c
            ("DEMSPWSTAT", "u1"),  # 146   0x009e
            ("DEMRESETCNT", "u1"),  # 147   0x009f
            ("HTRGRP1_V", ">u2"),  # 148   0x00a0
            ("HTRGRP2_V", ">u2"),  # 150   0x00a2
            ("HTR1_I", ">u2"),  # 152   0x00a4
            ("HTR2_I", ">u2"),  # 154   0x00a6
            ("HTR3_I", ">u2"),  # 156   0x00a8
            ("HTR4_I", ">u2"),  # 158   0x00aa
            ("HTR1_CALCPVAL", ">f4"),  # 160   0x00ac
            ("HTR2_CALCPVAL", ">f4"),  # 164   0x00b0
            ("HTR3_CALCPVAL", ">f4"),  # 168   0x00b4
            ("HTR4_CALCPVAL", ">f4"),  # 172   0x00b8
            ("HTR1_CALCIVAL", ">f4"),  # 176   0x00bc
            ("HTR2_CALCIVAL", ">f4"),  # 180   0x00c0
            ("HTR3_CALCIVAL", ">f4"),  # 184   0x00c4
            ("HTR4_CALCIVAL", ">f4"),  # 188   0x00c8
            ("HTR1_DUTYCYCL", ">u2"),  # 192   0x00cc
            ("HTR2_DUTYCYCL", ">u2"),  # 194   0x00ce
            ("HTR3_DUTYCYCL", ">u2"),  # 196   0x00d0
            ("HTR4_DUTYCYCL", ">u2"),  # 198   0x00d2
            ("LED1_ENADIS", "u1"),  # 200   0x00d4
            ("LED2_ENADIS", "u1"),  # 201   0x00d5
            # ('FillerByte', 'u1')              # 202   0x00d6
            ("LED1_ANODE_V", ">u4"),
            # ('FillerByte', 'u1')              # 206   0x00da
            ("LED1_CATH_V", ">u4"),
            # ('FillerByte', 'u1')              # 210   0x00de
            ("LED1_I", ">u4"),
            # ('FillerByte', 'u1')              # 214   0x00e2
            ("LED2_ANODE_V", ">u4"),
            # ('FillerByte', 'u1')              # 218   0x00e6
            ("LED2_CATH_V", ">u4"),
            # ('FillerByte', 'u1')              # 222   0x00ea
            ("LED2_I", ">u4"),
            # ('FillerByte', 'u1')              # 226   0x00ee
            ("ADC1_VCC", ">u4"),
            # ('FillerByte', 'u1')              # 230   0x00f2
            ("ADC1_GAIN", ">u4"),
            # ('FillerByte', 'u1')              # 234   0x00f6
            ("ADC1_REF", ">u4"),
            # ('FillerByte', 'u1')              # 238   0x00fa
            ("ADC1_T", ">u4"),
            # ('FillerByte', 'u1')              # 242   0x00fe
            ("ADC1_OFFSET", ">u4"),
            # ('FillerByte', 'u1')              # 246   0x0102
            ("ADC2_VCC", ">u4"),
            # ('FillerByte', 'u1')              # 250   0x0106
            ("ADC2_GAIN", ">u4"),
            # ('FillerByte', 'u1')              # 254   0x010a
            ("ADC2_REF", ">u4"),
            # ('FillerByte', 'u1')              # 258   0x010e
            ("ADC2_T", ">u4"),
            # ('FillerByte', 'u1')              # 262   0x0112
            ("ADC2_OFFSET", ">u4"),
            ("DEM_V", ">u2"),  # 266   0x0116
            ("DEM_I", ">u2"),  # 268   0x0118
            ("REG_FW_VERSION", "u1"),  # 270   0x011a
            ("dummy_03", "u1"),  # 271   0x011b
            ("DET_T", ">u2"),  # 272   0x011c
            ("REG_SPW_ERROR", "u1"),  # 274   0x011e
            ("REG_CMV_OUTOFSYNC", "u1"),  # 275   0x011f
            ("REG_OCD_ACTUAL", "u1"),  # 276   0x0120
            ("REG_OCD_STICKY", "u1"),  # 277   0x0121
            ("REG_PWR_SENS", "u1"),  # 278   0x0122
            ("REG_FLASH_STATUS", "u1"),  # 279   0x0123
            ("REG_FLASH_EDAC_BLOCK", ">u2"),  # 280   0x0124
            ("SW_MAIN_LOOP_COUNT", ">u4"),  # 282   0x0126
        ]  # 286

    if apid == 0x322:  # ***** DemHK *****
        return [  # offs  start in packet
            ("REG_STATUS", "u1"),  # 0     0x000c
            ("REG_NCOADDFRAMES", "u1"),  # 1     0x000d
            ("REG_IGEN_SELECT", "u1"),  # 2     0x000e
            ("REG_FIFO_STATUS", "u1"),  # 3     0x000f
            ("REG_SPW_TURBO", "u1"),  # 4     0x0010
            ("REG_IGEN_MODE", "u1"),  # 5     0x0011
            ("REG_IGEN_VALUE", ">u2"),  # 6     0x0012
            ("REG_FULL_FRAME", "u1"),  # 8     0x0014
            ("dummy_01", "u1"),  # 9     0x0015
            ("REG_FLASH_ERASE", "<u4"),  # 10    0x0016 LE
            ("REG_BINNING_TABLE_START", ">u4"),  # 14   0x001a
            ("REG_CMV_OUTPUTMODE", "u1"),  # 18    0x001e
            ("REG_DETECT_ENABLE", "u1"),  # 19    0x001f
            ("REG_POWERUP_DELAY", "<u4"),  # 20    0x0020 LE
            # ('FillerByte', 'u1')
            ("REG_LU_THRESHOLD", "<u2"),  # 24    0x0024 LE
            ("REG_COADD_BUF_START", "<u4"),  # 26    0x0026 LE
            ("REG_COADD_RESA_START", "<u4"),  # 30    0x002a LE
            ("REG_COADD_RESB_START", "<u4"),  # 34    0x002e LE
            ("REG_FRAME_BUFA_START", "<u4"),  # 38    0x0032 LE
            ("REG_FRAME_BUFB_START", "<u4"),  # 42    0x0036 LE
            ("dummy_02", "u1"),  # 46    0x003a
            ("REG_FLASH_PAGE_SPR_BYTE", "u1"),  # 47    0x003b
            ("REG_LINE_ENABLE_START", "<u4"),  # 48    0x003c LE
            ("DET_REG000", "u1"),  # 52    0x0040
            ("dummy_03", "u1"),  # 53    0x0041
            ("DET_NUMLINES", ">u2"),  # 54    0x0042
            ("DET_START1", ">u2"),  # 56    0x0044
            ("DET_START2", ">u2"),  # 58    0x0046
            ("DET_START3", ">u2"),  # 60    0x0048
            ("DET_START4", ">u2"),  # 62    0x004a
            ("DET_START5", ">u2"),  # 64    0x004c
            ("DET_START6", ">u2"),  # 66    0x004e
            ("DET_START7", ">u2"),  # 68    0x0050
            ("DET_START8", ">u2"),  # 70    0x0052
            ("DET_NUMLINES1", ">u2"),  # 72    0x0054
            ("DET_NUMLINES2", ">u2"),  # 74    0x0056
            ("DET_NUMLINES3", ">u2"),  # 76    0x0058
            ("DET_NUMLINES4", ">u2"),  # 78    0x005a
            ("DET_NUMLINES5", ">u2"),  # 80    0x005c
            ("DET_NUMLINES6", ">u2"),  # 82    0x005e
            ("DET_NUMLINES7", ">u2"),  # 84    0x0060
            ("DET_NUMLINES8", ">u2"),  # 86    0x0062
            ("DET_SUBS", ">u2"),  # 88    0x0064
            ("DET_SUBA", ">u2"),  # 90    0x0066
            ("DET_MONO", "u1"),  # 92    0x0068
            ("DET_IMFLIP", "u1"),  # 93    0x0069
            ("DET_EXPCNTR", "u1"),  # 94    0x006a
            ("dummy_04", "u1"),  # 95    0x006b
            ("DET_EXPTIME", ">u4"),  # 96    0x006c
            # ('FillerByte', 'u1')
            ("DET_EXPSTEP", ">u4"),  # 100   0x0070
            # ('FillerByte', 'u1')
            ("DET_KP1", ">u4"),  # 104   0x0074
            # ('FillerByte', 'u1')
            ("DET_KP2", ">u4"),  # 108   0x0078
            # ('FillerByte', 'u1')
            ("DET_NOFSLOPES", "u1"),  # 112   0x007c
            ("DET_EXPSEQ", "u1"),  # 113   0x007d
            ("DET_EXPTIME2", ">u4"),  # 114   0x007e
            # ('FillerByte', 'u1')
            ("DET_EXPSTEP2", ">u4"),  # 118   0x0082
            # ('FillerByte', 'u1')
            ("DET_REG062", "u1"),  # 122   0x0086
            ("DET_REG063", "u1"),  # 123   0x0087
            ("DET_REG064", "u1"),  # 124   0x0088
            ("DET_REG065", "u1"),  # 125   0x0089
            ("DET_REG066", "u1"),  # 126   0x008a
            ("DET_REG067", "u1"),  # 127   0x008b
            ("DET_REG068", "u1"),  # 128   0x008c
            ("DET_EXP2_SEQ", "u1"),  # 129   0x008d
            ("DET_NOFFRAMES", ">u2"),  # 130   0x008e
            ("DET_OUTMODE", "u1"),  # 132   0x0090
            ("DET_FOTLEN", "u1"),  # 133   0x0091
            ("DET_ILVDSRCVR", "u1"),  # 134   0x0092
            ("DET_REG075", "u1"),  # 135   0x0093
            ("DET_REG076", "u1"),  # 136   0x0094
            ("DET_CALIB", "u1"),  # 137   0x0095
            ("DET_TRAINPTRN", ">u2"),  # 138   0x0096
            ("DET_CHENA", ">u4"),  # 140   0x0098
            # ('FillerByte', 'u1')
            ("DET_ILVDS", "u1"),  # 144   0x009c
            ("DET_ICOL", "u1"),  # 145   0x009d
            ("DET_ICOLPR", "u1"),  # 146   0x009e
            ("DET_IADC", "u1"),  # 147   0x009f
            ("DET_IAMP", "u1"),  # 148   0x00a0
            ("DET_VTFL1", "u1"),  # 149   0x00a1
            ("DET_VTFL2", "u1"),  # 150   0x00a2
            ("DET_VTFL3", "u1"),  # 151   0x00a3
            ("DET_VRSTL", "u1"),  # 152   0x00a4
            ("DET_REG092", "u1"),  # 153   0x00a5
            ("DET_REG093", "u1"),  # 154   0x00a6
            ("DET_VPRECH", "u1"),  # 155   0x00a7
            ("DET_VREF", "u1"),  # 156   0x00a8
            ("DET_REG096", "u1"),  # 157   0x00a9
            ("DET_REG097", "u1"),  # 158   0x00aa
            ("DET_VRAMP1", "u1"),  # 159   0x00ab
            ("DET_VRAMP2", "u1"),  # 160   0x00ac
            ("dummy_05", "u1"),  # 161   0x00ad
            ("DET_OFFSET", ">u2"),  # 162   0x00ae
            ("DET_PGAGAIN", "u1"),  # 164   0x00b0
            ("DET_ADCGAIN", "u1"),  # 165   0x00b1
            ("DET_REG104", "u1"),  # 166   0x00b2
            ("DET_REG105", "u1"),  # 167   0x00b3
            ("DET_REG106", "u1"),  # 168   0x00b4
            ("DET_REG107", "u1"),  # 169   0x00b5
            ("DET_TDIG1", "u1"),  # 170   0x00b6
            ("DET_TDIG2", "u1"),  # 171   0x00b7
            ("DET_REG110", "u1"),  # 172   0x00b8
            ("DET_BITMODE", "u1"),  # 173   0x00b9
            ("DET_ADCRES", "u1"),  # 174   0x00ba
            ("DET_PLLENA", "u1"),  # 175   0x00bb
            ("DET_PLLINFRE", "u1"),  # 176   0x00bc
            ("DET_PLLBYP", "u1"),  # 177   0x00bd
            ("DET_PLLRATE", "u1"),  # 178   0x00be
            ("DET_PLLLoad", "u1"),  # 179   0x00bf
            ("DET_DETDum", "u1"),  # 180   0x00c0
            ("DET_REG119", "u1"),  # 181   0x00c1
            ("DET_REG120", "u1"),  # 182   0x00c2
            ("DET_BLACKCOL", "u1"),  # 183   0x00c3
            ("DET_REG122", "u1"),  # 184   0x00c4
            ("DET_VBLACKSUN", "u1"),  # 185   0x00c5
            ("DET_REG124", "u1"),  # 186   0x00c6
            ("DET_REG125", "u1"),  # 187   0x00c7
        ]  # 188

    raise ValueError("Telemetry APID not implemented")


def tmtc_dtype(apid: int) -> np.dtype:
    """Obtain SPEXone telemetry packet definition.

    Parameters
    ----------
    apid : int
       SPEXone telemetry APID.
       Implemented APIDs: 0x350 (Science), 0x320 (NomHK) and 0x322 (DemHK).

    Returns
    -------
    numpy.dtype
       Definition of SPEXone telemetry packet.

    Examples
    --------
    Usage of `tmtc_dtype`::

    >> from pyspex.lib.tmtc_def import tmtc_dtype
    >> mps_dtype = tmtc_dtype(0x350)

    """
    return np.dtype(__tmtc_def(apid))


# --------------------------------------------------
# from pyspex.lv0_lib import CorruptPacketWarning
# --------------------------------------------------
class CorruptPacketWarning(UserWarning):
    """Creating a custom warning."""


# --------------------------------------------------
# from .lv0_lib import dump_hkt, dump_science, read_lv0_data
# --------------------------------------------------
def dump_hkt(flname: str, ccsds_hk: tuple[np.ndarray, ...]) -> None:
    """Dump header info of the SPEXone housekeeping telemetry packets."""

    def msg_320(val: np.ndarray) -> str:
        return f" {val['ICUSWVER']:8x} {val['MPS_ID']:6d}"

    def msg_331(val: np.ndarray) -> str:
        return f" {-1:8x} {-1:6d} {val['TcSeqControl'][0]:12d}"

    def msg_332(val: np.ndarray) -> str:
        return (
            f" {-1:8x} {-1:6d} {val['TcSeqControl'][0]:12d}"
            f" {bin(val['TcRejectCode'][0])}"
            f" 0x{val['RejectParameter1'][0]:x}"
            f" 0x{val['RejectParameter2'][0]:x}"
        )

    def msg_333(val: np.ndarray) -> str:
        return f" {-1:8x} {-1:6d} {val['TcSeqControl'][0]:12d}"

    def msg_334(val: np.ndarray) -> str:
        return (
            f" {-1:8x} {-1:6d} {val['TcSeqControl'][0]:12d}"
            f" {bin(val['TcFailCode'][0])}"
            f" 0x{val['FailParameter1'][0]:x}"
            f" 0x{val['FailParameter2'][0]:x}"
        )

    def msg_335(val: np.ndarray) -> str:
        return (
            f" {-1:8x} {-1:6d} {bin(val['Event_ID'][0])}"
            f" {bin(val['Event_Sev'][0])}"
            f" 0x{val['Word1'][0]:x}"
            f" 0x{val['Word2'][0]:x}"
            f" 0x{val['Word3'][0]:x}"
            f" 0x{val['Word4'][0]:x}"
        )

    with Path(flname).open("w", encoding="ascii") as fp:
        fp.write(
            "APID Grouping Counter Length     TAI_SEC    SUB_SEC"
            " ICUSWVER MPS_ID TcSeqControl TcErrorCode\n"
        )
        for buf in ccsds_hk:
            ccsds_hdr = CCSDShdr(buf["hdr"][0])
            msg = (
                f"{ccsds_hdr.apid:4x} {ccsds_hdr.grouping_flag:8d}"
                f" {ccsds_hdr.sequence:7d} {ccsds_hdr.packet_size:6d}"
                f" {ccsds_hdr.tai_sec:11d} {ccsds_hdr.sub_sec:10d}"
            )

            if ccsds_hdr.apid == 0x320:
                msg_320(buf["hk"][0])
            else:
                method = {
                    0x331: msg_331,
                    0x332: msg_332,
                    0x333: msg_333,
                    0x334: msg_334,
                    0x335: msg_335,
                }.get(ccsds_hdr.apid, None)
                msg += "" if method is None else method(buf)
            fp.write(msg + "\n")


def dump_science(flname: str, ccsds_sci: tuple[np.ndarray, ...]) -> None:
    """Dump telemetry header info (Science)."""
    with Path(flname).open("w", encoding="ascii") as fp:
        fp.write(
            "APID Grouping Counter Length"
            " ICUSWVER MPS_ID  IMRLEN     ICU_SEC ICU_SUBSEC\n"
        )
        for segment in ccsds_sci:
            ccsds_hdr = CCSDShdr(segment["hdr"][0])
            if ccsds_hdr.grouping_flag == 1:
                nom_hk = segment["hk"]
                icu_tm = segment["icu_tm"]
                fp.write(
                    f"{ccsds_hdr.apid:4x}"
                    f" {ccsds_hdr.grouping_flag:8d}"
                    f" {ccsds_hdr.sequence:7d}"
                    f" {ccsds_hdr.packet_size:6d}"
                    f" {nom_hk['ICUSWVER'][0]:8x}"
                    f" {nom_hk['MPS_ID'][0]:6d}"
                    f" {nom_hk['IMRLEN'][0]:7d}"
                    f" {icu_tm['tai_sec'][0]:11d}"
                    f" {icu_tm['sub_sec'][0]:10d}\n"
                )
            else:
                fp.write(
                    f"{ccsds_hdr.apid:4x}"
                    f" {ccsds_hdr.grouping_flag:8d}"
                    f" {ccsds_hdr.sequence:7d}"
                    f" {ccsds_hdr.packet_size:6d}\n"
                )


def _cfe_header_(flname: Path) -> np.ndarray:
    """Read cFE file header (only for file_format='dsb')."""
    # define numpy data-type to read the cFE file-header
    dtype_cfe = np.dtype(
        [
            ("ContentType", "S4"),
            ("SubType", "S4"),
            ("FileHeaderLength", ">u4"),
            ("SpacecraftID", "S4"),
            ("ProcessorID", ">u4"),
            ("InstrumentID", "S4"),
            ("TimeSec", ">u4"),
            ("TimeSubSec", ">u4"),
            ("Filename", "S32"),
        ]
    )

    cfe_hdr = np.fromfile(flname, count=1, dtype=dtype_cfe)[0]
    module_logger.debug('content of cFE header "%s"', cfe_hdr)
    return cfe_hdr


def _fix_hk24_(sci_hk: np.ndarray) -> np.ndarray:
    """Correct 32-bit values in the Science HK.

    Which originate from 24-bit values in the detector register parameters.

    In addition::

    - copy the first 4 bytes of 'DET_CHENA' to 'DET_ILVDS'
    - parameter 'REG_BINNING_TABLE_START' was writen in little-endian

    """
    res = sci_hk.copy()
    if sci_hk["ICUSWVER"] < 0x129:
        res["REG_BINNING_TABLE_START"] = sci_hk["REG_BINNING_TABLE_START"].byteswap()

    res["DET_ILVDS"] = sci_hk["DET_CHENA"] & 0xF
    for key in [
        "TS1_DEM_N_T",
        "TS2_HOUSING_N_T",
        "TS3_RADIATOR_N_T",
        "TS4_DEM_R_T",
        "TS5_HOUSING_R_T",
        "TS6_RADIATOR_R_T",
        "LED1_ANODE_V",
        "LED1_CATH_V",
        "LED1_I",
        "LED2_ANODE_V",
        "LED2_CATH_V",
        "LED2_I",
        "ADC1_VCC",
        "ADC1_REF",
        "ADC1_T",
        "ADC2_VCC",
        "ADC2_REF",
        "ADC2_T",
        "DET_EXPTIME",
        "DET_EXPSTEP",
        "DET_KP1",
        "DET_KP2",
        "DET_EXPTIME2",
        "DET_EXPSTEP2",
        "DET_CHENA",
    ]:
        res[key] = sci_hk[key] >> 8

    return res


def read_lv0_data(
    file_list: list[Path], file_format: str, *, debug: bool = False
) -> tuple[tuple, tuple]:
    """Read level 0 data and return Science and telemetry data.

    Parameters
    ----------
    file_list : list of Path
       list of CCSDS files
    file_format : {'raw', 'st3', 'dsb'}
       type of CCSDS data
    debug : bool, default=False
       run in debug mode

    Returns
    -------
    tuple
         Contains all Science and TmTc CCSDS packages as numpy arrays,
         or None if called with debug is True

    """
    scihk_dtype = tmtc_dtype(0x350)
    icutm_dtype = np.dtype([("tai_sec", ">u4"), ("sub_sec", ">u2")])

    # read level 0 headers and CCSDS data of Science and TmTc data
    ccsds_sci = ()
    ccsds_hk = ()
    for flname in file_list:
        offs = 0
        if file_format == "dsb":
            cfe_hdr = _cfe_header_(flname)
            offs += cfe_hdr["FileHeaderLength"]

        buff_sci = ()  # Use chunking to speed-up memory allocation
        buff_hk = ()
        with open(flname, "rb") as fp:
            module_logger.debug('processing file "%s"', flname)

            # read CCSDS header and user data
            ccsds_data = fp.read()
            while offs < len(ccsds_data):
                try:
                    ccsds_hdr = CCSDShdr()
                    ccsds_hdr.read(file_format, ccsds_data, offs)
                    hdr_dtype = ccsds_hdr.dtype
                except ValueError as exc:
                    module_logger.warning('header read error with "%s".', exc)
                    break

                # check for data corruption (length > 0 and odd)
                if ccsds_hdr.apid != 0x340 and ccsds_hdr.packet_size % 2 == 0:
                    msg = (
                        "corrupted CCSDS packet detected:"
                        f" APID: {ccsds_hdr.apid}"
                        f", grouping_flag: {ccsds_hdr.grouping_flag}"
                        f", itemsize: {hdr_dtype.itemsize}"
                        f", packet_length: {ccsds_hdr.packet_size}"
                        f", file position: {offs}"
                    )
                    warnings.warn(msg, category=CorruptPacketWarning, stacklevel=1)
                    break

                if debug:
                    print(
                        ccsds_hdr.apid,
                        ccsds_hdr.grouping_flag,
                        hdr_dtype.itemsize,
                        ccsds_hdr.packet_size,
                        offs,
                    )
                    if not 0x320 <= ccsds_hdr.apid <= 0x350:
                        break

                    offs += hdr_dtype.itemsize + ccsds_hdr.packet_size - 5
                    continue

                # copy the full CCSDS package
                if ccsds_hdr.apid == 0x350:  # Science APID
                    nbytes = ccsds_hdr.packet_size - 5
                    if ccsds_hdr.grouping_flag == 1:
                        buff = np.empty(
                            1,
                            dtype=np.dtype(
                                [
                                    ("hdr", hdr_dtype),
                                    ("hk", scihk_dtype),
                                    ("icu_tm", icutm_dtype),
                                    ("frame", "O"),
                                ]
                            ),
                        )
                        buff["hdr"] = ccsds_hdr.hdr
                        offs += hdr_dtype.itemsize
                        buff["hk"] = _fix_hk24_(
                            np.frombuffer(
                                ccsds_data, count=1, offset=offs, dtype=scihk_dtype
                            )[0]
                        )
                        offs += scihk_dtype.itemsize
                        buff["icu_tm"] = np.frombuffer(
                            ccsds_data, count=1, offset=offs, dtype=icutm_dtype
                        )[0]
                        offs += icutm_dtype.itemsize
                        nbytes -= scihk_dtype.itemsize + icutm_dtype.itemsize
                    else:
                        buff = np.empty(
                            1, dtype=np.dtype([("hdr", hdr_dtype), ("frame", "O")])
                        )
                        buff["hdr"] = ccsds_hdr.hdr
                        offs += hdr_dtype.itemsize

                    buff["frame"][0] = np.frombuffer(
                        ccsds_data, count=nbytes // 2, offset=offs, dtype=">u2"
                    )
                    buff_sci += (buff.copy(),)
                    offs += nbytes
                elif 0x320 <= ccsds_hdr.apid < 0x350:  # other valid APIDs
                    dtype_tmtc = ccsds_hdr.data_dtype
                    buff = np.frombuffer(
                        ccsds_data, count=1, offset=offs, dtype=dtype_tmtc
                    )
                    buff_hk += (buff,)
                    offs += dtype_tmtc.itemsize
                else:
                    offs += hdr_dtype.itemsize + ccsds_hdr.packet_size - 5
            del ccsds_data

        ccsds_sci += buff_sci
        ccsds_hk += buff_hk

    module_logger.debug("number of Science packages %d", len(ccsds_sci))
    module_logger.debug("number of Engineering packages %d", len(ccsds_hk))

    return ccsds_sci, ccsds_hk


# --------------------------------------------------
# from .l1a_io import L1Aio
#
# Remarks:
# - removed local functions:
#   * _binning_table_()
#   * _digital_offset_()
#   * _exposure_time_()
#   * _nr_coadditions_()
# - removed obsoleted L1Aio methods:
#   * fill_science()
#   * fill_nomhk()
# --------------------------------------------------
class L1Aio:
    """Class to create SPEXone level-1A products.

    Parameters
    ----------
    product :  str
       Name of the SPEXone level-1A product
    ref_date :  dt.datetime
       Date of the first detector image (UTC)
    dims :  dict
       Dimensions of the datasets, default values::

          number_of_images : None     # number of image frames
          samples_per_image : 184000  # depends on binning table
          hk_packets : None           # number of HK tlm-packets

    compression : bool, default=False
       Use compression on dataset /science_data/detector_images

    """

    dset_stored = {
        "/science_data/detector_images": 0,
        "/science_data/detector_telemetry": 0,
        "/image_attributes/binning_table": 0,
        "/image_attributes/digital_offset": 0,
        "/image_attributes/nr_coadditions": 0,
        "/image_attributes/exposure_time": 0,
        "/image_attributes/icu_time_sec": 0,
        "/image_attributes/icu_time_subsec": 0,
        "/image_attributes/image_time": 0,
        "/image_attributes/image_ID": 0,
        "/engineering_data/NomHK_telemetry": 0,
        # '/engineering_data/DemHK_telemetry': 0,
        "/engineering_data/temp_detector": 0,
        "/engineering_data/temp_housing": 0,
        "/engineering_data/temp_radiator": 0,
        "/engineering_data/HK_tlm_time": 0,
    }

    def __init__(
        self: L1Aio,
        product: Path | str,
        ref_date: dt.datetime,
        dims: dict,
        compression: bool = False,
    ) -> None:
        """Initialize access to a SPEXone level-1A product."""
        self.product: Path = Path(product) if isinstance(product, str) else product

        # initialize private class-attributes
        self.__epoch = ref_date

        # initialize level-1A product
        self.fid = init_l1a(product, ref_date, dims, compression)
        for key in self.dset_stored:
            self.dset_stored[key] = 0

    def __iter__(self: L1Aio) -> None:
        """Allow iteration."""
        for attr in sorted(self.__dict__):
            if not attr.startswith("__"):
                yield attr

    def __enter__(self: L1Aio) -> L1Aio:
        """Initiate the context manager."""
        return self

    def __exit__(self: L1Aio, *args: str) -> bool:
        """Exit the context manager."""
        self.close()
        return False  # any exception is raised by the with statement.

    def close(self: L1Aio) -> None:
        """Close product and check if required datasets are filled with data."""
        if self.fid is None:
            return

        # check if at least one dataset is updated
        if self.fid.dimensions["number_of_images"].size == 0:
            self.fid.close()
            self.fid = None
            return

        # check of all required dataset their sizes
        self.check_stored(allow_empty=True)
        self.fid.close()
        self.fid = None

    # ---------- PUBLIC FUNCTIONS ----------
    @property
    def epoch(self: L1Aio) -> dt.datetime:
        """Provide epoch for SPEXone."""
        return self.__epoch

    def get_dim(self: L1Aio, name: str) -> int:
        """Get size of a netCDF4 dimension."""
        return self.fid.dimensions[name].size

    # ----- ATTRIBUTES --------------------
    def get_attr(self: L1Aio, name: str, ds_name: str | None = None) -> str | None:
        """Read data of an attribute.

        Global or attached to a group or variable.

        Parameters
        ----------
        name : str
           name of the attribute
        ds_name : str, default=None
           name of dataset to which the attribute is attached

        Returns
        -------
        scalar or array_like
           value of attribute 'name', global or attached to dataset 'ds_name'

        """
        if ds_name is None:
            res = self.fid.getncattr(name)
        else:
            if ds_name not in self.fid.groups and ds_name not in self.fid.variables:
                return None
            res = self.fid[ds_name].getncattr(name)

        if isinstance(res, bytes):
            return res.decode("ascii")

        return res

    def set_attr(
        self: L1Aio,
        name: str,
        value: Any,  # noqa: ANN401
        ds_name: str | None = None,
    ) -> None:
        """Write data to an attribute.

        Global or attached to a group or variable.

        Parameters
        ----------
        name : string
           name of the attribute
        value : scalar, array_like
           value or values to be written
        ds_name : str, default=None
           name of group or dataset to which the attribute is attached
           **Use group name without starting '/'**

        """
        if ds_name is None:
            if isinstance(value, str):
                self.fid.setncattr(name, np.bytes_(value))
            else:
                self.fid.setncattr(name, value)
        else:
            grp_name = str(PurePosixPath(ds_name).parent)
            var_name = str(PurePosixPath(ds_name).name)
            if grp_name != ".":
                if (
                    var_name not in self.fid[grp_name].groups
                    and var_name not in self.fid[grp_name].variables
                ):
                    raise KeyError(f"ds_name {ds_name} not in product")
            else:
                if (
                    var_name not in self.fid.groups
                    and var_name not in self.fid.variables
                ):
                    raise KeyError(f"ds_name {ds_name} not in product")

            if isinstance(value, str):
                self.fid[ds_name].setncattr(name, np.bytes_(value))
            else:
                self.fid[ds_name].setncattr(name, value)

    # ----- VARIABLES --------------------
    def get_dset(self: L1Aio, name: str) -> None:
        """Read data of a netCDF4 variable.

        Parameters
        ----------
        name : str
           name of dataset

        Returns
        -------
        scalar or array_like
           value of dataset 'name'

        """
        grp_name = str(PurePosixPath(name).parent)
        var_name = str(PurePosixPath(name).name)
        if grp_name != ".":
            if var_name not in self.fid[grp_name].variables:
                raise KeyError(f"dataset {name} not in level-1A product")
        else:
            if var_name not in self.fid.variables:
                raise KeyError(f"dataset {name} not in level-1A product")

        return self.fid[name][:]

    def set_dset(self: L1Aio, name: str, value: Any) -> None:  # noqa: ANN401
        """Write data to a netCDF4 variable.

        Parameters
        ----------
        name : str
           Name of level-1A dataset
        value : scalar or array_like
           Value or values to be written

        """
        value = np.asarray(value)
        grp_name = str(PurePosixPath(name).parent)
        var_name = str(PurePosixPath(name).name)
        if grp_name != ".":
            if var_name not in self.fid[grp_name].variables:
                raise KeyError(f"dataset {name} not in level-1A product")
        else:
            if var_name not in self.fid.variables:
                raise KeyError(f"dataset {name} not in level-1A product")

        self.fid[name][...] = value
        self.dset_stored[name] += 1 if value.shape == () else value.shape[0]

    # -------------------------
    def fill_global_attrs(self: L1Aio, inflight: bool = False) -> None:
        """Define global attributes in the SPEXone level-1A products.

        Parameters
        ----------
        inflight :  bool, default=False
           Measurements performed on-ground or inflight

        """
        dict_attrs = attrs_def(inflight)
        dict_attrs["product_name"] = self.product.name
        for key, value in dict_attrs.items():
            if value is not None:
                self.fid.setncattr(key, value)

    # - L1A specific functions ------------------------
    def check_stored(self: L1Aio, allow_empty: bool = False) -> None:
        """Check variables with the same first dimension have equal sizes.

        Parameters
        ----------
        allow_empty :  bool, default=False
            allow variables to be present, but empty

        """
        warn_str = (
            "SPEX level-1A format check [WARNING]:"
            ' size of variable "{:s}" is wrong, only {:d} elements'
        )

        # check image datasets
        dim_sz = self.get_dim("number_of_images")
        res = []
        key_list = [
            x
            for x in self.dset_stored
            if (x.startswith("/science_data") or x.startswith("/image_attributes"))
        ]
        for key in key_list:
            res.append(self.dset_stored[key])
        res = np.array(res)
        if allow_empty:
            indx = ((res > 0) & (res != dim_sz)).nonzero()[0]
        else:
            indx = np.nonzero(res != dim_sz)[0]
        for ii in indx:
            print(warn_str.format(key_list[ii], res[ii]))

        # check house-keeping datasets
        dim_sz = self.get_dim("hk_packets")
        key_list = [x for x in self.dset_stored if x.startswith("/engineering_data")]
        res = []
        for key in key_list:
            res.append(self.dset_stored[key])
        res = np.array(res)
        if allow_empty:
            indx = np.nonzero((res > 0) & (res != dim_sz))[0]
        else:
            indx = np.nonzero(res != dim_sz)[0]
        for ii in indx:
            print(warn_str.format(key_list[ii], res[ii]))


# --------------------------------------------------
# from .hkt_io import CoverageFlag, HKTio
# --------------------------------------------------
class CoverageFlag(IntFlag):
    """Define flags for coverage_quality (navigation_data)."""

    MISSING_SAMPLES = auto()
    TOO_SHORT_EXTENDS = auto()
    NO_EXTEND_AT_START = auto()
    NO_EXTEND_AT_END = auto()

    @classmethod
    def check(
        cls: CoverageFlag,
        nav_data: xr.Dataset,
        coverage: list[dt.datetime, dt.datetime],
    ) -> int:
        """Check coverage time of navigation data."""
        coverage_quality = cls(0)
        l1a_time_range = (
            np.datetime64(coverage[0].replace(tzinfo=None)),
            np.datetime64(coverage[1].replace(tzinfo=None)),
        )
        nav_time_range = (
            nav_data["att_time"].values[0],
            nav_data["att_time"].values[-1],
        )

        # check at the start of the data
        if l1a_time_range[0] < nav_time_range[0]:
            coverage_quality |= CoverageFlag.NO_EXTEND_AT_START
            module_logger.error(
                "time coverage of navigation data starts after L1A science data"
            )

        diff_coverage = l1a_time_range[0] - nav_time_range[0]
        if diff_coverage <= SEVEN_SECONDS:
            coverage_quality |= CoverageFlag.TOO_SHORT_EXTENDS
            module_logger.warning(
                "extends of time coverage of navigation data too short",
            )

        # check at the end of the data
        if l1a_time_range[1] > nav_time_range[1]:
            coverage_quality |= CoverageFlag.NO_EXTEND_AT_END
            module_logger.error(
                "time coverage of navigation data ends before L1A science data"
            )

        diff_coverage = nav_time_range[1] - l1a_time_range[1]
        if diff_coverage <= SEVEN_SECONDS:
            coverage_quality |= CoverageFlag.TOO_SHORT_EXTENDS
            module_logger.warning(
                "extends of time coverage of navigation data too short",
            )
        return coverage_quality


class HKTio:
    """Class to read housekeeping and navigation data from PACE-HKT products.

    Parameters
    ----------
    flnames :  str | Path | list[Path]
        sorted list of PACE_HKT filenames (netCDF4 format)

    Notes
    -----
    This class has the following methods::

     - housekeeping(instrument: str) -> tuple[np.ndarray]
     - navigation() -> dict

    """

    def __init__(self: HKTio, flnames: str | Path | list[Path]) -> None:
        """Initialize access to PACE HKT products."""
        self.nav_data: xr.Dataset | None = None
        if isinstance(flnames, str):
            self.flnames = [Path(flnames)]
        elif isinstance(flnames, Path):
            self.flnames = [flnames]
        else:
            self.flnames = flnames

        for hkt_fl in self.flnames:
            if not hkt_fl.is_file():
                raise FileNotFoundError(f"file {hkt_fl} not found on system")

    @staticmethod
    def __read_hkt__(hkt_file: Path, instrument: str) -> np.ndarray | None:
        """Return housekeeping data of a given instrument."""
        with h5py.File(hkt_file) as fid:
            gid = fid["housekeeping_data"]
            if instrument == "spx":
                ds_name = (
                    "SPEX_HKT_packets"
                    if "SPEX_HKT_packets" in gid
                    else "SPEXone_HKT_packets"
                )
            elif instrument == "sc":
                ds_name = "SC_HKT_packets"
            elif instrument == "oci":
                ds_name = "OCI_HKT_packets"
            elif instrument == "harp":
                ds_name = (
                    "HARP_HKT_packets"
                    if "HARP_HKT_packets" in gid
                    else "HARP2_HKT_packets"
                )
            else:
                raise KeyError("data of unknown instrument requested")

            res = gid[ds_name][:] if ds_name in gid else None

        return res

    # ---------- PUBLIC FUNCTIONS ----------
    def housekeeping(self: HKTio, instrument: str = "spx") -> tuple[np.ndarray]:
        """Get housekeeping telemetry data.

        Parameters
        ----------
        instrument : {'spx', 'oci', 'harp', 'sc'}, default='spx'
           name of PACE instrument: 'harp': HARP2, 'oci': OCI,
           'sc': spacecraft, 'spx': SPEXone.

        Notes
        -----
        Current implementation only works for SPEXone.

        """
        res = None
        for hkt_file in self.flnames:
            buff = self.__read_hkt__(hkt_file, instrument)
            if buff is not None:
                res = buff if res is None else np.concatenate((res, buff), axis=0)
        if res is None:
            return ()

        ccsds_hk = ()
        for packet in res:
            try:
                ccsds_hdr = CCSDShdr()
                ccsds_hdr.read("raw", packet)
            except ValueError as exc:
                module_logger.warning('CCSDS header read error with "%s"', exc)
                break

            try:
                dtype_apid = ccsds_hdr.data_dtype
            except ValueError:
                print(
                    f"APID: 0x{ccsds_hdr.apid:x};"
                    f" Packet Length: {ccsds_hdr.packet_size:d}"
                )
                dtype_apid = None

            if dtype_apid is not None:  # all valid APIDs
                buff = np.frombuffer(packet, count=1, offset=0, dtype=dtype_apid)
                ccsds_hk += (buff,)
            else:
                module_logger.warning(
                    "package with APID 0x%x and length %d is not implemented",
                    ccsds_hdr.apid,
                    ccsds_hdr.packet_size,
                )

        return ccsds_hk

    def navigation(self: HKTio) -> None:
        """..."""
        if self.nav_data is not None:
            self.nav_data.close()
            self.nav_data = None

        nav = None
        for hkt_file in self.flnames:
            if nav is None:
                nav = xr.open_dataset(hkt_file, group="navigation_data")
            else:
                res = ()
                buff = xr.open_dataset(hkt_file, group="navigation_data")
                for key, xarr in buff.data_vars.items():
                    res += (xr.concat((nav[key], buff[key]), dim=xarr.dims[0]),)
                nav = xr.merge(res, combine_attrs="drop_conflicts")

        # fix coordinates
        if "att_time" in nav.data_vars:
            nav = nav.rename({"att_records": "att_time"}).set_coords(["att_time"])
        if "orb_time" in nav.data_vars:
            nav = nav.rename({"orb_records": "orb_time"}).set_coords(["orb_time"])
        if "tilt_time" in nav.data_vars:
            nav = nav.rename({"tilt_records": "tilt_time"}).set_coords(["tilt_time"])
        # clean-up Dataset attributes
        for key in list(nav.attrs):
            del nav.attrs[key]
        nav.attrs["time_coverage_start"] = str(nav["att_time"].values[0])[:23] + "Z"
        nav.attrs["time_coverage_end"] = str(nav["att_time"].values[-1])[:23] + "Z"
        self.nav_data = nav

    def add_nav(
        self: HKTio, l1a_file: Path, coverage: list[dt.datetime, dt.datetime]
    ) -> None:
        """..."""
        time_range = (
            np.datetime64(coverage[0].replace(tzinfo=None)) - TEN_SECONDS,
            np.datetime64(coverage[1].replace(tzinfo=None)) + TEN_SECONDS,
        )
        att_indx = (
            (self.nav_data.att_time.values >= time_range[0])
            & (self.nav_data.att_time.values <= time_range[1])
        ).nonzero()[0]
        orb_indx = (
            (self.nav_data.orb_time.values >= time_range[0])
            & (self.nav_data.orb_time.values <= time_range[1])
        ).nonzero()[0]
        tilt_indx = (
            (self.nav_data.tilt_time.values >= time_range[0])
            & (self.nav_data.tilt_time.values <= time_range[1])
        ).nonzero()[0]
        nav = self.nav_data.isel(
            att_time=att_indx, orb_time=orb_indx, tilt_time=tilt_indx
        )
        self.nav_data.close()

        # add coverage-quality flag
        xarr = xr.DataArray(
            np.array([CoverageFlag.check(nav, coverage)]),
            name="coverage_quality",
            attrs={
                "long_name": "coverage quality of navigation data",
                "standard_name": "status_flag",
                "valid_range": np.array([0, 15], dtype="u2"),
                "flag_values": np.array([0, 1, 2, 4, 8], dtype="u2"),
                "flag_meanings": (
                    "good missing-samples too_short_extends no_extend_at_start"
                    " no_extend_at_end"
                ),
            },
        )
        nav["coverage_quality"] = xarr
        nav.to_netcdf(l1a_file, group="navigation_data", mode="a")
        nav.close()


# --------------------------------------------------
# from pyspex.tlm import SPXtlm
#
# Remarks:
# - Option 'githash' not available for pyspex_version()
# --------------------------------------------------
# - helper functions ------------------------
def subsec2musec(sub_sec: int) -> int:
    """Return subsec as microseconds."""
    return 100 * int(sub_sec / 65536 * 10000)


def mask2slice(mask: npt.NDArray[bool]) -> None | slice | tuple | npt.NDArray[bool]:
    """Try to slice (faster), instead of boolean indexing (slow)."""
    if np.all(~mask):
        return None
    if np.all(mask):
        return np.s_[:]  # read everything

    indx = np.nonzero(mask)[0]
    if np.all(np.diff(indx) == 1):
        # perform start-stop indexing
        return np.s_[indx[0] : indx[-1] + 1]

    # perform boolean indexing
    return mask


def add_proc_conf(l1a_file: Path, yaml_conf: Path) -> None:
    """Add dataset 'processor_configuration' to an existing L1A product.

    Parameters
    ----------
    l1a_file :  Path
       name of an existing L1A product.
    yaml_conf :  Path
       name of the YAML file with the processor settings

    """
    with Dataset(l1a_file, "r+") as fid:
        dset = fid.createVariable("processor_configuration", str)
        dset.comment = (
            "Configuration parameters used during"
            " the processor run that produced this file."
        )
        dset.markup_language = "YAML"
        dset[0] = "".join(
            [
                s
                for s in yaml_conf.open(encoding="ascii").readlines()
                if not (s == "\n" or s.startswith("#"))
            ]
        )


# - class HKtlm -----------------------------
class HKtlm:
    """Class to handle SPEXone housekeeping telemetry packets."""

    def __init__(self: HKtlm) -> None:
        """Initialize HKtlm object."""
        self.hdr: np.ndarray | None = None
        self.tlm: np.ndarray | None = None
        self.tstamp: list[dt.datetime, ...] | list = []
        self.events: list[np.ndarray, ...] | list = []

    def init_attrs(self: HKtlm) -> None:
        """Initialize class attributes."""
        self.hdr = None
        self.tlm = None
        self.tstamp = []
        self.events = []

    @property
    def size(self: HKtlm) -> int:
        """Return number of elements."""
        return 0 if self.tlm is None else len(self.tlm)

    def copy(self: HKtlm) -> HKtlm:
        """Return deep-copy of HKtlm object."""
        hkt = HKtlm()
        hkt.hdr = self.hdr.copy()
        hkt.tlm = self.tlm.copy()
        hkt.tstamp = copy(self.tstamp)
        hkt.events = copy(self.events)
        return hkt

    def sel(self: HKtlm, mask: np.NDArray[bool]) -> HKtlm:
        """Return subset of HKtlm object using a mask array."""
        hkt = HKtlm()
        hkt.hdr = self.hdr[mask]
        hkt.tlm = self.tlm[mask]
        hkt.tstamp = [x for x, y in zip(self.tstamp, mask) if y]
        hkt.events = self.events.copy()
        return hkt

    def extract_l0_hk(self: HKtlm, ccsds_hk: tuple, epoch: dt.datetime) -> None:
        """Extract data from SPEXone level-0 housekeeping telemetry packets.

        Parameters
        ----------
        ccsds_hk :  tuple[np.ndarray, ...]
           SPEXone level-0 housekeeping telemetry packets
        epoch :  dt.datetime
           Epoch of the telemetry packets (1958 or 1970)

        """
        self.init_attrs()
        if not ccsds_hk:
            return

        self.hdr = np.empty(len(ccsds_hk), dtype=ccsds_hk[0]["hdr"].dtype)
        self.tlm = np.empty(len(ccsds_hk), dtype=tmtc_dtype(0x320))
        ii = 0
        for buf in ccsds_hk:
            ccsds_hdr = CCSDShdr(buf["hdr"][0])

            # Catch TcAccept, TcReject, TcExecute, TcFail and EventRp as events
            if ccsds_hdr.apid != 0x320 or buf["hdr"]["tai_sec"] < len(ccsds_hk):
                if 0x331 <= ccsds_hdr.apid <= 0x335:
                    self.events.append(buf)
                continue

            self.hdr[ii] = buf["hdr"]
            self.tlm[ii] = buf["hk"]
            self.tstamp.append(
                epoch
                + dt.timedelta(
                    seconds=int(buf["hdr"]["tai_sec"][0]),
                    microseconds=subsec2musec(buf["hdr"]["sub_sec"][0]),
                )
            )
            ii += 1

        self.hdr = self.hdr[:ii]
        self.tlm = self.tlm[:ii]

        # These values are originally stored in little-endian, but
        # Numpy does not accept a mix of little & big-endian values
        # in a structured array.
        self.tlm["HTR1_CALCPVAL"][:] = self.tlm["HTR1_CALCPVAL"].byteswap()
        self.tlm["HTR2_CALCPVAL"][:] = self.tlm["HTR2_CALCPVAL"].byteswap()
        self.tlm["HTR3_CALCPVAL"][:] = self.tlm["HTR3_CALCPVAL"].byteswap()
        self.tlm["HTR4_CALCPVAL"][:] = self.tlm["HTR4_CALCPVAL"].byteswap()
        self.tlm["HTR1_CALCIVAL"][:] = self.tlm["HTR1_CALCIVAL"].byteswap()
        self.tlm["HTR2_CALCIVAL"][:] = self.tlm["HTR2_CALCIVAL"].byteswap()
        self.tlm["HTR3_CALCIVAL"][:] = self.tlm["HTR3_CALCIVAL"].byteswap()
        self.tlm["HTR4_CALCIVAL"][:] = self.tlm["HTR4_CALCIVAL"].byteswap()

    def extract_l1a_hk(self: HKtlm, fid: h5py.File, mps_id: int | None) -> None:
        """Extract data from SPEXone level-1a housekeeping telemetry packets.

        Parameters
        ----------
        fid :  h5py.File
           File pointer to a SPEXone level-1a product
        mps_id : int, optional
           Select data performed with MPS equals 'mps_id'

        """
        self.init_attrs()

        # pylint: disable=no-member
        dset = fid["/engineering_data/NomHK_telemetry"]
        if mps_id is None:
            data_sel = np.s_[:]
        else:
            data_sel = mask2slice(dset.fields("MPS_ID")[:] == mps_id)
            if data_sel is None:
                return
        self.tlm = dset[data_sel]

        dset = fid["/engineering_data/HK_tlm_time"]
        ref_date = dset.attrs["units"].decode()[14:] + "+00:00"
        epoch = dt.datetime.fromisoformat(ref_date)
        for sec in dset[data_sel]:
            self.tstamp.append(epoch + dt.timedelta(seconds=sec))

    def convert(self: HKtlm, key: str) -> np.ndarray:
        """Convert telemetry parameter to physical units.

        Parameters
        ----------
        key :  str
           Name of telemetry parameter

        Returns
        -------
        np.ndarray

        """
        parm = key.upper()
        if parm in ("HTR1_POWER", "HTR2_POWER", "HTR3_POWER", "HTR4_POWER"):
            parm = parm.replace("_POWER", "_I")
        else:
            if parm not in self.tlm.dtype.names:
                raise KeyError(
                    f"Parameter: {parm} not found" f" in {self.tlm.dtype.names}"
                )
        raw_data = np.array([x[parm] for x in self.tlm])
        return convert_hk(key.upper(), raw_data)


# - class SCItlm ----------------------------
class SCItlm:
    """Class to handle SPEXone Science-telemetry packets."""

    def __init__(self: SCItlm) -> None:
        """Initialize SCItlm object."""
        self.hdr: np.ndarray | None = None
        self.tlm: np.ndarray | None = None
        self.tstamp: np.ndarray | None = None
        self.images: tuple[np.ndarray, ...] | tuple[()] = ()

    def init_attrs(self: SCItlm) -> None:
        """Initialize class attributes."""
        self.hdr = None
        self.tlm = None
        self.tstamp = None
        self.images = ()

    @property
    def size(self: SCItlm) -> int:
        """Return number of elements."""
        return 0 if self.tlm is None else len(self.tlm)

    def copy(self: SCItlm) -> SCItlm:
        """Return deep-copy of SCItlm object."""
        sci = SCItlm()
        sci.hdr = self.hdr.copy()
        sci.tlm = self.tlm.copy()
        sci.tstamp = self.tstamp.copy()
        sci.images = copy(self.images)

    def sel(self: SCItlm, mask: np.NDArray[bool]) -> SCItlm:
        """Return subset of SCItlm object using a mask array."""
        sci = SCItlm()
        sci.hdr = self.hdr[mask]
        sci.tlm = self.tlm[mask]
        sci.tstamp = self.tstamp[mask]
        sci.images = tuple(x for x, y in zip(self.images, mask) if y)
        return sci

    def extract_l0_sci(self: SCItlm, ccsds_sci: tuple, epoch: dt.datetime) -> int:
        """Extract SPEXone level-0 Science-telemetry data.

        Parameters
        ----------
        ccsds_sci :  tuple[np.ndarray, ...]
           SPEXone level-0 Science-telemetry packets
        epoch :  dt.datetime
           Epoch of the telemetry packets (1958 or 1970)

        Returns
        -------
        int
            number of detector frames

        """
        self.init_attrs()
        if not ccsds_sci:
            return 0

        n_frames = 0
        hdr_dtype = None
        hk_dtype = None
        found_start_first = False
        for buf in ccsds_sci:
            ccsds_hdr = CCSDShdr(buf["hdr"][0])
            if ccsds_hdr.grouping_flag == 1:
                found_start_first = True
                if n_frames == 0:
                    hdr_dtype = buf["hdr"].dtype
                    hk_dtype = buf["hk"].dtype
                    continue

            if not found_start_first:
                continue

            if ccsds_hdr.grouping_flag == 2:
                found_start_first = False
                n_frames += 1

        # do we have any complete detector images (Note ccsds_sci not empty!)?
        if n_frames == 0:
            return 0

        # allocate memory
        self.hdr = np.empty(n_frames, dtype=hdr_dtype)
        self.tlm = np.empty(n_frames, dtype=hk_dtype)
        self.tstamp = np.empty(n_frames, dtype=TSTAMP_TYPE)

        # extract data from ccsds_sci
        ii = 0
        img = None
        found_start_first = False
        for buf in ccsds_sci:
            ccsds_hdr = CCSDShdr(buf["hdr"][0])
            if ccsds_hdr.grouping_flag == 1:
                found_start_first = True
                self.hdr[ii] = buf["hdr"]
                self.tlm[ii] = buf["hk"]
                self.tstamp[ii] = (
                    buf["icu_tm"]["tai_sec"][0],
                    buf["icu_tm"]["sub_sec"][0],
                    epoch
                    + dt.timedelta(
                        seconds=int(buf["icu_tm"]["tai_sec"][0]),
                        microseconds=subsec2musec(buf["icu_tm"]["sub_sec"][0]),
                    ),
                )
                img = (buf["frame"][0],)
                continue

            if not found_start_first:
                continue

            if ccsds_hdr.grouping_flag == 0:
                img += (buf["frame"][0],)
            elif ccsds_hdr.grouping_flag == 2:
                found_start_first = False
                img += (buf["frame"][0],)
                img = np.concatenate(img)
                if img.size == self.tlm[ii]["IMRLEN"] // 2:
                    self.images += (img,)
                    ii += 1
                else:
                    n_frames -= 1
                if ii == n_frames:
                    break

        # adjust number of frames for corrupted images
        if ii != self.hdr.size:
            self.hdr = self.hdr[:ii]
            self.tlm = self.tlm[:ii]
            self.tstamp = self.tstamp[:ii]

        return n_frames

    def extract_l1a_sci(self: SCItlm, fid: h5py.File, mps_id: int | None) -> None:
        """Extract data from SPEXone level-1a Science-telemetry packets.

        Parameters
        ----------
        fid :  h5py.File
           File pointer to a SPEXone level-1a product
        mps_id : int, optional
           Select data performed with MPS equals 'mps_id'

        """
        # pylint: disable=no-member
        self.init_attrs()

        # read science telemetry
        dset = fid["/science_data/detector_telemetry"]
        if mps_id is None:
            data_sel = np.s_[:]
        else:
            data_sel = mask2slice(dset.fields("MPS_ID")[:] == mps_id)
            if data_sel is None:
                return
        self.tlm = fid["/science_data/detector_telemetry"][data_sel]

        # determine time-stamps
        dset = fid["/image_attributes/icu_time_sec"]
        seconds = dset[data_sel]
        try:
            _ = dset.attrs["units"].index(b"1958")
        except ValueError:
            epoch = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc)
        else:
            epoch = dt.datetime(1958, 1, 1, tzinfo=dt.timezone.utc)
            epoch -= dt.timedelta(seconds=get_leap_seconds(seconds[0]))
        subsec = fid["/image_attributes/icu_time_subsec"][data_sel]

        _dt = []
        for ii, sec in enumerate(seconds):
            msec_offs = self.readout_offset(ii)
            _dt.append(
                epoch
                + dt.timedelta(
                    seconds=int(sec),
                    milliseconds=-msec_offs,
                    microseconds=subsec2musec(subsec[ii]),
                )
            )

        self.tstamp = np.empty(len(seconds), dtype=TSTAMP_TYPE)
        self.tstamp["tai_sec"] = seconds
        self.tstamp["sub_sec"] = subsec
        self.tstamp["dt"] = _dt

        # read image data
        self.images = fid["/science_data/detector_images"][data_sel, :]

    def exposure_time(self: SCItlm, indx: int | None = None) -> np.ndarray:
        """Return exposure time [ms]."""
        if indx is None:
            indx = np.s_[:]
        return 129e-4 * (
            0.43 * self.tlm["DET_FOTLEN"][indx] + self.tlm["DET_EXPTIME"][indx]
        )

    def frame_period(self: SCItlm, indx: int) -> float:
        """Return frame period of detector measurement [ms]."""
        n_coad = self.tlm["REG_NCOADDFRAMES"][indx]
        # binning mode
        if self.tlm["REG_FULL_FRAME"][indx] == 2:
            return float(n_coad * DET_CONSTS["FTI_science"])

        # full-frame mode
        return float(
            n_coad
            * np.clip(
                DET_CONSTS["FTI_margin"]
                + DET_CONSTS["overheadTime"]
                + self.exposure_time(indx),
                a_min=DET_CONSTS["FTI_diagnostic"],
                a_max=None,
            )
        )

    def readout_offset(self: SCItlm, indx: int) -> float:
        """Return offset wrt start-of-integration [ms]."""
        n_coad = self.tlm["REG_NCOADDFRAMES"][indx]
        n_frm = (
            n_coad + 3
            if self.tlm["IMRLEN"][indx] == FULLFRAME_BYTES
            else 2 * n_coad + 2
        )

        return n_frm * self.frame_period(indx)

    def binning_table(self: SCItlm) -> np.ndarray:
        """Return binning table identifier (zero for full-frame images)."""
        bin_tbl = np.zeros(len(self.tlm), dtype="i1")
        _mm = self.tlm["IMRLEN"] == FULLFRAME_BYTES
        if np.sum(_mm) == len(self.tlm):
            return bin_tbl

        bin_tbl_start = self.tlm["REG_BINNING_TABLE_START"]
        bin_tbl[~_mm] = 1 + (bin_tbl_start[~_mm] - 0x80000000) // 0x400000
        return bin_tbl

    def digital_offset(self: SCItlm) -> np.ndarray:
        """Return digital offset including ADC offset [count]."""
        buff = self.tlm["DET_OFFSET"].astype("i4")
        buff[buff >= 8192] -= 16384

        return buff + 70

    def convert(self: SCItlm, key: str) -> np.ndarray:
        """Convert telemetry parameter to physical units.

        Parameters
        ----------
        key :  str
           Name of telemetry parameter

        Returns
        -------
        np.ndarray

        """
        parm = key.upper()
        if parm in ("HTR1_POWER", "HTR2_POWER", "HTR3_POWER", "HTR4_POWER"):
            parm = parm.replace("_POWER", "_I")
        else:
            if parm not in self.tlm.dtype.names:
                raise KeyError(
                    f"Parameter: {parm} not found" f" in {self.tlm.dtype.names}"
                )
        raw_data = np.array([x[parm] for x in self.tlm])
        return convert_hk(key.upper(), raw_data)


def get_l1a_filename(
    config: dataclass,
    coverage: tuple[dt.datetime, dt.datetime],
    mode: str | None = None,
) -> Path:
    """Return filename of level-1A product.

    Parameters
    ----------
    config :  dataclass
        Settings for the L0->l1A processing
    coverage :  tuple[dt.datetime, dt.datetime]
        time_coverage_start and time_coverage_end of the data
    mode :  {'binned', 'full'} | None, default=None
        Select Science packages with full-frame images or binned images

    Returns
    -------
    Path
        Filename of level-1A product.

    Notes
    -----
    L1A file name format, following the NASA ... naming convention:
        PACE_SPEXONE[_TTT].YYYYMMDDTHHMMSS.L1A[.Vnn].nc
    where
        TTT is an optional data type (e.g., for the calibration data files)
        YYYYMMDDTHHMMSS is time stamp of the first image in the file
        Vnn file-version number (omitted when nn=1)

    for example (file-version=1):
        [Science Product] PACE_SPEXONE.20230115T123456.L1A.nc
        [Calibration Product] PACE_SPEXONE_CAL.20230115T123456.L1A.nc
        [Dark science Product] PACE_SPEXONE_DARK.20230115T123456.L1A.nc

    """
    if config.outfile:
        return config.outdir / config.outfile

    # +++++ in-flight product-name convention +++++
    if config.eclipse is None:
        subtype = "_OCAL"
    elif not config.eclipse:
        subtype = ""
    else:
        subtype = "_CAL" if mode == "full" else "_DARK"

    prod_ver = "" if config.file_version == 1 else f".V{config.file_version:02d}"

    return config.outdir / (
        f'PACE_SPEXONE{subtype}'
        f'.{coverage[0].strftime("%Y%m%dT%H%M%S"):15s}'
        f'.L1A{prod_ver}.nc'
    )


# - class SPXtlm ----------------------------
class SPXtlm:
    """Access/convert parameters of SPEXone Science telemetry data.

    Notes
    -----
    This class has the following methods::

     - set_coverage(coverage: tuple[datetime, datetime] | None,
                    update: boot = False) -> None
     - reference_date() -> datetime
     - time_coverage_start() -> datetime
     - time_coverage_end() -> datetime
     - from_hkt(flnames: Path | list[Path], *,
                instrument: str | None = None, dump: bool = False) -> None
     - from_lv0(flnames: Path | list[Path], *,
                file_format: str, tlm_type: str | None = None,
                debug: bool = False, dump: bool = False) -> None
     - from_l1a(flname: Path, *, tlm_type: str | None = None,
                mps_id: int | None = None) -> None
     - gen_l1a(config: dataclass) -> None


    Examples
    --------
    Read data from SPEXone level-0 products

    >>> from pyspex.tlm import SPXtlm
    >>> spx = SPXtlm()
    >>> spx.from_l0(list_l0_products, tlm_type='hk')
    # returns list of TcAccept, TcReject, TcExecute, TcFail and EventRp
    >>> spx.nomhk.events
    # return detector images
    >>> spx.from_l0(list_l0_products, tlm_type='sci')
    >>> spx.science.images

    Read data from a SPEXone level-1a product

    >>> from pyspex.tlm import SPXtlm
    >>> spx = SPXtlm()
    >>> spx.from_l1a(l1a_product, tlm_type='sci', mps_is=47)
    >>> spx.science.images

    Read data from a PACE-HKT product

    >>> from pyspex.tlm import SPXtlm
    >>> spx = SPXtlm()
    >>> spx.from_hkt(hkt_product)
    # returns list of TcAccept, TcReject, TcExecute, TcFail and EventRp
    >>> spx.nomhk.events

    """

    def __init__(self: SPXtlm) -> None:
        """Initialize SPXtlm object."""
        self.mode = "all"
        self.logger = logging.getLogger(__name__)
        self.file_list: list[Path, ...] | None = None
        self._coverage: list[dt.datetime, dt.datetime] | None = None
        self.nomhk = HKtlm()
        self.science = SCItlm()

    @property
    def coverage(self: SPXtlm) -> list[dt.datetime, dt.datetime] | None:
        """Return data time_coverage."""
        return self._coverage

    def set_coverage(
        self: SPXtlm,
        coverage_new: list[dt.datetime, dt.datetime] | None,
        extent: bool = False,
    ) -> None:
        """Set, reset or update the class attribute `coverage`.

        Parameters
        ----------
        coverage_new :  list[dt.datetime, dt.datetime] | None,
           Provide new time_coverage range, or reset when coverage_new is None
        extent :  bool, default=False
           Extent existing time_coverage range with coverage_new

        Examples
        --------
        Reset self.coverage to a new time_coverage:
        > self.set_coverage(None)
        > self.set_coverage(coverage_new)

        Extent self.coverage with a new time_coverage
        if self.coverage is not None else self.coverage will be coverage_new:
        > self.set_coverage(coverage_new, extent=True)

        This will not change self.coverage to coverage_new
        when self.coverage is not None:
        > self.set_coverage(coverage_new)

        """
        if self._coverage is None or coverage_new is None:
            self._coverage = coverage_new
            return

        if extent:
            one_hour = dt.timedelta(hours=1)
            if self._coverage[0] - one_hour < coverage_new[0] < self._coverage[0]:
                self._coverage[0] = coverage_new[0]
            if self._coverage[1] < coverage_new[1] < self._coverage[1] + one_hour:
                self._coverage[1] = coverage_new[1]

    @property
    def reference_date(self: SPXtlm) -> dt.datetime:
        """Return date of reference day (tzone aware)."""
        if self._coverage is None:
            raise ValueError("no valid timestamps found")

        return dt.datetime.combine(
            self._coverage[0].date(), dt.time(0), dt.timezone.utc
        )

    @property
    def time_coverage_start(self: SPXtlm) -> dt.datetime | None:
        """Return time_coverage_start."""
        return None if self._coverage is None else self._coverage[0]

    @property
    def time_coverage_end(self: SPXtlm) -> dt.datetime | None:
        """Return time_coverage_end."""
        return None if self._coverage is None else self._coverage[1]

    def sel(self: SPXtlm, mask: np.NDArray[bool]) -> SPXtlm:
        """Return subset of SPXtlm object using a mask array."""
        spx = copy(self)
        spx.set_coverage(None)
        spx.science = self.science.sel(mask)

        # set Science time_coverage_range
        indices = mask.nonzero()[0]
        if len(indices) == 1:
            frame_period = dt.timedelta(milliseconds=self.science.frame_period(0))
            spx.set_coverage(
                [
                    spx.science.tstamp[0]["dt"],
                    spx.science.tstamp[0]["dt"] + frame_period,
                ]
            )
        else:
            frame_period = dt.timedelta(milliseconds=self.science.frame_period(-1))
            spx.set_coverage(
                [
                    spx.science.tstamp[0]["dt"],
                    spx.science.tstamp[-1]["dt"] + frame_period,
                ]
            )
        # select nomhk data within Science time_coverage_range
        hk_tstamps = np.array(
            [x.replace(tzinfo=None) for x in self.nomhk.tstamp], dtype="datetime64"
        )
        dt_min = np.datetime64(spx.coverage[0].replace(tzinfo=None)) - np.timedelta64(
            1, "s"
        )
        dt_max = np.datetime64(spx.coverage[1].replace(tzinfo=None)) + np.timedelta64(
            1, "s"
        )
        spx.nomhk = self.nomhk.sel((hk_tstamps >= dt_min) & (hk_tstamps <= dt_max))
        return spx

    def from_hkt(
        self: SPXtlm,
        flnames: Path | list[Path],
        *,
        instrument: str | None = None,
        dump: bool = False,
    ) -> None:
        """Read telemetry data from PACE HKT product(s).

        Parameters
        ----------
        flnames :  Path | list[Path]
           sorted list of PACE_HKT filenames (netCDF4 format)
        instrument :  {'spx', 'sc', 'oci', 'harp'}, default='spx'
           abbreviations for the PACE instruments
        dump :  bool, default=False
           dump header information of the telemetry packages @1Hz for
           debugging purposes

        """
        self.set_coverage(None)
        if isinstance(flnames, Path):
            flnames = [flnames]
        if instrument is None:
            instrument = "spx"
        elif instrument not in ["spx", "sc", "oci", "harp"]:
            raise KeyError("instrument not in ['spx', 'sc', 'oci', 'harp']")

        self.file_list = flnames

        # check number of telemetry data-packages
        ccsds_hk = HKTio(flnames).housekeeping(instrument)
        if not ccsds_hk:
            return

        # perform dump of telemetry data-packages
        if dump:
            dump_hkt(flnames[0].stem + "_hkt.dump", ccsds_hk)
            return

        # check if TAI timestamp is valid
        ii = len(ccsds_hk) // 2
        tai_sec = ccsds_hk[ii]["hdr"]["tai_sec"][0]
        if tai_sec == 0:
            return

        # set epoch
        epoch = dt.datetime(1958, 1, 1, tzinfo=dt.timezone.utc)
        epoch -= dt.timedelta(seconds=get_leap_seconds(float(tai_sec)))
        self.nomhk.extract_l0_hk(ccsds_hk, epoch)

        # reject nomHK records before or after a big time-jump
        _mm = np.diff(self.nomhk.tstamp) > np.timedelta64(1, "D")
        if np.any(_mm):
            indx = _mm.nonzero()[0]
            _mm = np.full(self.nomhk.size, True, dtype=bool)
            _mm[indx[0] + 1 :] = False
            if np.sum(_mm) < self.nomhk.size // 2:
                _mm = ~_mm
            self.logger.warning(
                "rejected nomHK: %d -> %d", self.nomhk.size, np.sum(_mm)
            )
            self.nomhk.sel(_mm)

        # set time-coverage
        self.set_coverage(
            [self.nomhk.tstamp[0], self.nomhk.tstamp[-1] + dt.timedelta(seconds=1)]
        )

    def from_lv0(
        self: SPXtlm,
        flnames: Path | list[Path],
        *,
        file_format: str,
        tlm_type: str | None = None,
        debug: bool = False,
        dump: bool = False,
    ) -> None:
        """Read telemetry data from SPEXone level-0 product(s).

        Parameters
        ----------
        flnames :  Path | list[Path]
           sorted list of CCSDS filenames
        file_format : {'raw', 'st3', 'dsb'}
           type of CCSDS data
        tlm_type :  {'hk', 'sci', 'all'}, default='all'
           select type of telemetry packages
           Note that we allways read the complete level-0 products.
        debug : bool, default=False
           run in debug mode, read only packages heades
        dump :  bool, default=False
           dump header information of the telemetry packages @1Hz for
           debugging purposes

        """
        self.set_coverage(None)
        if isinstance(flnames, Path):
            flnames = [flnames]
        if tlm_type is None:
            tlm_type = "all"
        elif tlm_type not in ["hk", "sci", "all"]:
            raise KeyError("tlm_type not in ['hk', 'sci', 'all']")
        if file_format not in ["raw", "st3", "dsb"]:
            raise KeyError("file_format not in ['raw', 'st3', 'dsb']")
        self.file_list = flnames

        # read telemetry data
        ccsds_sci, ccsds_hk = read_lv0_data(flnames, file_format, debug=debug)
        if dump:
            if ccsds_hk:
                dump_hkt(flnames[0].stem + "_hkt.dump", ccsds_hk)
            if ccsds_sci:
                dump_science(flnames[0].stem + "_sci.dump", ccsds_sci)

        # exit when debugging or only an ASCII data-dump is requested
        if debug or dump:
            return

        # exit when Science data is requested and no Science data is available
        #   or when housekeeping is requested and no housekeeping data is available
        if (not ccsds_sci and tlm_type == "sci") or (not ccsds_hk and tlm_type == "hk"):
            self.logger.info("Asked for tlm_type=%s, but none found", tlm_type)
            return

        # set epoch
        if file_format == "dsb":
            # check if TAI timestamp is valid
            ii = len(ccsds_hk) // 2
            tai_sec = ccsds_hk[ii]["hdr"]["tai_sec"][0]
            if tai_sec == 0:
                return

            epoch = dt.datetime(1958, 1, 1, tzinfo=dt.timezone.utc)
            epoch -= dt.timedelta(seconds=get_leap_seconds(float(tai_sec)))
        else:
            epoch = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc)

        if tlm_type != "hk":
            # collect Science telemetry data
            if self.science.extract_l0_sci(ccsds_sci, epoch) == 0:
                self.logger.info("no valid Science package found")
            else:
                # reject Science records before or after a big time-jump
                _mm = np.diff(self.science.tstamp["dt"]) > np.timedelta64(1, "D")
                if np.any(_mm):
                    indx = _mm.nonzero()[0]
                    _mm = np.full(self.science.size, True, dtype=bool)
                    _mm[indx[0] + 1 :] = False
                    if np.sum(_mm) < self.science.size // 2:
                        _mm = ~_mm
                    self.logger.warning(
                        "rejected science: %d -> %d", self.science.size, np.sum(_mm)
                    )
                    self.science.sel(_mm)

                # set time-coverage
                intg = dt.timedelta(milliseconds=self.science.frame_period(-1))
                self.set_coverage(
                    [self.science.tstamp["dt"][0], self.science.tstamp["dt"][-1] + intg]
                )

        # collected NomHK telemetry data
        if tlm_type != "sci" and ccsds_hk:
            self.nomhk.extract_l0_hk(ccsds_hk, epoch)

            # reject nomHK records before or after a big time-jump
            _mm = np.diff(self.nomhk.tstamp) > np.timedelta64(1, "D")
            if np.any(_mm):
                indx = _mm.nonzero()[0]
                _mm = np.full(self.nomhk.size, True, dtype=bool)
                _mm[indx[0] + 1 :] = False
                if self.coverage is None:
                    if np.sum(_mm) < self.nomhk.size // 2:
                        _mm = ~_mm
                else:
                    dt_start = abs(self.coverage[0] - self.nomhk.tstamp[0])
                    dt_end = abs(self.coverage[-1] - self.nomhk.tstamp[-1])
                    if dt_start > dt_end:
                        _mm = ~_mm
                self.logger.warning(
                    "rejected nomHK: %d -> %d", self.nomhk.size, np.sum(_mm)
                )
                self.nomhk.sel(_mm)

            # set time-coverage (only, when self._coverage is None)
            self.set_coverage(
                [self.nomhk.tstamp[0], self.nomhk.tstamp[-1] + dt.timedelta(seconds=1)]
            )

    def from_l1a(
        self: SPXtlm,
        flname: Path,
        *,
        tlm_type: str | None = None,
        mps_id: int | None = None,
    ) -> None:
        """Read telemetry data from SPEXone level-1A product.

        Parameters
        ----------
        flname :  Path
           name of one SPEXone level-1A product
        tlm_type :  {'hk', 'sci', 'all'}, default='all'
           select type of telemetry packages
        mps_id :  int, optional
           select on MPS ID

        """
        self.set_coverage(None)
        if tlm_type is None:
            tlm_type = "all"
        elif tlm_type not in ["hk", "sci", "all"]:
            raise KeyError("tlm_type not in ['hk', 'sci', 'all']")

        self.file_list = [flname]
        with h5py.File(flname) as fid:
            # collect Science telemetry data
            if tlm_type != "hk":
                self.science.extract_l1a_sci(fid, mps_id)
                if self.science.tstamp is not None:
                    _mm = self.science.tstamp["tai_sec"] > TSTAMP_MIN
                    if np.any(_mm):
                        tstamp = self.science.tstamp["dt"][_mm]
                        ii = int(np.nonzero(_mm)[0][-1])
                        intg = dt.timedelta(milliseconds=self.science.frame_period(ii))
                        self.set_coverage([tstamp[0], tstamp[-1] + intg])

            # collected NomHk telemetry data
            if tlm_type != "sci":
                self.nomhk.extract_l1a_hk(fid, mps_id)
                self.set_coverage(
                    [
                        self.nomhk.tstamp[0],
                        self.nomhk.tstamp[-1] + dt.timedelta(seconds=1),
                    ]
                )

    def l1a_file(self: SPXtlm, config: dataclass, mode: str) -> Path:
        """Return filename of level-1A product.

        Parameters
        ----------
        config :  dataclass
           Settings for the L0->l1A processing
        mode :  {'all', 'binned', 'full'}
           Select Science packages with full-frame images or binned images

        Returns
        -------
        Path
           Filename of level-1A product.

        Notes
        -----
        === Inflight ===
        L1A file name format, following the NASA ... naming convention:
           PACE_SPEXONE[_TTT].YYYYMMDDTHHMMSS.L1A[.Vnn].nc
        where
           TTT is an optional data type (e.g., for the calibration data files)
           YYYYMMDDTHHMMSS is time stamp of the first image in the file
           Vnn file-version number (omitted when nn=1)
        for example (file-version=1):
           [Science Product] PACE_SPEXONE.20230115T123456.L1A.nc
           [Calibration Product] PACE_SPEXONE_CAL.20230115T123456.L1A.nc
           [Dark science Product] PACE_SPEXONE_DARK.20230115T123456.L1A.nc

        === OCAL ===
        L1A file name format:
           SPX1_OCAL_<msm_id>[_YYYYMMDDTHHMMSS]_L1A_vvvvvvv.nc
        where
           msm_id is the measurement identifier
           YYYYMMDDTHHMMSS is time stamp of the first image in the file
           vvvvvvv is the git-hash string of the pyspex repository

        """
        if config.outfile:
            return config.outdir / config.outfile

        # +++++ in-flight product-name convention +++++
        if config.l0_format != "raw":
            if config.eclipse is None:
                subtype = "_OCAL"
            elif not config.eclipse:
                subtype = ""
            else:
                subtype = "_CAL" if mode == "full" else "_DARK"

            prod_ver = (
                "" if config.file_version == 1 else f".V{config.file_version:02d}"
            )

            return config.outdir / (
                f'PACE_SPEXONE{subtype}'
                f'.{self.time_coverage_start.strftime("%Y%m%dT%H%M%S"):15s}'
                f'.L1A{prod_ver}.nc'
            )

        # +++++ OCAL product-name convention +++++
        # determine measurement identifier
        msm_id = config.l0_list[0].stem
        try:
            new_date = dt.datetime.strptime(msm_id[-22:], "%y-%j-%H:%M:%S.%f").strftime(
                "%Y%m%dT%H%M%S.%f"
            )
        except ValueError:
            pass
        else:
            msm_id = msm_id[:-22] + new_date

        return config.outdir / f"SPX1_OCAL_{msm_id}_L1A_{pyspex_version()}.nc"

    def full(self: SPXtlm) -> SPXtlm:
        """Select full-frame measurements."""
        self.mode = "full"
        if self.science.size == 0:
            return self

        sci_mask = (self.science.tstamp["tai_sec"] > TSTAMP_MIN) & (
            self.science.tlm["IMRLEN"] == FULLFRAME_BYTES
        )
        if np.all(~sci_mask):
            return SPXtlm()  # return empy object
        if np.all(sci_mask):
            return self  # return original object
        self.logger.debug("Rejected %d binned Science images", np.sum(~sci_mask))

        return copy(self).sel(sci_mask)

    def binned(self: SPXtlm) -> SPXtlm:
        """Select binned images from data."""
        self.mode = "binned"
        if self.science.size == 0:
            return self

        sci_mask = (self.science.tstamp["tai_sec"] > TSTAMP_MIN) & (
            self.science.tlm["IMRLEN"] < FULLFRAME_BYTES
        )
        if np.all(~sci_mask):
            return SPXtlm()  # return empy object
        if np.all(sci_mask):
            return self  # return original object
        self.logger.debug("Rejected %d full-frame Science images", np.sum(~sci_mask))

        return copy(self).sel(sci_mask)

    def gen_l1a(self: SPXtlm, config: dataclass) -> None:
        """Generate a SPEXone level-1A product.

        Parameters
        ----------
        config :  dataclass
           Settings for the L0->L1A processing

        """
        # sanity check
        if self.science.size == 0 and self.nomhk.size == 0:
            return

        if self.science.size > 0:
            mps_list = np.unique(self.science.tlm["MPS_ID"])
            self.logger.debug("unique Science MPS: %s", mps_list)
            self.nomhk = self.nomhk.sel(np.in1d(self.nomhk.tlm["MPS_ID"], mps_list))

        dims = {
            "number_of_images": self.science.size,
            "samples_per_image": (
                DET_CONSTS["dimRow"]
                if self.science.size == 0
                else np.max(self.science.tlm["IMRLEN"]) // 2
            ),
            "hk_packets": self.nomhk.size,
        }

        l1a_path = get_l1a_filename(config, self.coverage, self.mode)
        with L1Aio(
            l1a_path,
            self.reference_date,
            dims,
            compression=config.compression,
        ) as l1a:
            l1a.fill_global_attrs(inflight=config.l0_format != "raw")
            l1a.set_attr("processing_version", config.processing_version)
            l1a.set_attr("icu_sw_version", f'0x{self.nomhk.tlm["ICUSWVER"][0]:x}')
            l1a.set_attr(
                "time_coverage_start",
                self.time_coverage_start.isoformat(timespec="milliseconds"),
            )
            l1a.set_attr(
                "time_coverage_end",
                self.time_coverage_end.isoformat(timespec="milliseconds"),
            )
            l1a.set_attr("input_files", [x.name for x in config.l0_list])
            self.logger.debug("(1) initialized level-1A product")

            self._fill_engineering(l1a)
            self.logger.debug("(2) added engineering data")
            self._fill_science(l1a, dims["samples_per_image"])
            self.logger.debug("(3) added science data")
            self._fill_image_attrs(l1a, config.l0_format)
            self.logger.debug("(4) added image attributes")

        # add PACE navigation information from HKT products
        if config.hkt_list:
            hkt = HKTio(config.hkt_list)
            hkt.navigation()
            hkt.add_nav(l1a_path, self.coverage)
            self.logger.debug("(5) added PACE navigation data")

        # add processor_configuration
        if config.yaml_fl:
            add_proc_conf(l1a_path, config.yaml_fl)

        self.logger.info("successfully generated: %s", l1a_path.name)

    def _fill_engineering(self: SPXtlm, l1a: L1Aio) -> None:
        """Fill datasets in group '/engineering_data'."""
        if self.nomhk.size == 0:
            return

        l1a.set_dset("/engineering_data/NomHK_telemetry", self.nomhk.tlm)
        ref_date = self.reference_date
        l1a.set_dset(
            "/engineering_data/HK_tlm_time",
            [(x - ref_date).total_seconds() for x in self.nomhk.tstamp],
        )
        l1a.set_dset(
            "/engineering_data/temp_detector",
            self.nomhk.convert("TS1_DEM_N_T"),
        )
        l1a.set_dset(
            "/engineering_data/temp_housing",
            self.nomhk.convert("TS2_HOUSING_N_T"),
        )
        l1a.set_dset(
            "/engineering_data/temp_radiator",
            self.nomhk.convert("TS3_RADIATOR_N_T"),
        )

    def _fill_science(self: SPXtlm, l1a: L1Aio, samples_per_image: int) -> None:
        """Fill datasets in group '/science_data'."""
        if self.science.size == 0:
            return

        if len(np.unique([img.size for img in self.science.images])) == 1:
            l1a.set_dset("/science_data/detector_images", self.science.images)
        else:
            images = np.zeros((self.science.size, samples_per_image), dtype="u2")
            for ii, img in enumerate(self.science.images):
                images[ii, : img.size] = img
            l1a.set_dset("/science_data/detector_images", images)
        l1a.set_dset("/science_data/detector_telemetry", self.science.tlm)

    def _fill_image_attrs(self: SPXtlm, l1a: L1Aio, lv0_format: str) -> None:
        """Fill datasets in group '/image_attributes'."""
        if self.science.size == 0:
            return

        l1a.set_dset("/image_attributes/icu_time_sec", self.science.tstamp["tai_sec"])
        # modify attribute units for non-DSB products
        if lv0_format != "dsb":
            # timestamp of 2020-01-01T00:00:00+00:00
            l1a.set_attr(
                "valid_min",
                np.uint32(1577836800),
                ds_name="/image_attributes/icu_time_sec",
            )
            # timestamp of 2024-01-01T00:00:00+00:00
            l1a.set_attr(
                "valid_max",
                np.uint32(1704067200),
                ds_name="/image_attributes/icu_time_sec",
            )
            l1a.set_attr(
                "units",
                "seconds since 1970-01-01 00:00:00",
                ds_name="/image_attributes/icu_time_sec",
            )
        l1a.set_dset(
            "/image_attributes/icu_time_subsec",
            self.science.tstamp["sub_sec"],
        )
        ref_date = self.reference_date
        l1a.set_dset(
            "/image_attributes/image_time",
            [(x - ref_date).total_seconds() for x in self.science.tstamp["dt"]],
        )
        l1a.set_dset(
            "/image_attributes/image_ID",
            np.bitwise_and(self.science.hdr["sequence"], 0x3FFF),
        )
        l1a.set_dset("/image_attributes/binning_table", self.science.binning_table())
        l1a.set_dset("/image_attributes/digital_offset", self.science.digital_offset())
        l1a.set_dset(
            "/image_attributes/exposure_time",
            self.science.exposure_time() / 1000,
        )
        l1a.set_dset(
            "/image_attributes/nr_coadditions",
            self.science.tlm["REG_NCOADDFRAMES"],
        )


# --------------------------------------------------
# from pyspex.gen_l1a.cli import main
# --------------------------------------------------
def main() -> int:
    """Execute the main bit of the application."""
    error_code = 0
    warn_code = 0

    # initialize logger
    start_logger()
    logging.captureWarnings(True)

    # parse command-line parameters and YAML file for settings
    config = argparse_gen_l1a()
    logging.getLogger().setLevel(config.verbose)  # first, set the root logger
    logger = logging.getLogger("pyspex.gen_l1a")  # then initiate a descendant
    logger.debug("%s", config)

    # check input files (SEPXone level-0)
    try:
        check_input_files(config)
    except FileNotFoundError as exc:
        logger.fatal('File "%s" not found on system.', exc)
        return 110
    except TypeError as exc:
        logger.fatal("%s", exc)
        return 121

    # read level 0 data
    # Note that we read as much as possible packages from a file, but stop at
    # the first occurence of a corrupted data-packet, then the remainder of
    # the file is neglected.
    tlm = None
    with warnings.catch_warnings(record=True) as wrec_list:
        warnings.simplefilter("always", category=CorruptPacketWarning)
        try:
            tlm = SPXtlm()
            tlm.from_lv0(
                config.l0_list,
                file_format=config.l0_format,
                debug=config.debug,
                dump=config.dump,
            )
        except FileNotFoundError as exc:
            logger.fatal('FileNotFoundError exception raised for "%s".', exc)
            error_code = 110
        except TypeError as exc:
            logger.fatal('TypeError exception raised with "%s".', exc)
            error_code = 121

        for wrec in wrec_list:
            logger.warning('CorruptPacketWarning raised with "%s".', str(wrec.message))
            warn_code = 122

    if error_code != 0 or config.debug or config.dump:
        return error_code

    # Write Level-1A product.
    if not config.outdir.is_dir():
        config.outdir.mkdir(mode=0o755, parents=True)
    try:
        if config.eclipse is None or tlm.science.size == 0:
            tlm.gen_l1a(config)
        elif config.eclipse:
            tlm.full().gen_l1a(config)
            tlm.binned().gen_l1a(config)
        else:
            tlm.binned().gen_l1a(config)
    except (KeyError, RuntimeError) as exc:
        logger.fatal('RuntimeError with "%s"', exc)
        error_code = 131
    except UserWarning as exc:
        logger.warning('navigation data is incomplete: "%s".', exc)
        error_code = 132
    except Exception as exc:
        # raise RuntimeError from exc
        logger.fatal('Unexpected exception occurred with "%s".', exc)
        error_code = 135

    return warn_code if error_code == 0 else error_code


# --------------------------------------------------
if __name__ == "__main__":
    mtime_str = dt.datetime.fromtimestamp(Path(__file__).stat().st_mtime).isoformat(
        sep=" ", timespec="seconds"
    )
    print(f"l1agen_spex.py {pyspex_version()} ({mtime_str})\n")
    sys.exit(main())
