From e626c21be1ab70f7e15cfaf25a97642cd9ea7a8e Mon Sep 17 00:00:00 2001
From: Bruce Flynn <brucef@ssec.wisc.edu>
Date: Sat, 26 Sep 2015 15:26:08 +0000
Subject: [PATCH] Add jpssrdr module

---
 edosl0util/jpssrdr.py | 222 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 222 insertions(+)
 create mode 100644 edosl0util/jpssrdr.py

diff --git a/edosl0util/jpssrdr.py b/edosl0util/jpssrdr.py
new file mode 100644
index 0000000..0abed0d
--- /dev/null
+++ b/edosl0util/jpssrdr.py
@@ -0,0 +1,222 @@
+"""
+Code for reading/writing/manipulating JPSS Common RDR files as documented in:
+
+    Joint Polar Satellite System (JPSS)
+    Common Data Format Control Book - External (CDFCB-X)
+    Volume II - RDR Formats
+
+    http://jointmission.gsfc.nasa.gov/sciencedocs/2015-06/474-00001-02_JPSS-CDFCB-X-Vol-II_0123B.pdf
+"""
+import logging
+import ctypes as c
+from collections import namedtuple
+
+import numpy as np
+from h5py import File as H5File
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseStruct(c.BigEndianStructure):
+    _pack_ = 1
+    _fields_ = []
+
+    def __repr__(self):
+        attrs = ' '.join('%s=%s' % (f[0], getattr(self, f[0])) for f in self._fields_)
+        return '<{} {}>'.format(self.__class__.__name__, attrs)
+
+
+class StaticHeader(BaseStruct):
+
+    _fields_ = [
+        ('satellite', c.c_char * 4),
+        ('sensor', c.c_char * 16),
+        ('type_id', c.c_char * 16),
+        ('num_apids', c.c_uint32),
+        ('apid_list_offset', c.c_uint32),
+        ('pkt_tracker_offset', c.c_uint32),
+        ('ap_storage_offset', c.c_uint32),
+        ('next_pkt_pos', c.c_uint32),
+        ('start_boundry', c.c_uint64),
+        ('end_boundry', c.c_uint64),
+    ]
+
+
+class Apid(BaseStruct):
+
+    _fields_ = [
+        ('name', c.c_char * 16),
+        ('value', c.c_uint32),
+        ('pkt_tracker_start_idx', c.c_uint32),
+        ('pkts_reserved', c.c_uint32),
+        ('pkts_received', c.c_uint32),
+    ]
+
+
+class PacketTracker(BaseStruct):
+
+    _fields_ = [
+        ('obs_time', c.c_int64),
+        ('sequence_number', c.c_int32),
+        ('size', c.c_int32),
+        ('offset', c.c_int32),
+        ('fill_percent', c.c_int32),
+    ]
+
+
+def _make_packet(buf, size, offset):
+    """
+    Create a struct for a CCSDS packet. The struct size is dynamic so we need
+    a new class for each packet.
+
+    XXX: Many packets will have similar sizes so perhaps we should cache
+         packet impls based on their size.
+    """
+    # size minus the CCSDS primary header size
+    data_size = size - 6
+    class PktImpl(BaseStruct):  # noqa
+        _fields_ = [
+            ('version', c.c_uint8, 3),
+            ('type', c.c_uint8, 1),
+            ('secondary_header', c.c_uint8, 1),
+            ('apid', c.c_uint16, 11),
+            ('sequence_grouping', c.c_uint8, 2),
+            ('sequence_number', c.c_uint16, 14),
+            ('length_minus1', c.c_uint16),
+            ('data', c.c_uint8 * data_size)
+        ]
+    pkt = PktImpl.from_buffer(buf, offset)
+    assert pkt.length_minus1 + 1 == data_size
+    return pkt
+
+
+Packet = namedtuple('Packet', ('tracker', 'packet'))
+
+
+class CommonRdr(namedtuple('CommonRdr', ('buf', 'header', 'apids'))):
+
+    def packets_for_apid(self, apid):
+        """
+        Return a generator that yields tuples of (tracker, packet) for the
+        given `Apid`.
+        """
+        return _packets_for_apid(self.buf, self.header, apid)
+
+    def packets(self):
+        """
+        Return a generator that yields `Packet`s for each apid in the
+        apid list.
+        """
+        for apid in self.apids:
+            for tracker, packet in self.packets_for_apid(apid):
+                yield Packet(tracker, packet)
+
+
+def _packets_for_apid(buf, header, apid):
+    t_off = header.pkt_tracker_offset + apid.pkt_tracker_start_idx * c.sizeof(PacketTracker)
+    for idx in range(apid.pkts_received):
+        tracker = PacketTracker.from_buffer(buf, t_off)
+        t_off += c.sizeof(PacketTracker)
+        if tracker.offset < 0:  # -1 == missing
+            continue
+
+        p_off = header.ap_storage_offset + tracker.offset
+        # packet = buf[p_off:p_off+tracker.size]
+        yield tracker, _make_packet(buf, tracker.size, p_off)
+
+
+def _read_apid_list(header, buf):
+    """
+    Return a generator that yields `Apid`s
+    """
+    start = header.apid_list_offset
+    end = header.num_apids * c.sizeof(Apid)
+    for offset in range(start, end, c.sizeof(Apid)):
+        yield Apid.from_buffer(buf, offset)
+
+
+def read_common_rdrs(sensor, filepath):
+    """
+    Return a generator that yields `CommonRdr` for each dataset provided by
+    `read_rdr_datasets`.
+    """
+    for buf in read_rdr_datasets(sensor, filepath):
+        header = StaticHeader.from_buffer(buf)
+        apids = _read_apid_list(header, buf)
+        yield CommonRdr(buf, header, list(apids))
+
+
+def read_rdr_datasets(sensor, filepath):
+    """
+    Return a generator that yields bytearrays for each RawApplicationPackets
+    dataset in numerical order.
+    """
+    sensor = sensor.upper()
+    fobj = H5File(filepath)
+    grp = fobj['/All_Data/{}-SCIENCE-RDR_All'.format(sensor)]
+
+    dsnames = [k for k in grp.keys() if k.startswith('RawApplicationPackets_')]
+    # compare ds names by index after '_'
+    cmp_names = lambda *tup: cmp(*(int(x.split('_')[-1]) for x in tup))  # noqa
+    for name in sorted(dsnames, cmp=cmp_names):
+        ds = grp[name]
+        yield np.getbuffer(np.array(ds))
+
+
+def sort_packets_by_obs_time(packets):
+    """
+    Sort `Packet`s in-place by the PacketTracker obs_time (IET).
+    """
+    return sorted(packets, key=lambda p: p.tracker.obs_time)
+
+
+def sort_packets_by_apid(packets, order=None):
+    """
+    Sort packets in-place by apid. An error will be raised if `order` is given
+    and packet has an apid not in the list.
+    """
+    if order:
+        return sorted(packets, key=lambda p: order.index(p.packet.apid))
+    else:
+        return sorted(packets, key=lambda p: p.packet.apid)
+
+
+def sort_packets_edos(rdr, packets):
+    """
+    Sort packets by timestamp and order the apids the same as EDOS NASA L0
+    files.
+    """
+    order = None
+    if rdr.header.sensor.lower() == 'viirs':
+        order = [826, 821] + range(0, 825)
+    return sort_packets_by_obs_time(sort_packets_by_apid(packets, order=order))
+
+
+def convert_to_nasa_l0(sensor, filename, skipfill=False, outfn=None):
+    """
+    Convert a JSPP RDR to a NASA L0 PDS file.
+    """
+    outfn = outfn or '{}.pds'.format(filename)
+    with open(outfn, 'wb') as fptr:
+        for idx, rdr in enumerate(read_common_rdrs(sensor, filename)):
+            packets = sort_packets_edos(rdr, rdr.packets())
+            for packet in packets:
+                if packet.tracker.fill_percent != 0:
+                    LOG.debug('fill: %s', packet.tracker)
+                    if skipfill:
+                        continue
+                fptr.write(packet.packet)
+    return outfn
+
+
+if __name__ == '__main__':
+    import argparse
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-o', '--output')
+    parser.add_argument('-f', '--skipfill', action='store_true')
+    parser.add_argument('sensor', choices=('viirs', 'atms', 'cris'))
+    parser.add_argument('rdr')
+    args = parser.parse_args()
+
+    logging.basicConfig(level=logging.DEBUG)
+    convert_to_nasa_l0(args.sensor, args.rdr, outfn=args.output)
-- 
GitLab