changeset 1:34a24b77b216

initial
author Radomir Dopieralski <sheep-devel@sheep.art.pl>
date Mon, 29 Dec 2014 23:18:47 +0100
parents a290783bf8d6
children 7ae82aedfde4
files beep.wav bell.wav chunk.py leg.py maestro.py pew.wav robot.py wave.py
diffstat 8 files changed, 777 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
Binary file beep.wav has changed
Binary file bell.wav has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chunk.py	Mon Dec 29 23:18:47 2014 +0100
@@ -0,0 +1,168 @@
+"""Simple class to read IFF chunks.
+
+An IFF chunk (used in formats such as AIFF, TIFF, RMFF (RealMedia File
+Format)) has the following structure:
+
++----------------+
+| ID (4 bytes)   |
++----------------+
+| size (4 bytes) |
++----------------+
+| data           |
+| ...            |
++----------------+
+
+The ID is a 4-byte string which identifies the type of chunk.
+
+The size field (a 32-bit value, encoded using big-endian byte order)
+gives the size of the whole chunk, including the 8-byte header.
+
+Usually an IFF-type file consists of one or more chunks.  The proposed
+usage of the Chunk class defined here is to instantiate an instance at
+the start of each chunk and read from the instance until it reaches
+the end, after which a new instance can be instantiated.  At the end
+of the file, creating a new instance will fail with a EOFError
+exception.
+
+Usage:
+while True:
+    try:
+        chunk = Chunk(file)
+    except EOFError:
+        break
+    chunktype = chunk.getname()
+    while True:
+        data = chunk.read(nbytes)
+        if not data:
+            pass
+        # do something with data
+
+The interface is file-like.  The implemented methods are:
+read, close, seek, tell, isatty.
+Extra methods are: skip() (called by close, skips to the end of the chunk),
+getname() (returns the name (ID) of the chunk)
+
+The __init__ method has one required argument, a file-like object
+(including a chunk instance), and one optional argument, a flag which
+specifies whether or not chunks are aligned on 2-byte boundaries.  The
+default is 1, i.e. aligned.
+"""
+
+class Chunk:
+    def __init__(self, file, align=True, bigendian=True, inclheader=False):
+        import struct
+        self.closed = False
+        self.align = align      # whether to align to word (2-byte) boundaries
+        if bigendian:
+            strflag = '>'
+        else:
+            strflag = '<'
+        self.file = file
+        self.chunkname = file.read(4)
+        if len(self.chunkname) < 4:
+            raise EOFError
+        try:
+            data = file.read(4)
+            self.chunksize = struct.unpack(strflag+'L', data)[0]
+        except:# struct.error:
+            raise EOFError
+        if inclheader:
+            self.chunksize = self.chunksize - 8 # subtract header
+        self.size_read = 0
+        try:
+            self.offset = self.file.tell()
+        except:
+            self.seekable = False
+        else:
+            self.seekable = True
+
+    def getname(self):
+        """Return the name (ID) of the current chunk."""
+        return self.chunkname
+
+    def getsize(self):
+        """Return the size of the current chunk."""
+        return self.chunksize
+
+    def close(self):
+        if not self.closed:
+            self.skip()
+            self.closed = True
+
+    def isatty(self):
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        return False
+
+    def seek(self, pos, whence=0):
+        """Seek to specified position into the chunk.
+        Default position is 0 (start of chunk).
+        If the file is not seekable, this will result in an error.
+        """
+
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        if not self.seekable:
+            raise OSError("cannot seek")
+        if whence == 1:
+            pos = pos + self.size_read
+        elif whence == 2:
+            pos = pos + self.chunksize
+        if pos < 0 or pos > self.chunksize:
+            raise RuntimeError
+        self.file.seek(self.offset + pos, 0)
+        self.size_read = pos
+
+    def tell(self):
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        return self.size_read
+
+    def read(self, size=-1):
+        """Read at most size bytes from the chunk.
+        If size is omitted or negative, read until the end
+        of the chunk.
+        """
+
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        if self.size_read >= self.chunksize:
+            return ''
+        if size < 0:
+            size = self.chunksize - self.size_read
+        if size > self.chunksize - self.size_read:
+            size = self.chunksize - self.size_read
+        data = self.file.read(size)
+        self.size_read = self.size_read + len(data)
+        if self.size_read == self.chunksize and \
+           self.align and \
+           (self.chunksize & 1):
+            dummy = self.file.read(1)
+            self.size_read = self.size_read + len(dummy)
+        return data
+
+    def skip(self):
+        """Skip the rest of the chunk.
+        If you are not interested in the contents of the chunk,
+        this method should be called so that the file points to
+        the start of the next chunk.
+        """
+
+        if self.closed:
+            raise ValueError("I/O operation on closed file")
+        if self.seekable:
+            try:
+                n = self.chunksize - self.size_read
+                # maybe fix alignment
+                if self.align and (self.chunksize & 1):
+                    n = n + 1
+                self.file.seek(n, 1)
+                self.size_read = self.size_read + n
+                return
+            except OSError:
+                pass
+        while self.size_read < self.chunksize:
+            n = min(8192, self.chunksize - self.size_read)
+            dummy = self.read(n)
+            if not dummy:
+                raise EOFError
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/leg.py	Mon Dec 29 23:18:47 2014 +0100
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+import math
+
+
+class Leg(object):
+    body = 36.0
+    coxa = 33.0
+    femur = 43.0
+    tibia = 53.0
+
+    def __init__(self, servos, angle):
+        angle = math.radians(angle)
+        self.servos = servos
+        self.angle = angle
+        self.base = (
+            self.body * math.cos(angle),
+            self.body * math.sin(angle)
+        )
+        self.home = (
+            (self.coxa + self.femur) * math.cos(angle),
+            (self.coxa + self.femur) * math.sin(angle),
+            -self.tibia,
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/maestro.py	Mon Dec 29 23:18:47 2014 +0100
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+
+
+class Maestro(object):
+    def __init__(self, serial):
+        self.serial = serial
+
+    def _write(self, values):
+        self.serial.send(''.join(chr(v) for v in values))
+
+    def _write_command(self, command):
+        self._write([int(command) | 128])
+
+    def _write_byte(self, byte):
+        self._write([int(byte) & 127])
+
+    def _write_word(self, word):
+        byte2, byte1 = divmod(int(word), 128)
+        self._write([byte1 & 127, byte2 & 127])
+
+    def _read(self, count):
+        values = [ord(c) for c in self.serial.recv(count)]
+        return values
+
+    def _read_byte(self):
+        return self._read(1)[0]
+
+    def _read_word(self):
+        byte1, byte2 = self._read(2)
+        return byte1 + byte2 * 256
+
+    def init(self):
+        """Initialize the communication."""
+        self._write_command(0xAA)
+
+    def set_target(self, servo, target):
+        """Set the servo position, in 1/4 of milliseconds of pulse width."""
+        self._write_command(0x84)
+        self._write_byte(servo)
+        self._write_word(target)
+
+    def set_speed(self, servo, speed):
+        """Set the servo speed limit."""
+        self._write_command(0x87)
+        self._write_byte(servo)
+        self._write_word(speed)
+
+    def set_accel(self, servo, accel):
+        """Set the servo acceleration limit."""
+        self._write_command(0x89)
+        self._write_byte(servo)
+        self._write_word(accel)
+
+    def get_position(self, servo):
+        """Read the current position of a servo."""
+        self._write_command(0x90)
+        self._write_byte(servo)
+        return self._read_word()
+
+    def get_moving_state(self):
+        """True if any servo is still moving, otherwise False."""
+        self._write_command(0x93)
+        return bool(self._read_byte())
Binary file pew.wav has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/robot.py	Mon Dec 29 23:18:47 2014 +0100
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+import pyb
+import chunk
+import wave
+import maestro
+
+dac = pyb.DAC(2)
+adc = pyb.ADC('Y11')
+uart = pyb.UART(3, 115200)
+servos = maestro.Maestro(uart)
+
+
+def get_battery():
+    return adc.read() * 0.0024169921874999997
+
+def play(filename):
+    with wave.open(filename) as f:
+        dac.write_timed(f.readframes(f.getnframes()), f.getframerate())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wave.py	Mon Dec 29 23:18:47 2014 +0100
@@ -0,0 +1,503 @@
+"""Stuff to parse WAVE files.
+
+Usage.
+
+Reading WAVE files:
+      f = wave.open(file, 'r')
+where file is either the name of a file or an open file pointer.
+The open file pointer must have methods read(), seek(), and close().
+When the setpos() and rewind() methods are not used, the seek()
+method is not  necessary.
+
+This returns an instance of a class with the following public methods:
+      getnchannels()  -- returns number of audio channels (1 for
+                         mono, 2 for stereo)
+      getsampwidth()  -- returns sample width in bytes
+      getframerate()  -- returns sampling frequency
+      getnframes()    -- returns number of audio frames
+      getcomptype()   -- returns compression type ('NONE' for linear samples)
+      getcompname()   -- returns human-readable version of
+                         compression type ('not compressed' linear samples)
+      getparams()     -- returns a namedtuple consisting of all of the
+                         above in the above order
+      getmarkers()    -- returns None (for compatibility with the
+                         aifc module)
+      getmark(id)     -- raises an error since the mark does not
+                         exist (for compatibility with the aifc module)
+      readframes(n)   -- returns at most n frames of audio
+      rewind()        -- rewind to the beginning of the audio stream
+      setpos(pos)     -- seek to the specified position
+      tell()          -- return the current position
+      close()         -- close the instance (make it unusable)
+The position returned by tell() and the position given to setpos()
+are compatible and have nothing to do with the actual position in the
+file.
+The close() method is called automatically when the class instance
+is destroyed.
+
+Writing WAVE files:
+      f = wave.open(file, 'w')
+where file is either the name of a file or an open file pointer.
+The open file pointer must have methods write(), tell(), seek(), and
+close().
+
+This returns an instance of a class with the following public methods:
+      setnchannels(n) -- set the number of channels
+      setsampwidth(n) -- set the sample width
+      setframerate(n) -- set the frame rate
+      setnframes(n)   -- set the number of frames
+      setcomptype(type, name)
+                      -- set the compression type and the
+                         human-readable compression type
+      setparams(tuple)
+                      -- set all parameters at once
+      tell()          -- return current position in output file
+      writeframesraw(data)
+                      -- write audio frames without pathing up the
+                         file header
+      writeframes(data)
+                      -- write audio frames and patch up the file header
+      close()         -- patch up the file header and close the
+                         output file
+You should set the parameters before the first writeframesraw or
+writeframes.  The total number of frames does not need to be set,
+but when it is set to the correct value, the header does not have to
+be patched up.
+It is best to first set all parameters, perhaps possibly the
+compression type, and then write audio frames using writeframesraw.
+When all frames have been written, either call writeframes('') or
+close() to patch up the sizes in the header.
+The close() method is called automatically when the class instance
+is destroyed.
+"""
+
+import builtins
+
+__all__ = ["open", "openfp", "Error"]
+
+class Error(Exception):
+    pass
+
+WAVE_FORMAT_PCM = 0x0001
+
+_array_fmts = None, 'b', 'h', None, 'i'
+
+#import audioop
+import struct
+import sys
+from chunk import Chunk
+from _collections import namedtuple
+
+_wave_params = namedtuple('_wave_params',
+                     'nchannels sampwidth framerate nframes comptype compname')
+
+class Wave_read:
+    """Variables used in this class:
+
+    These variables are available to the user though appropriate
+    methods of this class:
+    _file -- the open file with methods read(), close(), and seek()
+              set through the __init__() method
+    _nchannels -- the number of audio channels
+              available through the getnchannels() method
+    _nframes -- the number of audio frames
+              available through the getnframes() method
+    _sampwidth -- the number of bytes per audio sample
+              available through the getsampwidth() method
+    _framerate -- the sampling frequency
+              available through the getframerate() method
+    _comptype -- the AIFF-C compression type ('NONE' if AIFF)
+              available through the getcomptype() method
+    _compname -- the human-readable AIFF-C compression type
+              available through the getcomptype() method
+    _soundpos -- the position in the audio stream
+              available through the tell() method, set through the
+              setpos() method
+
+    These variables are used internally only:
+    _fmt_chunk_read -- 1 iff the FMT chunk has been read
+    _data_seek_needed -- 1 iff positioned correctly in audio
+              file for readframes()
+    _data_chunk -- instantiation of a chunk class for the DATA chunk
+    _framesize -- size of one frame in the file
+    """
+
+    def initfp(self, file):
+        self._convert = None
+        self._soundpos = 0
+        self._file = Chunk(file, bigendian = 0)
+        if self._file.getname() != b'RIFF':
+            raise Error('file does not start with RIFF id')
+        if self._file.read(4) != b'WAVE':
+            raise Error('not a WAVE file')
+        self._fmt_chunk_read = 0
+        self._data_chunk = None
+        while 1:
+            self._data_seek_needed = 1
+            try:
+                chunk = Chunk(self._file, bigendian = 0)
+            except EOFError:
+                break
+            chunkname = chunk.getname()
+            if chunkname == b'fmt ':
+                self._read_fmt_chunk(chunk)
+                self._fmt_chunk_read = 1
+            elif chunkname == b'data':
+                if not self._fmt_chunk_read:
+                    raise Error('data chunk before fmt chunk')
+                self._data_chunk = chunk
+                self._nframes = chunk.chunksize // self._framesize
+                self._data_seek_needed = 0
+                break
+            chunk.skip()
+        if not self._fmt_chunk_read or not self._data_chunk:
+            raise Error('fmt chunk and/or data chunk missing')
+
+    def __init__(self, f):
+        self._i_opened_the_file = None
+        if isinstance(f, str):
+            f = builtins.open(f, 'rb')
+            self._i_opened_the_file = f
+        # else, assume it is an open file object already
+        try:
+            self.initfp(f)
+        except:
+            if self._i_opened_the_file:
+                f.close()
+            raise
+
+    def __del__(self):
+        self.close()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
+    #
+    # User visible methods.
+    #
+    def getfp(self):
+        return self._file
+
+    def rewind(self):
+        self._data_seek_needed = 1
+        self._soundpos = 0
+
+    def close(self):
+        if self._i_opened_the_file:
+            self._i_opened_the_file.close()
+            self._i_opened_the_file = None
+        self._file = None
+
+    def tell(self):
+        return self._soundpos
+
+    def getnchannels(self):
+        return self._nchannels
+
+    def getnframes(self):
+        return self._nframes
+
+    def getsampwidth(self):
+        return self._sampwidth
+
+    def getframerate(self):
+        return self._framerate
+
+    def getcomptype(self):
+        return self._comptype
+
+    def getcompname(self):
+        return self._compname
+
+    def getparams(self):
+        return _wave_params(self.getnchannels(), self.getsampwidth(),
+                       self.getframerate(), self.getnframes(),
+                       self.getcomptype(), self.getcompname())
+
+    def getmarkers(self):
+        return None
+
+    def getmark(self, id):
+        raise Error('no marks')
+
+    def setpos(self, pos):
+        if pos < 0 or pos > self._nframes:
+            raise Error('position not in range')
+        self._soundpos = pos
+        self._data_seek_needed = 1
+
+    def readframes(self, nframes):
+        if self._data_seek_needed:
+            self._data_chunk.seek(0, 0)
+            pos = self._soundpos * self._framesize
+            if pos:
+                self._data_chunk.seek(pos, 0)
+            self._data_seek_needed = 0
+        if nframes == 0:
+            return b''
+        data = self._data_chunk.read(nframes * self._framesize)
+        if self._sampwidth != 1 and sys.byteorder == 'big':
+            data = audioop.byteswap(data, self._sampwidth)
+        if self._convert and data:
+            data = self._convert(data)
+        self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth)
+        return data
+
+    #
+    # Internal methods.
+    #
+
+    def _read_fmt_chunk(self, chunk):
+        wFormatTag, self._nchannels, self._framerate, dwAvgBytesPerSec, wBlockAlign = struct.unpack('<HHLLH', chunk.read(14))
+        if wFormatTag == WAVE_FORMAT_PCM:
+            sampwidth = struct.unpack('<H', chunk.read(2))[0]
+            self._sampwidth = (sampwidth + 7) // 8
+        else:
+            raise Error('unknown format: %r' % (wFormatTag,))
+        self._framesize = self._nchannels * self._sampwidth
+        self._comptype = 'NONE'
+        self._compname = 'not compressed'
+
+class Wave_write:
+    """Variables used in this class:
+
+    These variables are user settable through appropriate methods
+    of this class:
+    _file -- the open file with methods write(), close(), tell(), seek()
+              set through the __init__() method
+    _comptype -- the AIFF-C compression type ('NONE' in AIFF)
+              set through the setcomptype() or setparams() method
+    _compname -- the human-readable AIFF-C compression type
+              set through the setcomptype() or setparams() method
+    _nchannels -- the number of audio channels
+              set through the setnchannels() or setparams() method
+    _sampwidth -- the number of bytes per audio sample
+              set through the setsampwidth() or setparams() method
+    _framerate -- the sampling frequency
+              set through the setframerate() or setparams() method
+    _nframes -- the number of audio frames written to the header
+              set through the setnframes() or setparams() method
+
+    These variables are used internally only:
+    _datalength -- the size of the audio samples written to the header
+    _nframeswritten -- the number of frames actually written
+    _datawritten -- the size of the audio samples actually written
+    """
+
+    def __init__(self, f):
+        self._i_opened_the_file = None
+        if isinstance(f, str):
+            f = builtins.open(f, 'wb')
+            self._i_opened_the_file = f
+        try:
+            self.initfp(f)
+        except:
+            if self._i_opened_the_file:
+                f.close()
+            raise
+
+    def initfp(self, file):
+        self._file = file
+        self._convert = None
+        self._nchannels = 0
+        self._sampwidth = 0
+        self._framerate = 0
+        self._nframes = 0
+        self._nframeswritten = 0
+        self._datawritten = 0
+        self._datalength = 0
+        self._headerwritten = False
+
+    def __del__(self):
+        self.close()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
+    #
+    # User visible methods.
+    #
+    def setnchannels(self, nchannels):
+        if self._datawritten:
+            raise Error('cannot change parameters after starting to write')
+        if nchannels < 1:
+            raise Error('bad # of channels')
+        self._nchannels = nchannels
+
+    def getnchannels(self):
+        if not self._nchannels:
+            raise Error('number of channels not set')
+        return self._nchannels
+
+    def setsampwidth(self, sampwidth):
+        if self._datawritten:
+            raise Error('cannot change parameters after starting to write')
+        if sampwidth < 1 or sampwidth > 4:
+            raise Error('bad sample width')
+        self._sampwidth = sampwidth
+
+    def getsampwidth(self):
+        if not self._sampwidth:
+            raise Error('sample width not set')
+        return self._sampwidth
+
+    def setframerate(self, framerate):
+        if self._datawritten:
+            raise Error('cannot change parameters after starting to write')
+        if framerate <= 0:
+            raise Error('bad frame rate')
+        self._framerate = int(round(framerate))
+
+    def getframerate(self):
+        if not self._framerate:
+            raise Error('frame rate not set')
+        return self._framerate
+
+    def setnframes(self, nframes):
+        if self._datawritten:
+            raise Error('cannot change parameters after starting to write')
+        self._nframes = nframes
+
+    def getnframes(self):
+        return self._nframeswritten
+
+    def setcomptype(self, comptype, compname):
+        if self._datawritten:
+            raise Error('cannot change parameters after starting to write')
+        if comptype not in ('NONE',):
+            raise Error('unsupported compression type')
+        self._comptype = comptype
+        self._compname = compname
+
+    def getcomptype(self):
+        return self._comptype
+
+    def getcompname(self):
+        return self._compname
+
+    def setparams(self, params):
+        nchannels, sampwidth, framerate, nframes, comptype, compname = params
+        if self._datawritten:
+            raise Error('cannot change parameters after starting to write')
+        self.setnchannels(nchannels)
+        self.setsampwidth(sampwidth)
+        self.setframerate(framerate)
+        self.setnframes(nframes)
+        self.setcomptype(comptype, compname)
+
+    def getparams(self):
+        if not self._nchannels or not self._sampwidth or not self._framerate:
+            raise Error('not all parameters set')
+        return _wave_params(self._nchannels, self._sampwidth, self._framerate,
+              self._nframes, self._comptype, self._compname)
+
+    def setmark(self, id, pos, name):
+        raise Error('setmark() not supported')
+
+    def getmark(self, id):
+        raise Error('no marks')
+
+    def getmarkers(self):
+        return None
+
+    def tell(self):
+        return self._nframeswritten
+
+    def writeframesraw(self, data):
+        if not isinstance(data, (bytes, bytearray)):
+            data = memoryview(data).cast('B')
+        self._ensure_header_written(len(data))
+        nframes = len(data) // (self._sampwidth * self._nchannels)
+        if self._convert:
+            data = self._convert(data)
+        if self._sampwidth != 1 and sys.byteorder == 'big':
+            data = audioop.byteswap(data, self._sampwidth)
+        self._file.write(data)
+        self._datawritten += len(data)
+        self._nframeswritten = self._nframeswritten + nframes
+
+    def writeframes(self, data):
+        self.writeframesraw(data)
+        if self._datalength != self._datawritten:
+            self._patchheader()
+
+    def close(self):
+        if self._file:
+            try:
+                self._ensure_header_written(0)
+                if self._datalength != self._datawritten:
+                    self._patchheader()
+                self._file.flush()
+            finally:
+                self._file = None
+        if self._i_opened_the_file:
+            self._i_opened_the_file.close()
+            self._i_opened_the_file = None
+
+    #
+    # Internal methods.
+    #
+
+    def _ensure_header_written(self, datasize):
+        if not self._headerwritten:
+            if not self._nchannels:
+                raise Error('# channels not specified')
+            if not self._sampwidth:
+                raise Error('sample width not specified')
+            if not self._framerate:
+                raise Error('sampling rate not specified')
+            self._write_header(datasize)
+
+    def _write_header(self, initlength):
+        assert not self._headerwritten
+        self._file.write(b'RIFF')
+        if not self._nframes:
+            self._nframes = initlength // (self._nchannels * self._sampwidth)
+        self._datalength = self._nframes * self._nchannels * self._sampwidth
+        try:
+            self._form_length_pos = self._file.tell()
+        except (AttributeError, OSError):
+            self._form_length_pos = None
+        self._file.write(struct.pack('<L4s4sLHHLLHH4s',
+            36 + self._datalength, b'WAVE', b'fmt ', 16,
+            WAVE_FORMAT_PCM, self._nchannels, self._framerate,
+            self._nchannels * self._framerate * self._sampwidth,
+            self._nchannels * self._sampwidth,
+            self._sampwidth * 8, b'data'))
+        if self._form_length_pos is not None:
+            self._data_length_pos = self._file.tell()
+        self._file.write(struct.pack('<L', self._datalength))
+        self._headerwritten = True
+
+    def _patchheader(self):
+        assert self._headerwritten
+        if self._datawritten == self._datalength:
+            return
+        curpos = self._file.tell()
+        self._file.seek(self._form_length_pos, 0)
+        self._file.write(struct.pack('<L', 36 + self._datawritten))
+        self._file.seek(self._data_length_pos, 0)
+        self._file.write(struct.pack('<L', self._datawritten))
+        self._file.seek(curpos, 0)
+        self._datalength = self._datawritten
+
+def open(f, mode=None):
+    if mode is None:
+        if hasattr(f, 'mode'):
+            mode = f.mode
+        else:
+            mode = 'rb'
+    if mode in ('r', 'rb'):
+        return Wave_read(f)
+    elif mode in ('w', 'wb'):
+        return Wave_write(f)
+    else:
+        raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
+
+openfp = open # B/W compatibility