#!/usr/bin/python
"""This utility reads the serial port data from the
RadioShack Digital Multimeter 22-812
and translates it into human-readable output.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Copyright 2006,2008 Eli Carter, retracile@gmail.com
"""

import sys
import time
import termios

# The elements of each LCD digit are labeled as A-G and the PERIOD:
#     A
#     --
#  F |  | B
#     -- G
#  E |  | C
#   . --
#  P  D
# Those map to the bits in a byte:
A = 1 << 0
B = 1 << 4
C = 1 << 6
D = 1 << 7
E = 1 << 2
F = 1 << 1
G = 1 << 5
# Characters marked as "speculation" are combinations I have not seen used by
# the multimeter, but I have added to avoid "?" output if that combination does
# get used in some case.
byte_digit_mapping = {
    0:              " ",
    B+C:            "1",
    A+B+G+E+D:      "2",
    A+B+G+C+D:      "3",
    F+G+B+C:        "4",
    A+F+G+C+D:      "5", # Doubles as "S"
    A+F+G+E+D+C:    "6",
    A+B+C:          "7",
    A+B+C+D+E+F+G:  "8",
    A+B+C+D+F+G:    "9",
    A+B+C+D+E+F:    "0", # Doubles as "O"
    G:              "-",

    A+F+E+G+B+C:    "A", # speculation
    F+G+C+D+E:      "b", # speculation
    A+F+E+C:        "C",
    E+G+B+C+D:      "d", # speculation
    A+F+G+E+D:      "E",
    A+F+G+E:        "F",
    A+F+E+D+C:      "G", # speculation
    F+E+G+C:        "h",
    F+B+G+E+C:      "H", # speculation
    C:              "i", # speculation
    B+C+D:          "J", # speculation
    F+E+D:          "L",
    E+G+C:          "n",
    E+G+C+D:        "o",
    F+E+A+B+G:      "P",
    E+G:            "r",
    F+G+E+D:        "t",
    E+D+C:          "u", # speculation
    F+E+D+C+B:      "U", # speculation
}

def byte_to_digit(b):
    """Interpret the LCD elements into something meaningful such as digits and
    letters.
    """
    PERIOD = 1 << 3
    # The period comes before the digit
    if b & PERIOD: # decimal?
        digit = "."
    else:
        digit = ""
    try:
        # Mask out the decimal point indicator
        digit += byte_digit_mapping[b & ~PERIOD]
    except KeyError:
        # TODO: it might be helpful to say which elements were lit...
        digit += "?"
    return digit


class BitMapper:
    """Given a byte of output, provide a string of lit LCD elements."""
    def __init__(self, mapping):
        self.mapping = mapping
    def __call__(self, value):
        # Each bit of the given value represents the item in the mapping list;
        # return a string
        output = []
        for i in range(8):
            if value & (1<<i):
                output.append(self.mapping[i])
        return " ".join(output)


class RadioShackDigitalMultimeter22812:
    """Interface for reading meaningful data from the serial output of the
    RadioShack Digital Multimeter 22-812.
    """
    indicator_map_1 = BitMapper(["m", "V", "A", "F", "M", "K", "Ohm", "Hz"])
    indicator_map_2 = BitMapper(["MIN", "REL", "hFE", "%", "S", "dBm", "n", "u"])
    indicator_map_3 = BitMapper(["AUTO", "RS232", "~", "-", "HOLD", "BAT", "DIODE", "BEEP"])
    # The mode byte is a number indicating which mode we're in
    mode_byte_mapping = [
        "DC V",
        "AC V",
        "DC uA",
        "DC mA",
        "DC A",
        "AC uA",
        "AC mA",
        "AC A",
        "OHM",
        "CAP",
        "HZ",
        "NET HZ",
        "AMP HZ",
        "DUTY",
        "NET DUTY",
        "AMP DUTY",
        "WIDTH",
        "NET WIDTH",
        "AMP WIDTH",
        "DIODE",
        "CONT",
        "HFE",
        "LOGIC",
        "DBM",
        "EF",
        "TEMP",
    ]

    def __init__(self, serialport, debug=False):
        self.serialport = serialport
        self.debug = debug
        self.setup_serial_port()

    def setup_serial_port(self):
        # configure serial port
        # print termios.tcgetattr(serialport.fileno()) # DEBUG
        iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(
            self.serialport.fileno())
        # These values were obtained by configuring the serial port with minicom,
        # then reading the settings.  FIXME: It would be nice to set them based on
        # their meaning rather than raw values.
        iflag = 1
        oflag = 0
        cflag = -2147481412
        lflag = 0
        # 4800 baud
        ispeed = termios.B4800
        ospeed = termios.B4800
        result = termios.tcsetattr(self.serialport.fileno(), termios.TCSANOW,
            [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])

        if self.debug:
            print "result=%s" % result
            print termios.tcgetattr(serialport.fileno())

    def byte_to_mode(self, b):
        return self.mode_byte_mapping[b]

    def readline(self):
        data = [ord(x) for x in serialport.read(9)]
        timestamp = time.time()

        # Then interpret the data
        #print repr(data) # DEBUG
        # Byte 1
        mode = self.byte_to_mode(data[0])
        # Byte 2
        indicator1 = self.indicator_map_1(data[1])
        # Byte 3
        indicator2 = self.indicator_map_2(data[2])
        # Bytes 4-7
        rawlcd = data[3:7]
        lcd = "".join([byte_to_digit(x) for x in reversed(rawlcd)])
        # Check for some special cases such as short/open indicators
        if lcd.startswith("."):
            # The period in byte 7 lights "MAX", not a period.
            lcd = "MAX " + lcd[1:]
        elif lcd == '5hrt':
            lcd = 'Short'
        elif lcd == '0PEn':
            lcd = 'Open'
        # Byte 8
        indicator3 = self.indicator_map_3(data[7])
        # Byte 9
        # TODO: This is a checksum + 0x57

        # And finally print out the interpretation.
        return "%.6f %s %s %s %s %s" % (timestamp, mode, indicator3, lcd,
            indicator2, indicator1)

if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.stderr.write("Syntax error: serial device name required.\n")
        sys.exit(1)
    serialport = open(sys.argv[1],"r")

    multimeter = RadioShackDigitalMultimeter22812(serialport)
    while True:
        # Read the data and grab a timestamp for the reading
        print multimeter.readline()

# vim:foldmethod=indent foldcolumn=8
# vim:shiftwidth=4 ts=4 softtabstop=4 expandtab