Repository: openradar/xradar

Issue:
NEXRADLevel2 Reader Error for Older Files
* xradar version: 0.8.0
* Python version: 3.12.2
* Operating System: Windows 11

### Description

I was loading in some radar data from KLIX on 8/28/2005 at 1800z using the NEXRADLevel2 engine for xarray that xradar provides and ran into this error. Seems to only happen with the pre-super resolution radars -- I haven't encountered any issues with the modern sites. 

### What I Did

```py
data = xr.open_dataset(r"C:\Users\deela\Downloads\radar" + f + ".gz", engine = 'nexradlevel2', group = f'sweep_{str(g).zfill(2)}')
```

Traceback
```
Traceback (most recent call last):
  File "C:\Users\deela\.anaconda\cyclobot\getRadar.py", line 155, in <module>
    print(getPastData('klix', '2005', '08', '28', '1800'))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\deela\.anaconda\cyclobot\getRadar.py", line 139, in getPastData
    data = xr.open_dataset(r"C:\Users\deela\Downloads\radar" + f + ".gz", engine = 'nexradlevel2', group = f'sweep_{str(g).zfill(2)}')
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xarray\backends\api.py", line 670, in open_dataset
    backend_ds = backend.open_dataset(
                 ^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xradar\io\backends\nexrad_level2.py", line 1447, in open_dataset
    ds = store_entrypoint.open_dataset(
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xarray\backends\store.py", line 44, in open_dataset
    vars, attrs = filename_or_obj.load()
                  ^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xarray\backends\common.py", line 305, in load
    (_decode_variable_name(k), v) for k, v in self.get_variables().items()
                                              ^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xradar\io\backends\nexrad_level2.py", line 1401, in get_variables
    for k, v in self.ds["sweep_data"].items()
                ^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xradar\io\backends\nexrad_level2.py", line 1321, in ds
    return self._acquire()
           ^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xradar\io\backends\nexrad_level2.py", line 1315, in _acquire
    root.get_sweep(self._group)
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xradar\io\backends\nexrad_level2.py", line 490, in get_sweep
    moments = self.msg_31_data_header[sweep_number]["msg_31_data_header"].keys()
              ^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xradar\io\backends\nexrad_level2.py", line 465, in msg_31_data_header
    ) = self.get_data_header()
        ^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xradar\io\backends\nexrad_level2.py", line 627, in get_data_header
    while self.init_next_record():
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\deela\anaconda3\envs\Env2272024\Lib\site-packages\xradar\io\backends\nexrad_level2.py", line 364, in init_next_record
    return self.init_record(self.record_number + 1)
                            ~~~~~~~~~~~~~~~~~~~^~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/openradar/xradar/pull/267?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) Report
Attention: Patch coverage is `78.68852%` with `13 lines` in your changes missing coverage. Please review.
> Project coverage is 92.93%. Comparing base [(`f5f1f6f`)](https://app.codecov.io/gh/openradar/xradar/commit/f5f1f6f713f6271f9de5007112ea1ba5dc406d67?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) to head [(`5555ad5`)](https://app.codecov.io/gh/openradar/xradar/commit/5555ad55774bb770e77cac74e588d5607bc982f6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar).

| [Files with missing lines](https://app.codecov.io/gh/openradar/xradar/pull/267?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) | Patch % | Lines |
|---|---|---|
| [xradar/io/backends/nexrad\_level2.py](https://app.codecov.io/gh/openradar/xradar/pull/267?src=pr&el=tree&filepath=xradar%2Fio%2Fbackends%2Fnexrad_level2.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar#diff-eHJhZGFyL2lvL2JhY2tlbmRzL25leHJhZF9sZXZlbDIucHk=) | 78.68% | [13 Missing :warning: ](https://app.codecov.io/gh/openradar/xradar/pull/267?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #267      +/-   ##
==========================================
- Coverage   93.57%   92.93%   -0.64%     
==========================================
  Files          26       26              
  Lines        5042     5068      +26     
==========================================
- Hits         4718     4710       -8     
- Misses        324      358      +34     
```

| [Flag](https://app.codecov.io/gh/openradar/xradar/pull/267/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) | Coverage Δ | |
|---|---|---|
| [notebooktests](https://app.codecov.io/gh/openradar/xradar/pull/267/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) | `?` | |
| [unittests](https://app.codecov.io/gh/openradar/xradar/pull/267/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar) | `92.93% <78.68%> (-0.23%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/openradar/xradar/pull/267?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openradar).


Patch:
diff --git a/docs/history.md b/docs/history.md
index 8f2a6ee..1946f0a 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -11,6 +11,7 @@
 * FIX: DataTree reader now works with sweeps containing different variables ({pull}`252`) by [@egouden](https://github.com/egouden).
 * FIX: Correct retrieval of intermediate records in nexrad level2 reader ({issue}`259`) ({pull}`261`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
 * FIX: Test for magic number BZhX1AY&SY (where X is any number between 0..9) when retrieving BZ2 record indices in nexrad level2 reader ({issue}`264`) ({pull}`266`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
+* ENH: Add message type 1 decoding to nexrad level 2 reader ({issue}`256`) ({pull}`267`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
 
 ## 0.8.0 (2024-11-04)
 
diff --git a/xradar/io/backends/nexrad_level2.py b/xradar/io/backends/nexrad_level2.py
index c036f83..27c1485 100644
--- a/xradar/io/backends/nexrad_level2.py
+++ b/xradar/io/backends/nexrad_level2.py
@@ -37,7 +37,7 @@ __doc__ = __doc__.format("\n   ".join(__all__))
 
 import bz2
 import struct
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
 
 import numpy as np
 import xarray as xr
@@ -519,35 +519,35 @@ class NEXRADLevel2File(NEXRADRecordFile):
     def get_metadata_header(self):
         """Get metadaata header"""
         # data offsets
-        # ICD 2620010E
+        # ICD 2620010J
         # 7.3.5 Metadata Record
-        meta_offsets = dict(
-            msg_15=(0, 77),
-            msg_13=(77, 126),
-            msg_18=(126, 131),
-            msg_3=(131, 132),
-            msg_5=(132, 133),
-            msg_2=(133, 134),
-        )
-        meta_headers = {}
-        for msg, (ms, me) in meta_offsets.items():
-            mheader = []
-            for rec in np.arange(ms, me):
-                self.init_record(rec)
-                filepos = self.filepos
-                message_header = self.get_message_header()
-                # do not read zero blocks of data
-                if message_header["size"] == 0:
-                    break
-                message_header["record_number"] = self.record_number
-                message_header["filepos"] = filepos
-                mheader.append(message_header)
-            meta_headers[msg] = mheader
+        # the above document will evolve over time
+        # please revisit and adopt accordingly
+        meta_headers = defaultdict(list)
+        rec = 0
+        # iterate over alle messages until type outside [2, 3, 5, 13, 15, 18, 32]
+        while True:
+            self.init_record(rec)
+            rec += 1
+            filepos = self.filepos
+            message_header = self.get_message_header()
+            # do not read zero blocks of data
+            if (mtype := message_header["type"]) == 0:
+                continue
+            # stop if first non meta header is found
+            if mtype not in [2, 3, 5, 13, 15, 18, 32]:
+                break
+            message_header["record_number"] = self.record_number
+            message_header["filepos"] = filepos
+            meta_headers[f"msg_{mtype}"].append(message_header)
         return meta_headers
 
     def get_msg_5_data(self):
         """Get MSG5 data."""
-        self.init_record(132)
+        # get the record number from the meta header
+        recnum = self.meta_header["msg_5"][0]["record_number"]
+        self.init_record(recnum)
+        # skip header
         self.rh.pos += LEN_MSG_HEADER + 12
         # unpack msg header
         msg_5 = _unpack_dictionary(
@@ -613,7 +613,11 @@ class NEXRADLevel2File(NEXRADRecordFile):
 
     def get_data_header(self):
         """Load all data header from file."""
-        self.init_record(133)
+        # get the record number from the meta header
+        # message 2 is the last meta header, after which the
+        # data records are located
+        recnum = self.meta_header["msg_2"][0]["record_number"]
+        self.init_record(recnum)
         current_sweep = -1
         current_header = -1
         sweep_msg_31_header = []
@@ -634,12 +638,15 @@ class NEXRADLevel2File(NEXRADRecordFile):
 
             # keep all data headers
             data_header.append(msg_header)
-            # only check type 31 for now
-            if msg_header["type"] == 31:
+            # check which message type we have (1 or 31)
+            if (msg_type := msg_header["type"]) in [1, 31]:
+                msg_len, msg = {1: (LEN_MSG_1, MSG_1), 31: (LEN_MSG_31, MSG_31)}[
+                    msg_type
+                ]
                 # get msg_31_header
                 msg_31_header = _unpack_dictionary(
-                    self._rh.read(LEN_MSG_31, width=1),
-                    MSG_31,
+                    self._rh.read(msg_len, width=1),
+                    msg,
                     self._rawdata,
                     byte_order=">",
                 )
@@ -671,42 +678,101 @@ class NEXRADLevel2File(NEXRADRecordFile):
                     sweep["record_number"] = self.rh.recnum
                     sweep["filepos"] = self.filepos
                     sweep["msg_31_header"] = msg_31_header
-                    block_pointers = [
-                        v
-                        for k, v in msg_31_header.items()
-                        if k.startswith("block_pointer") and v > 0
-                    ]
-                    block_header = {}
-                    for i, block_pointer in enumerate(
-                        block_pointers[: msg_31_header["block_count"]]
-                    ):
-                        self.rh.pos = block_pointer + 12 + LEN_MSG_HEADER
-                        buf = self._rh.read(4, width=1)
-                        dheader = _unpack_dictionary(
-                            buf,
-                            DATA_BLOCK_HEADER,
-                            self._rawdata,
-                            byte_order=">",
-                        )
-                        block = DATA_BLOCK_TYPE_IDENTIFIER[dheader["block_type"]][
-                            dheader["data_name"]
+                    # add msg_type for later reconstruction
+                    sweep["msg_type"] = msg_type
+                    # new message 31 data
+                    if msg_type == 31:
+                        block_pointers = [
+                            v
+                            for k, v in msg_31_header.items()
+                            if k.startswith("block_pointer") and v > 0
                         ]
-                        LEN_BLOCK = struct.calcsize(
-                            _get_fmt_string(block, byte_order=">")
-                        )
-                        block_header[dheader["data_name"]] = _unpack_dictionary(
-                            self._rh.read(LEN_BLOCK, width=1),
-                            block,
-                            self._rawdata,
-                            byte_order=">",
+                        block_header = {}
+                        for i, block_pointer in enumerate(
+                            block_pointers[: msg_31_header["block_count"]]
+                        ):
+                            self.rh.pos = block_pointer + 12 + LEN_MSG_HEADER
+                            buf = self._rh.read(4, width=1)
+                            dheader = _unpack_dictionary(
+                                buf,
+                                DATA_BLOCK_HEADER,
+                                self._rawdata,
+                                byte_order=">",
+                            )
+                            block = DATA_BLOCK_TYPE_IDENTIFIER[dheader["block_type"]][
+                                dheader["data_name"]
+                            ]
+                            LEN_BLOCK = struct.calcsize(
+                                _get_fmt_string(block, byte_order=">")
+                            )
+                            block_header[dheader["data_name"]] = _unpack_dictionary(
+                                self._rh.read(LEN_BLOCK, width=1),
+                                block,
+                                self._rawdata,
+                                byte_order=">",
+                            )
+                            if dheader["block_type"] == "D":
+                                block_header[dheader["data_name"]][
+                                    "data_offset"
+                                ] = self.rh.pos
+
+                        sweep["msg_31_data_header"] = block_header
+                        _msg_31_data_header.append(sweep)
+                    # former message 1 data
+                    elif msg_type == 1:
+                        # creating block_pointers
+                        # data_offset is from start of message so need to
+                        # add LEN_MSG_HEADER + 12
+                        block_pointers = {
+                            "REF": dict(
+                                ngates=msg_31_header["sur_nbins"],
+                                gate_spacing=msg_31_header["sur_range_step"],
+                                first_gate=msg_31_header["sur_range_first"],
+                                word_size=8,
+                                scale=2.0,
+                                offset=66.0,
+                                data_offset=(
+                                    msg_31_header["sur_pointer"] + LEN_MSG_HEADER + 12
+                                ),
+                            ),
+                            "VEL": dict(
+                                ngates=msg_31_header["doppler_nbins"],
+                                gate_spacing=msg_31_header["doppler_range_step"],
+                                first_gate=msg_31_header["doppler_range_first"],
+                                word_size=8,
+                                scale={2: 2.0, 4: 1.0, 0: 0.0}[
+                                    msg_31_header["doppler_resolution"]
+                                ],
+                                offset=129.0,
+                                data_offset=(
+                                    msg_31_header["vel_pointer"] + LEN_MSG_HEADER + 12
+                                ),
+                            ),
+                            "SW": dict(
+                                ngates=msg_31_header["doppler_nbins"],
+                                gate_spacing=msg_31_header["doppler_range_step"],
+                                first_gate=msg_31_header["doppler_range_first"],
+                                word_size=8,
+                                scale=2.0,
+                                offset=192.0,
+                                data_offset=(
+                                    msg_31_header["width_pointer"] + LEN_MSG_HEADER + 12
+                                ),
+                            ),
+                        }
+                        block_header = {}
+                        block_header["VOL"] = dict(
+                            lat=0,
+                            lon=0,
+                            height=0,
+                            feedhorn_height=0,
+                            vcp=msg_31_header.pop("vcp"),
                         )
-                        if dheader["block_type"] == "D":
-                            block_header[dheader["data_name"]][
-                                "data_offset"
-                            ] = self.rh.pos
-
-                    sweep["msg_31_data_header"] = block_header
-                    _msg_31_data_header.append(sweep)
+                        for k, v in block_pointers.items():
+                            if v["ngates"]:
+                                block_header[k] = v
+                        sweep["msg_31_data_header"] = block_header
+                        _msg_31_data_header.append(sweep)
                 sweep_msg_31_header.append(msg_31_header)
             else:
                 sweep_intermediate_records.append(msg_header)
@@ -1261,9 +1327,13 @@ class NexradLevel2ArrayWrapper(BackendArray):
         # read the data if not available
         try:
             data = self.datastore.ds["sweep_data"][self.name]["data"]
+            print("ZZZZZAA:", self.name, data, key)
         except KeyError:
+            print("XXXXX:", self.group, self.name)
             self.datastore.root.get_data(self.group, self.name)
             data = self.datastore.ds["sweep_data"][self.name]["data"]
+            print("ZZZZZBB:", self.name, data, key)
+            print("YY0:", self.name, len(data), len(data[0]))
         # see 3.2.4.17.6 Table XVII-I Data Moment Characteristics and Conversion for Data Names
         word_size = self.datastore.ds["sweep_data"][self.name]["word_size"]
         if self.name == "PHI" and word_size == 16:
@@ -1274,6 +1344,7 @@ class NexradLevel2ArrayWrapper(BackendArray):
             x = np.uint16(0x7FF)
         else:
             x = np.uint8(0xFF)
+        print("YY1:", self.name, len(data[0]), self.shape)
         if len(data[0]) < self.shape[1]:
             return np.pad(
                 np.vstack(data) & x,
@@ -1353,10 +1424,18 @@ class NexradLevel2Store(AbstractDataStore):
 
     def open_store_coordinates(self):
         msg_31_header = self.root.msg_31_header[self._group]
-
+        msg_31_data_header = self.root.msg_31_data_header[self._group]
+        # check message type
+        msg_type = msg_31_data_header["msg_type"]
+        if msg_type == 1:
+            angle_scale = 180 / (4096 * 8.0)
+        else:
+            angle_scale = 1.0
         # azimuth/elevation
-        azimuth = np.array([ms["azimuth_angle"] for ms in msg_31_header])
-        elevation = np.array([ms["elevation_angle"] for ms in msg_31_header])
+        azimuth = np.array([ms["azimuth_angle"] for ms in msg_31_header]) * angle_scale
+        elevation = (
+            np.array([ms["elevation_angle"] for ms in msg_31_header]) * angle_scale
+        )
 
         # time
         # date is 1-based (1 -> 1970-01-01T00:00:00), so we need to subtract 1
@@ -1386,7 +1465,16 @@ class NexradLevel2Store(AbstractDataStore):
         prt_mode = "not_set"
         follow_mode = "not_set"
 
-        fixed_angle = self.root.msg_5["elevation_data"][self._group]["elevation_angle"]
+        elev_data = self.root.msg_5["elevation_data"]
+        # in some cases msg_5 doesn't contain meaningful data
+        # we extract the needed values from the data header instead
+        if elev_data:
+            fixed_angle = self.root.msg_5["elevation_data"][self._group][
+                "elevation_angle"
+            ]
+        else:
+            fixed_angle = self.root.msg_31_header[self._group][0]["elevation_angle"]
+        fixed_angle *= angle_scale
 
         coords = {
             "azimuth": Variable((dim,), azimuth, get_azimuth_attrs(), encoding),
@@ -1617,9 +1705,13 @@ def open_nexradlevel2_datatree(
     else:
         with NEXRADLevel2File(filename_or_obj, loaddata=False) as nex:
             nsweeps = nex.msg_5["number_elevation_cuts"]
-            # Check if duplicated sweeps ("split cut mode")
             n_sweeps = len(nex.msg_31_data_header)
-            if nsweeps > n_sweeps:
+            # check for zero (old files)
+            if nsweeps == 0:
+                nsweeps = n_sweeps
+                comment = "No message 5 information available"
+            # Check if duplicated sweeps ("split cut mode")
+            elif nsweeps > n_sweeps:
                 nsweeps = n_sweeps
                 comment = "Split Cut Mode scanning strategy"
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 012d043..c1590f9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -77,6 +77,21 @@ def nexradlevel2_file():
     return DATASETS.fetch("KATX20130717_195021_V06")
 
 
+@pytest.fixture(scope="session")
+def nexradlevel2_msg1_file(tmp_path_factory):
+    fnamei = DATASETS.fetch("KLIX20050828_180149.gz")
+    fnameo = os.path.join(
+        tmp_path_factory.mktemp("data"), f"{os.path.basename(fnamei)[:-3]}_gz"
+    )
+    import gzip
+    import shutil
+
+    with gzip.open(fnamei) as fin:
+        with open(fnameo, "wb") as fout:
+            shutil.copyfileobj(fin, fout)
+    return fnameo
+
+
 @pytest.fixture(scope="session")
 def nexradlevel2_gzfile(tmp_path_factory):
     fnamei = DATASETS.fetch("KLBB20160601_150025_V06.gz")
diff --git a/tests/io/test_nexrad_level2.py b/tests/io/test_nexrad_level2.py
index 942167f..c30e538 100644
--- a/tests/io/test_nexrad_level2.py
+++ b/tests/io/test_nexrad_level2.py
@@ -424,6 +424,120 @@ def test_open_nexradlevel2_file(nexradlevel2_files):
             assert len(head) == msg_31_header_length[i]
 
 
+def test_open_nexradlevel2_msg1_file(nexradlevel2_msg1_file):
+    with NEXRADLevel2File(nexradlevel2_msg1_file) as fh:
+
+        # volume header
+        assert fh.volume_header["tape"] == b"AR2V0001."
+        assert fh.volume_header["extension"] == b"201"
+        assert fh.volume_header["date"] == 3761373184
+        assert fh.volume_header["time"] == 3362708995
+        assert fh.volume_header["icao"] == b"KLIX"
+
+        # meta_header 15
+        assert len(fh.meta_header["msg_15"]) == 62
+        assert fh.meta_header["msg_15"][0]["size"] == 1208
+        assert fh.meta_header["msg_15"][0]["channels"] == 0
+        assert fh.meta_header["msg_15"][0]["type"] == 15
+        assert fh.meta_header["msg_15"][0]["seq_id"] == 819
+        assert fh.meta_header["msg_15"][0]["date"] == 13024
+        assert fh.meta_header["msg_15"][0]["ms"] == 51522855
+        assert fh.meta_header["msg_15"][0]["segments"] == 14
+        assert fh.meta_header["msg_15"][0]["seg_num"] == 1
+        assert fh.meta_header["msg_15"][0]["record_number"] == 0
+        # meta_header 13
+        assert len(fh.meta_header["msg_13"]) == 48
+        assert fh.meta_header["msg_13"][0]["size"] == 1208
+        assert fh.meta_header["msg_13"][0]["channels"] == 0
+        assert fh.meta_header["msg_13"][0]["type"] == 13
+        assert fh.meta_header["msg_13"][0]["seq_id"] == 0
+        assert fh.meta_header["msg_13"][0]["date"] == 13023
+        assert fh.meta_header["msg_13"][0]["ms"] == 43397314
+        assert fh.meta_header["msg_13"][0]["segments"] == 14
+        assert fh.meta_header["msg_13"][0]["seg_num"] == 1
+        assert fh.meta_header["msg_13"][0]["record_number"] == 62
+        # meta header 18
+        assert len(fh.meta_header["msg_18"]) == 4
+        assert fh.meta_header["msg_18"][0]["size"] == 1208
+        assert fh.meta_header["msg_18"][0]["channels"] == 0
+        assert fh.meta_header["msg_18"][0]["type"] == 18
+        assert fh.meta_header["msg_18"][0]["seq_id"] == 0
+        assert fh.meta_header["msg_18"][0]["date"] == 0
+        assert fh.meta_header["msg_18"][0]["ms"] == 0
+        assert fh.meta_header["msg_18"][0]["segments"] == 4
+        assert fh.meta_header["msg_18"][0]["seg_num"] == 1
+        assert fh.meta_header["msg_18"][0]["record_number"] == 110
+        # meta header 3
+        assert len(fh.meta_header["msg_3"]) == 1
+        assert fh.meta_header["msg_3"][0]["size"] == 528
+        assert fh.meta_header["msg_3"][0]["channels"] == 0
+        assert fh.meta_header["msg_3"][0]["type"] == 3
+        assert fh.meta_header["msg_3"][0]["seq_id"] == 5459
+        assert fh.meta_header["msg_3"][0]["date"] == 13024
+        assert fh.meta_header["msg_3"][0]["ms"] == 61897431
+        assert fh.meta_header["msg_3"][0]["segments"] == 1
+        assert fh.meta_header["msg_3"][0]["seg_num"] == 1
+        assert fh.meta_header["msg_3"][0]["record_number"] == 114
+        # meta header 5
+        assert len(fh.meta_header["msg_5"]) == 1
+        assert fh.meta_header["msg_5"][0]["size"] == 1208
+        assert fh.meta_header["msg_5"][0]["channels"] == 0
+        assert fh.meta_header["msg_5"][0]["type"] == 5
+        assert fh.meta_header["msg_5"][0]["seq_id"] == 0
+        assert fh.meta_header["msg_5"][0]["date"] == 0
+        assert fh.meta_header["msg_5"][0]["ms"] == 0
+        assert fh.meta_header["msg_5"][0]["segments"] == 1
+        assert fh.meta_header["msg_5"][0]["seg_num"] == 1
+        assert fh.meta_header["msg_5"][0]["record_number"] == 115
+        assert fh.msg_5 == OrderedDict(
+            [
+                ("message_size", 0),
+                ("pattern_type", 0),
+                ("pattern_number", 0),
+                ("number_elevation_cuts", 0),
+                ("clutter_map_group_number", 0),
+                ("doppler_velocity_resolution", 0),
+                ("pulse_width", 0),
+                ("elevation_data", []),
+            ]
+        )
+
+        # meta header 2
+        assert len(fh.meta_header["msg_2"]) == 1
+        assert fh.meta_header["msg_2"][0]["size"] == 48
+        assert fh.meta_header["msg_2"][0]["channels"] == 0
+        assert fh.meta_header["msg_2"][0]["type"] == 2
+        assert fh.meta_header["msg_2"][0]["seq_id"] == 29176
+        assert fh.meta_header["msg_2"][0]["date"] == 13024
+        assert fh.meta_header["msg_2"][0]["ms"] == 64889226
+        assert fh.meta_header["msg_2"][0]["segments"] == 1
+        assert fh.meta_header["msg_2"][0]["seg_num"] == 1
+        assert fh.meta_header["msg_2"][0]["record_number"] == 116
+
+        # data header
+        assert len(fh.data_header) == 5856
+        msg_31_header_length = [
+            367,
+            367,
+            367,
+            367,
+            367,
+            367,
+            367,
+            367,
+            366,
+            367,
+            366,
+            366,
+            365,
+            364,
+            363,
+            362,
+        ]
+        for i, head in enumerate(fh.msg_31_header):
+            assert len(head) == msg_31_header_length[i]
+
+
 def test_open_nexradlevel2_datatree(nexradlevel2_file):
     # Define kwargs to pass into the function
     kwargs = {
@@ -485,6 +599,64 @@ def test_open_nexradlevel2_datatree(nexradlevel2_file):
     assert dtree.attrs["scan_name"] == "VCP-11"
 
 
+def test_open_nexradlevel2_msg1_datatree(nexradlevel2_msg1_file):
+    # Define kwargs to pass into the function
+    kwargs = {
+        "sweep": [0, 1, 2, 5, 7],  # Test with specific sweeps
+        "first_dim": "auto",
+        "reindex_angle": {
+            "start_angle": 0.0,
+            "stop_angle": 360.0,
+            "angle_res": 1.0,
+            "direction": 1,  # Set a valid direction within reindex_angle
+        },
+        "fix_second_angle": True,
+        "site_coords": True,
+    }
+
+    # Call the function with an actual NEXRAD Level 2 file
+    dtree = open_nexradlevel2_datatree(nexradlevel2_msg1_file, **kwargs)
+
+    # Assertions
+    assert isinstance(dtree, DataTree), "Expected a DataTree instance"
+    assert "/" in dtree.subtree, "Root group should be present in the DataTree"
+    assert (
+        "/radar_parameters" in dtree.subtree
+    ), "Radar parameters group should be in the DataTree"
+    assert (
+        "/georeferencing_correction" in dtree.subtree
+    ), "Georeferencing correction group should be in the DataTree"
+    assert (
+        "/radar_calibration" in dtree.subtree
+    ), "Radar calibration group should be in the DataTree"
+
+    # Check if at least one sweep group is attached (e.g., "/sweep_0")
+    sweep_groups = [key for key in dtree.match("sweep_*")]
+    assert len(sweep_groups) == 5, "Expected at least one sweep group in the DataTree"
+
+    # Verify a sample variable in one of the sweep groups (adjust as needed based on expected variables)
+    sample_sweep = sweep_groups[0]
+    assert len(dtree[sample_sweep].data_vars) == 6
+    assert (
+        "DBZH" in dtree[sample_sweep].data_vars
+    ), f"DBZH should be a data variable in {sample_sweep}"
+    assert dtree[sample_sweep]["DBZH"].shape == (360, 460)
+    # Validate coordinates are attached correctly
+    assert (
+        "latitude" in dtree[sample_sweep]
+    ), "Latitude should be attached to the root dataset"
+    assert (
+        "longitude" in dtree[sample_sweep]
+    ), "Longitude should be attached to the root dataset"
+    assert (
+        "altitude" in dtree[sample_sweep]
+    ), "Altitude should be attached to the root dataset"
+
+    assert len(dtree.attrs) == 10
+    assert dtree.attrs["instrument_name"] == "KLIX"
+    assert dtree.attrs["scan_name"] == "VCP-0"
+
+
 @pytest.mark.parametrize(
     "sweeps_input, expected_sweeps, should_raise",
     [