# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is the Python Computer Graphics Kit.
#
# The Initial Developer of the Original Code is Matthias Baas.
# Portions created by the Initial Developer are Copyright (C) 2004
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
# $Id: fob.py,v 1.2 2005/04/29 15:10:22 mbaas Exp $


import sys, string, time, struct

try:
    import serial
    serial_installed = True
except:
    serial_installed = False

# Commands
ANGLES = 'W'
ANGLE_ALIGN1 = 'J'
ANGLE_ALIGN2 = 'q'
BORESIGHT = 'u'
BORESIGHT_REMOVE = 'v'
BUTTON_MODE = 'M'
BUTTON_READ = 'N'
CHANGE_VALUE = 'P'
EXAMINE_VALUE = 'O'
FBB_RESET = '/'
HEMISPHERE = 'L'
MATRIX = 'X'
METAL = 's'
METAL_ERROR = 't'
NEXT_TRANSMITTER = '0'
OFFSET = 'K'
POINT = 'B'
POSITION = 'V'
POSITION_ANGLES = 'Y'
POSITION_MATRIX = 'Z'
POSITION_QUATERNION = ']'
QUATERNION = '\\'
REFERENCE_FRAME1 = 'H'
REFERENCE_FRAME2 = 'r'
REPORT_RATE1 = 'Q'
REPORT_RATE2 = 'R'
REPORT_RATE8 = 'S'
REPORT_RATE32 = 'T'
RUN = 'F'
SLEEP = 'G'
STREAM = '@'
STREAM_STOP = '?'
SYNC = 'A'
XOFF = 0x13
XON = 0x11

# Change value/Examine Value parameters
BIRD_STATUS = chr(0x00)
SOFTWARE_REVISION_NUMBER = chr(0x01)
BIRD_COMPUTER_CRYSTAL_SPEED = chr(0x02)
POSITION_SCALING = chr(0x03)
BIRD_MEASUREMENT_RATE = chr(0x07)
SYSTEM_MODEL_IDENTIFICATION = chr(0x0f)
FLOCK_SYSTEM_STATUS = chr(0x24)
FBB_AUTO_CONFIGURATION = chr(0x32)

# Hemispheres
FORWARD = chr(0) + chr(0)
AFT =  chr(0) + chr(1)
UPPER = chr(0xc) + chr(1)
LOWER = chr(0xc) + chr(0)
LEFT = chr(6) + chr(1)
RIGHT = chr(6) + chr(0)

class DataError(Exception):
    """Exception.

    This exception is thrown when the number of bytes returned from the bird
    doesn't match what is expected."""
    pass

# BirdContext
class BirdContext:
    """Context class for one individual bird (sensor).

    This class stores the output mode in which a particular bird is
    currently in.  This information is used to decode the data record
    sent by the bird.
    """
    def __init__(self):
        #TODO: Check comments below, do we need to adjust?
        # 144 for extended range transmitter (see p. 89 of the FOB manual)
        # 2.54 to convert inches into cm
        self.pos_scale = 144*2.54

        # Mode (ANGLE, POSITION, ...)
        self.mode = POSITION_ANGLES
        # Size of data records (default is POSITION/ANGLES)
        self.record_size = 12
        # Number of words in one record (record_size = 2*num_words [+ metal])
        self.num_words = 6
        self.scales = [self.pos_scale, self.pos_scale, self.pos_scale, 180, 180, 180]
        self.metal_flag = False

    def angles(self, addr=None):
        self.mode = ANGLES
        self.num_words = 3
        self.scales = [180,180,180]
        self._calc_record_size()

    def position(self):
        self.mode = POSITION
        self.num_words = 3
        self.scales = [self.pos_scale, self.pos_scale, self.pos_scale]
        self._calc_record_size()

    def position_angles(self, addr=None):
        """POSITION/ANGLES command."""
        self.mode = POSITION_ANGLES
        self.num_words = 6
        self.scales = [self.pos_scale, self.pos_scale, self.pos_scale,180,180,180]
        self._calc_record_size()

    def matrix(self, addr=None):
        self.mode = MATRIX
        self.num_words = 9
        self.scales = [1,1,1,1,1,1,1,1,1]
        self._calc_record_size()

    def position_matrix(self, addr=None):
        """POSITION/MATRIX command."""
        self.mode = POSITION_MATRIX
        self.num_words = 12
        self.scales = [self.pos_scale, self.pos_scale, self.pos_scale,1,1,1,1,1,1,1,1,1]
        self._calc_record_size()

    def quaternion(self, addr=None):
        """QUATERNION command."""
        self.mode = QUATERNION
        self.num_words = 4
        self.scales = [1,1,1,1]
        self._calc_record_size()

    def position_quaternion(self, addr=None):
        """POSITION/QUATERNION command."""
        self.mode = POSITION_QUATERNION
        self.num_words = 7
        self.scales = [self.pos_scale, self.pos_scale, self.pos_scale,1,1,1,1]
        self._calc_record_size()

    def metal(self, flag):
        self.metal_flag = flag
        self._calc_record_size()

    def decode(self, s):
        values = []
        for i in range(self.num_words):
            v = self.decodeWord(s[2*i:2*i+2])
            v*=self.scales[i]
            v=v/32768.0
            values.append(v)
        return values
        

    def decodeWord(self, s):
        """Decode the word in string s.

        s must contain exactly 2 bytes.
        """

        ls = ord(s[0]) & 0x7f
        ms = ord(s[1])
        if ms&0x80==0x80:
            print("MSB bit7 nicht 0!")
        v = (ms<<9) | (ls<<2)
        if v<0x8000:
            return v
        else:
            return v-0x10000

    def _calc_record_size(self):
        self.record_size = 2*self.num_words

# FOB
class FOB:
    """Interface to the Ascension Flock Of Birds.

    This class can be used to communicate with the Ascension Flock
    of Birds motion tracker (http://www.ascension-tech.com/).
    The class directly accesses the serial port and has methods
    that correspond to the commands described in the \em Flock \em of
    \em Birds \em Installation \em and \em Operation \em Guide.

    Example usage:

    \code
    fob = FOB()
    # Open a connection on the serial port
    fob.open()
    # Initialize
    fob.fbbAutoConfig(numbirds=4)
    fob.fbbReset()
    ...

    # Set output mode for bird 1 to "position"
    fob.position(1)
    
    # Obtain a sensor value from bird 1
    pos = fob.point(1)
    ...
    
    fob.close()
    \endcode
    """
    def __init__(self):
        """Constructor."""
        # Serial() instance
        self.ser = None

        # Default-Context (Master)
        self.defaultctx = BirdContext()
        # A list of bird context classes (one per sensor)        
        self.birds = []

    def __str__(self):
        if self.ser==None:
            serstr = "not connected"
        else:
            serstr = "port:%s, baudrate:%d"%(self.ser.portstr, self.ser.baudrate)
        s = "Flock of Birds (%s):\n"%serstr
        s += '  Device Description: "%s"\n'%self.examineValue(SYSTEM_MODEL_IDENTIFICATION)
        status = self.examineValue(FLOCK_SYSTEM_STATUS)
        for i in range(14):
            s += "  Bird %2d: "%i
            b = status[i]
            if b&0x01:
                s += "ERT#0, "
            if b&0x02:
                s += "ERT#1, "
            if b&0x04:
                s += "ERT#2, "
            if b&0x08:
                s += "ERT#3, "
            if b&0x10:
                s += "Extended Range Controller, "
            if b&0x80==0:
                s += "not "
            s += "accessible, "
            if b&0x40==0:
                s += "not "
            s += "running, "
            if b&0x20:
                s += "has a sensor"
            else:
                s += "has no sensor"
            if i<14:
                s += "\n"
        return s
    
    # open
    def open(self, port=0, baudrate=115200, timeout=3):
        """Open a connection to the flock.

        \param port (\c int) Port number (0,1,...)
        \param baudrate (\c int) Baud rate
        \param timeout (\c float) Time out value for RS232 operations
        """
        if not serial_installed:
            raise RuntimeError('Cannot import the serial module (http://pyserial.sourceforge.net).')
        
        self.ser = serial.Serial(port=port,
                                 baudrate=baudrate,
                                 bytesize=serial.EIGHTBITS,
                                 parity=serial.PARITY_NONE,
                                 stopbits=serial.STOPBITS_ONE,
                                 xonxoff=0,
                                 rtscts=0,
                                 timeout=timeout)
        self.ser.setRTS(0)
        ser = self.ser #TODO: does this line do anything?

    # close
    def close(self):
        if self.ser!=None:
            self.ser.close()
            self.ser = None

    def examineValue(self, param, addr=None):
        """EXAMINE VALUE command.

        Here are the possible parameters and their return values

        - BIRD_STATUS: Integer (actually a word)
        - SOFTWARE_REVISION_NUMBER: Tuple (int,fra). The version number is int.fra
        - BIRD_COMPUTER_CRYSTAL_SPEED: Frequeny in MHz as an integer
        - BIRD_MEASUREMENT_RATE: Measurement rate in cycles/sec (as float)
        - SYSTEM_MODEL_IDENTIFICATION: Device description string
        - FLOCK_SYSTEM_STATUS: 14-tuple with the status byte for each bird
        - FBB_AUTO_CONFIGURATION: Tuple (configmode, devices, dependents)

        \param param Parameter number (there are symbolic constants)
        \return Value
        """
        if addr!=None:
            self.rs232ToFbb(addr)            
        self._write(EXAMINE_VALUE+param)
        # BIRD_STATUS
        if param==BIRD_STATUS:
            v = self._read(2, exc=True)
            res = struct.unpack("<H", v)[0]
        # SOFTWARE_REVISION_NUMBER
        elif param==SOFTWARE_REVISION_NUMBER:
            v = self._read(2, exc=True)
            int = struct.unpack("B", v[1])[0]
            fra = struct.unpack("B", v[0])[0]
            res = (int, fra)
        # BIRD_COMPUTER_CRYSTAL_SPEED
        elif param==BIRD_COMPUTER_CRYSTAL_SPEED:
            v = self._read(2, exc=True)
            res = struct.unpack("B", v[0])[0]
        # BIRD_MEASUREMENT_RATE
        elif param==BIRD_MEASUREMENT_RATE:
            v = self._read(2, exc=True)
            rate = struct.unpack("<H", v)[0]
            res = float(rate)/256.0
        # SYSTEM_MODEL_IDENTIFICATION
        elif param==SYSTEM_MODEL_IDENTIFICATION:
            v = self._read(10, exc=True)
            res = v.strip()
        # FLOCK_SYSTEM_STATUS
        elif param==FLOCK_SYSTEM_STATUS:
            v = self._read(14, exc=True)
            res = struct.unpack("BBBBBBBBBBBBBB", v)
        # FBB AUTO-CONFIGURATION
        elif param==FBB_AUTO_CONFIGURATION:
            v = self._read(5, exc=True)
            configmode = struct.unpack("B", v[0])[0]
            dev = struct.unpack("<H", v[1:3])[0]
            deps = struct.unpack("<H", v[3:])[0]
            res = (configmode, dev, deps)
        else:
            raise ValueError('Unknown parameter: %s'%param)

        return res


    def fbbReset(self):
        """FBB RESET command.

        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        self._write(FBB_RESET)

    def angles(self, addr=None):
        """ANGLES command.

        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        ctx = self._switch_mode(ANGLES, addr)
        ctx.angles()

    def position(self, addr=None):
        """POSITION command.

        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        ctx = self._switch_mode(POSITION, addr)
        ctx.position()

    def position_angles(self, addr=None):
        """POSITION/ANGLES command.

        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        ctx = self._switch_mode(POSITION_ANGLES, addr)
        ctx.position_angles()

    def matrix(self, addr=None):
        """MATRIX command.

        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        ctx = self._switch_mode(MATRIX, addr)
        ctx.matrix()

    def position_matrix(self, addr=None):
        """POSITION/MATRIX command.

        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        ctx = self._switch_mode(POSITION_MATRIX, addr)
        ctx.position_matrix()

    def quaternion(self, addr=None):
        """QUATERNION command.

        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        ctx = self._switch_mode(QUATERNION, addr)
        ctx.quaternion()

    def position_quaternion(self, addr=None):
        """POSITION/QUATERNION command.

        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        ctx = self._switch_mode(POSITION_QUATERNION, addr)
        ctx.position_quaternion()        

    def point(self, addr=None):
        """POINT command.

        \param addr Bird address (0,1,2...).
        \return A list of sensor values (the number of values depends on the output mode).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        if addr==None:
            ctx = self.defaultctx
        else:
            self.rs232ToFbb(addr)
            ctx = self.birds[addr]

        self._write(POINT)
        while 1:
            record = self._read()
            if record=="":
                print("FOB: No feedback from the ERC")
                return
            if ord(record[0])&0x80:
                break
            print("FOB: Bit 7 not set")
            
        record += self._read(ctx.record_size-1)
        if len(record)!=ctx.record_size:
            print("Wrong number of bytes read (%d instead of %d)"%(len(record), ctx.record_size))
            return None

        res = ctx.decode(record)
        return res

    def hemisphere(self, hemisphere, addr=None):
        """HEMISPHERE command.

        \param hemisphere The hemisphere to use (one of FORWARD, AFT, UPPER, LOWER, LEFT, RIGHT)
        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        if addr!=None:
            self.rs232ToFbb(addr)

        self._write(HEMISPHERE+hemisphere)            

    def run(self):
        """RUN command.

        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        self._write(RUN)

    def sleep(self):
        """SLEEP command.

        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        self._write(SLEEP)

    def metal(self, flag, data, addr=None):
        """METAL command.

        \param flag (\c int) Flag (see Flock of Birds manual)
        \param data (\c int) Data (see Flock of Birds manual)
        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        if addr==None:
            ctx = self.defaultctx
        else:
            self.rs232ToFbb(addr)
            ctx = self.birds[addr]
        self._write(METAL+chr(flag)+chr(data))
        if flag==0 and data==0:
            ctx.metal(False)
        else:
            ctx.metal(True)

    def metal_error(self, addr=None):
        """METAL ERROR command.

        \param addr Bird address (0,1,2...).
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        """
        if addr==None:
            ctx = self.defaultctx
        else:
            self.rs232ToFbb(addr)
            ctx = self.birds[addr]

        self._write(METAL_ERROR)               
        met = self._read(1)
        if len(met)!=1:
            print("Metal error byte not received.")
            return
        print("Metal error:",ord(met[0]))
        
    def fbbAutoConfig(self, numbirds):
        """FBB AUTO-CONFIGURATION command.

        Send the FBB AUTO-CONFIGURATION command (via CHANGE VALUE).

        \param numbirds (\c int) The number of birds to use (this is the parameter byte that the FBB AUTO-CONFIGURATION command expects)
        \see Flock of Birds Installation and Operation Guide, chapter 7: \em RS232 \em Commands
        \see Flock of Birds Installation and Operation Guide, chapter 8: \em Change \em Value / \em Examine \em Value
        """
        time.sleep(1.0)
        self._write(CHANGE_VALUE+FBB_AUTO_CONFIGURATION+chr(numbirds))
        time.sleep(1.0)
        # First element is not used (addr 0 does not exist)
        self.birds = [None]
        for i in range(numbirds):
            self.birds.append(BirdContext())

    def rs232ToFbb(self, addr):
        """Address a particular bird.

        Only for normal addressing mode!
        This method has to be called before the actual command is issued.

        \param addr (\c int) Bird address (1-14)
        """

        self._write(chr(0xf0+addr))

    def _switch_mode(self, mode, addr=None):
        """Generic mode switch command.

        mode is one of ANGLES, POSITION, MATRIX, QUATERNION, POSITION_ANGLES,
        POSITION_MATRIX, POSITION_QUATERNION.
        addr is the bird addr or None.
        The return value is the corresponding bird context. The calling
        function must still call the appropriate mode switch method on the
        context.

        \param mode Output mode
        \param addr Bird address (0,1,2...)
        \return A bird context class (BirdContext).
        """
        if addr==None:
            ctx = self.defaultctx
        else:
            self.rs232ToFbb(addr)
            ctx = self.birds[addr]
        self._write(mode)
        return ctx
        
    def _write(self, s):
        """Send string s to the Bird."""
        self.ser.write(s)

    def _read(self, size=1, exc=False):
        """Receive size bytes of data from the bird.

        If the timeout value is exceeded an empty string is returned.

        \param size (\c int) Number of bytes to read.
        \param exc (\c bool) If True an exception is thrown when the number of read bytes does not match
        \return String containing the received bytes (or empty string).
        """
        v = self.ser.read(size)
        if exc and len(v)!=size:
            raise DataError('Expected %d bytes from the bird, but got %d'%(size, len(v)))
        return v
