diff --git a/aosstower/frame.py b/aosstower/frame.py
new file mode 100644
index 0000000000000000000000000000000000000000..0878fd2e6f26a31f052e4cc4aa8947f1db0a592a
--- /dev/null
+++ b/aosstower/frame.py
@@ -0,0 +1,10 @@
+from aosstower import station
+
+
+class Frame(dict):
+
+    def __init__(self, width=station.DATA_INTERVAL):
+        self.width = width
+
+    def __getattr__(self, name, default=None):
+        return self.get(name, default)
diff --git a/aosstower/l00/parser.py b/aosstower/l00/parser.py
index bded52ab5f7d414ea229b08438455079c35f6a54..c90d41ed1c6bd76a3cc75e9c81cf030d2d0dd99c 100644
--- a/aosstower/l00/parser.py
+++ b/aosstower/l00/parser.py
@@ -37,15 +37,14 @@ import logging
 from datetime import datetime, timedelta
 
 from metobs import data as d
-
-from aosstower import station
 from aosstower.schema import database
+from aosstower.frame import Frame
 
 LOG = logging.getLogger(__name__)
 
 
 class LineParseError(Exception):
-    """Error parsing line of record data.
+    """Error parsing line of frame data.
     """
     @classmethod
     def raise_wrapped(cls, exception, msg=None):
@@ -54,32 +53,25 @@ class LineParseError(Exception):
         msg = msg or str(exception)
         raise cls(msg), None, traceback
 
-    def add_winds(self):
-        east, north, spd = d.wind_vector_components(float(self['wind_speed']),
-                                                    float(self['wind_dir']))
-
-        self['winddir_east'] = '%.3d' % east
-        self['winddir_north'] = '%.3d' % north
-        self['wind_speed'] = '%.3d' % spd
-
-    def add_altimeter(self, elev=station.ELEVATION):
-        self['altimeter'] = '%.3d' % d.altimeter(float(self['pressure']), elev)
 
-    def add_dewpoint(self):
-        self['dewpoint'] = '%.3d' % d.dewpoint(float(self['air_temp']),
-                                               float(self['rh']))
-
-
-def _make_record(data):
-    for key in data:
+def _make_frame(data):
+    """Construct a frame from a list of tuples.
+    """
+    frame = Frame()
+    for key, value in data:
         if key == 'stamp':
-            continue
-        if key in database:
-            data[key] = database[key].type(data[key])
-    return data
+            frame[key] = value
+        elif key in database:
+            frame[key] = database[key].type(value)
+        else:
+            frame[key] = value
+
+    return frame
 
 
 class ParserV0(object):
+    """Parses Version 0 data lines.
+    """
 
     # maps v0 names to names in schema db
     names = {'ACCURAIN': 'accum_precip',
@@ -102,29 +94,30 @@ class ParserV0(object):
     def maybe_mine(line):
         return line.startswith('TIME')
 
-    def parse(self, line):
+    def make_frame(self, line):
         parts = line.split()
         if len(parts) != 32:
             raise LineParseError("Expected 32 components", line)
-        raw_data = {}
-        for k1, v1 in {k: v for k, v in zip(parts[0::2], parts[1::2])}.items():
+        raw_data = [('version', 0)]
+        for k1, v1 in zip(parts[0::2], parts[1::2]):
             if k1 == 'TIME':
                 continue
             if k1 in self.names:
-                raw_data[self.names[k1]] = v1
+                raw_data.append((self.names[k1], v1))
             else:
                 raise LineParseError("Unexpected var: %s" % k1, line)
-        raw_data['version'] = 0
         try:
             time_str = parts[1]
             unix_time = int(time_str)
-            raw_data['stamp'] = datetime.utcfromtimestamp(unix_time)
+            raw_data.append(('stamp', datetime.utcfromtimestamp(unix_time)))
         except (ValueError, TypeError):
             raise LineParseError("Could not parse stamp", line)
-        return _make_record(raw_data)
+        return _make_frame(raw_data)
 
 
 class ParserV1V2(object):
+    """Parses Version 1 & 2 data lines.
+    """
 
     names = ['station_id', 'year', 'doy', 'hhmm', 'sec', 'box_pressure',
              'paro_air_temp_period', 'paro_pressure_period', 'paro_air_temp',
@@ -138,32 +131,32 @@ class ParserV1V2(object):
     def maybe_mine(line):
         return re.search('^\d,\d{4},\d{1,3}', line) is not None
 
-    def _get_stamp(self, data):
-        year = int(data['year'])
-        doy = int(data['doy'])
+    def _get_stamp(self, parts):
+        year = int(parts[1])
+        doy = int(parts[2])
         dt = datetime.strptime('{:d}.{:03d}'.format(int(year), int(doy)), '%Y.%j')
-        secs = d.hhmm_to_offset(data['hhmm'])
-        secs += float(data['sec'])
+        secs = d.hhmm_to_offset(parts[3])
+        secs += float(parts[4])
         secs -= (secs % 5)
         dt += timedelta(seconds=secs)
         return dt
 
-    def parse(self, line):
+    def make_frame(self, line):
         parts = line.split(',')
-        if len(parts) < 28:
-            raise LineParseError("Expected >= 28 parts", line)
-        raw_data = {k: v for k, v in zip(self.names, parts)}
-        raw_data['version'] = 1 if len(parts) == 28 else 2
+        if len(parts) not in [28, 29]:
+            raise LineParseError("Expected 28 or 29 parts", line)
+        version = 1 if len(parts) == 28 else 2
+        raw_data = [('version', version)] + zip(self.names, parts)
         try:
-            raw_data['stamp'] = self._get_stamp(raw_data)
+            raw_data.append(('stamp', self._get_stamp(parts)))
         except (TypeError, ValueError):
             raise LineParseError("Could not parse timesamp", line)
-        return _make_record(raw_data)
+        return _make_frame(raw_data)
 
 
-def read_records(source, error_handler=lambda *a: None):
-    """Returns a generator for reading records from `source`. Records are
-    checked line-by-line so record line versions may be mixed.
+def read_frames(source, error_handler=lambda *a: None):
+    """Returns a generator for reading frames from `source`. Frames are
+    checked line-by-line so frame line versions may be mixed.
     """
     if hasattr(source, 'readlines'):
         fptr = source
diff --git a/aosstower/l00/rrd.py b/aosstower/l00/rrd.py
index fb396f777d7a44df508e01143ba72f1a91227431..6d257b9f90d2506cb2ae9837a700012a9b83ee21 100644
--- a/aosstower/l00/rrd.py
+++ b/aosstower/l00/rrd.py
@@ -6,7 +6,8 @@ from datetime import datetime, timedelta
 
 import rrdtool
 
-from metobs.data import to_unix_timestamp
+from metobs import data as d
+from aosstower import station
 
 
 # minimum set of records for the tower
@@ -16,13 +17,31 @@ VARS = {'air_temp', 'rh', 'dewpoint',
         'solar_flux', 'altimeter'}
 
 
+def add_vector_winds(record):
+    east, north, spd = d.wind_vector_components(float(record['wind_speed']),
+                                                float(record['wind_dir']))
+
+    record['winddir_east'] = '%.3d' % east
+    record['winddir_north'] = '%.3d' % north
+    record['wind_speed'] = '%.3d' % spd
+
+
+def add_altimeter(record, elev=station.ELEVATION):
+    record['altimeter'] = '%.3d' % d.altimeter(float(record['pressure']), elev)
+
+
+def add_dewpoint(record):
+    record['dewpoint'] = '%.3d' % d.dewpoint(float(record['air_temp']),
+                                             float(record['rh']))
+
+
 def initialize_rrd(filepath, start=None, days=365, data_interval=5):
     """Create a new empty RRD database.
     """
     assert not os.path.exists(filepath), "DB already exists"
     start = start or (datetime.utcnow() - timedelta(days=days))
     # normalize start to data interval
-    secs = to_unix_timestamp(start)
+    secs = d.to_unix_timestamp(start)
     secs -= secs % data_interval
 
     rrdtool.create(filepath,
diff --git a/aosstower/station.py b/aosstower/station.py
index 327a6fab87decdd4662de165c12341557820f82d..4197d403ed89b5a0a4fdc7cc2bfe1cb07326ca74 100644
--- a/aosstower/station.py
+++ b/aosstower/station.py
@@ -1,9 +1,10 @@
+from datetime import timedelta
 
-# Time between data samples in seconds 
-DATA_INTERVAL = 5
+# Time between data samples in seconds
+DATA_INTERVAL = timedelta(seconds=5)
 
 # station elevation in meters above the surface in feet
 ELEVATION = 325
 
 # Id of station from v1 records
-ID = 1 
+ID = 1
diff --git a/aosstower/tests/l00/test_parser.py b/aosstower/tests/l00/test_parser.py
index 1424f1061e3560ddcec8987a4243685e3bc5bf04..6e3dfa5b999e851ca35c24bc1cae0e0089a0d772 100644
--- a/aosstower/tests/l00/test_parser.py
+++ b/aosstower/tests/l00/test_parser.py
@@ -25,7 +25,7 @@ class ParserV0Tests(unittest.TestCase):
     def test_record_format(self):
         parser = self._cut()
 
-        record = parser.parse(self.line)
+        record = parser.make_frame(self.line)
 
         self.assertIn('stamp', record)
         self.assertEqual(record['stamp'], datetime(1970, 1, 1))
@@ -35,7 +35,7 @@ class ParserV1V2Tests(unittest.TestCase):
 
     line = ("1,1970,1,0000,0,976.59,5.8564,30.085,25.893,977.36,58732,"
             "47.375,24.234,23.865,22.615,37.219,6.9222,67.398,145.2,45.581,"
-            "22.669,10.417,145.2,22.665,163.94,0,0,30.015,29.89\n")
+            "22.669,10.417,145.2,22.665,163.94,0,0,30.015\n")
 
     def _cut(self):
         from aosstower.l00.parser import ParserV1V2
@@ -51,7 +51,7 @@ class ParserV1V2Tests(unittest.TestCase):
     def test_record_format(self):
         parser = self._cut()
 
-        record = parser.parse(self.line)
+        record = parser.make_frame(self.line)
 
         self.assertIn('stamp', record)
         self.assertEqual(record['stamp'], datetime(1970, 1, 1))
@@ -59,5 +59,5 @@ class ParserV1V2Tests(unittest.TestCase):
     def test_record_supports_v1_and_v2(self):
         parser = self._cut()
 
-        parser.parse(self.line)
-        parser.parse(self.line.strip() + ',999\n')
+        parser.make_frame(self.line)
+        parser.make_frame(self.line.strip() + ',999\n')