source: digitalmultimeter/trunk/digitalmultimeter.py @ 26

Last change on this file since 26 was 4, checked in by retracile, 17 years ago

Bugfix: reference self.serialport instead of the global.

  • Property svn:executable set to *
File size: 6.6 KB
Line 
1#!/usr/bin/python
2"""This utility reads the serial port data from the
3RadioShack Digital Multimeter 22-812
4and translates it into human-readable output.
5
6This program is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 2 of the License, or
9(at your option) any later version.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19Copyright 2006,2008 Eli Carter, retracile@gmail.com
20"""
21
22import sys
23import time
24import termios
25
26# The elements of each LCD digit are labeled as A-G and the PERIOD:
27#     A
28#     --
29#  F |  | B
30#     -- G
31#  E |  | C
32#   . --
33#  P  D
34# Those map to the bits in a byte:
35A = 1 << 0
36B = 1 << 4
37C = 1 << 6
38D = 1 << 7
39E = 1 << 2
40F = 1 << 1
41G = 1 << 5
42# Characters marked as "speculation" are combinations I have not seen used by
43# the multimeter, but I have added to avoid "?" output if that combination does
44# get used in some case.
45byte_digit_mapping = {
46    0:              " ",
47    B+C:            "1",
48    A+B+G+E+D:      "2",
49    A+B+G+C+D:      "3",
50    F+G+B+C:        "4",
51    A+F+G+C+D:      "5", # Doubles as "S"
52    A+F+G+E+D+C:    "6",
53    A+B+C:          "7",
54    A+B+C+D+E+F+G:  "8",
55    A+B+C+D+F+G:    "9",
56    A+B+C+D+E+F:    "0", # Doubles as "O"
57    G:              "-",
58
59    A+F+E+G+B+C:    "A", # speculation
60    F+G+C+D+E:      "b", # speculation
61    A+F+E+C:        "C",
62    E+G+B+C+D:      "d", # speculation
63    A+F+G+E+D:      "E",
64    A+F+G+E:        "F",
65    A+F+E+D+C:      "G", # speculation
66    F+E+G+C:        "h",
67    F+B+G+E+C:      "H", # speculation
68    C:              "i", # speculation
69    B+C+D:          "J", # speculation
70    F+E+D:          "L",
71    E+G+C:          "n",
72    E+G+C+D:        "o",
73    F+E+A+B+G:      "P",
74    E+G:            "r",
75    F+G+E+D:        "t",
76    E+D+C:          "u", # speculation
77    F+E+D+C+B:      "U", # speculation
78}
79
80def byte_to_digit(b):
81    """Interpret the LCD elements into something meaningful such as digits and
82    letters.
83    """
84    PERIOD = 1 << 3
85    # The period comes before the digit
86    if b & PERIOD: # decimal?
87        digit = "."
88    else:
89        digit = ""
90    try:
91        # Mask out the decimal point indicator
92        digit += byte_digit_mapping[b & ~PERIOD]
93    except KeyError:
94        # TODO: it might be helpful to say which elements were lit...
95        digit += "?"
96    return digit
97
98
99class BitMapper:
100    """Given a byte of output, provide a string of lit LCD elements."""
101    def __init__(self, mapping):
102        self.mapping = mapping
103    def __call__(self, value):
104        # Each bit of the given value represents the item in the mapping list;
105        # return a string
106        output = []
107        for i in range(8):
108            if value & (1<<i):
109                output.append(self.mapping[i])
110        return " ".join(output)
111
112
113class RadioShackDigitalMultimeter22812:
114    """Interface for reading meaningful data from the serial output of the
115    RadioShack Digital Multimeter 22-812.
116    """
117    indicator_map_1 = BitMapper(["m", "V", "A", "F", "M", "K", "Ohm", "Hz"])
118    indicator_map_2 = BitMapper(["MIN", "REL", "hFE", "%", "S", "dBm", "n", "u"])
119    indicator_map_3 = BitMapper(["AUTO", "RS232", "~", "-", "HOLD", "BAT", "DIODE", "BEEP"])
120    # The mode byte is a number indicating which mode we're in
121    mode_byte_mapping = [
122        "DC V",
123        "AC V",
124        "DC uA",
125        "DC mA",
126        "DC A",
127        "AC uA",
128        "AC mA",
129        "AC A",
130        "OHM",
131        "CAP",
132        "HZ",
133        "NET HZ",
134        "AMP HZ",
135        "DUTY",
136        "NET DUTY",
137        "AMP DUTY",
138        "WIDTH",
139        "NET WIDTH",
140        "AMP WIDTH",
141        "DIODE",
142        "CONT",
143        "HFE",
144        "LOGIC",
145        "DBM",
146        "EF",
147        "TEMP",
148    ]
149
150    def __init__(self, serialport, debug=False):
151        self.serialport = serialport
152        self.debug = debug
153        self.setup_serial_port()
154
155    def setup_serial_port(self):
156        # configure serial port
157        # print termios.tcgetattr(serialport.fileno()) # DEBUG
158        iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(
159            self.serialport.fileno())
160        # These values were obtained by configuring the serial port with minicom,
161        # then reading the settings.  FIXME: It would be nice to set them based on
162        # their meaning rather than raw values.
163        iflag = 1
164        oflag = 0
165        cflag = -2147481412
166        lflag = 0
167        # 4800 baud
168        ispeed = termios.B4800
169        ospeed = termios.B4800
170        result = termios.tcsetattr(self.serialport.fileno(), termios.TCSANOW,
171            [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
172
173        if self.debug:
174            print "result=%s" % result
175            print termios.tcgetattr(serialport.fileno())
176
177    def byte_to_mode(self, b):
178        return self.mode_byte_mapping[b]
179
180    def readline(self):
181        data = [ord(x) for x in self.serialport.read(9)]
182        timestamp = time.time()
183
184        # Then interpret the data
185        #print repr(data) # DEBUG
186        # Byte 1
187        mode = self.byte_to_mode(data[0])
188        # Byte 2
189        indicator1 = self.indicator_map_1(data[1])
190        # Byte 3
191        indicator2 = self.indicator_map_2(data[2])
192        # Bytes 4-7
193        rawlcd = data[3:7]
194        lcd = "".join([byte_to_digit(x) for x in reversed(rawlcd)])
195        # Check for some special cases such as short/open indicators
196        if lcd.startswith("."):
197            # The period in byte 7 lights "MAX", not a period.
198            lcd = "MAX " + lcd[1:]
199        elif lcd == '5hrt':
200            lcd = 'Short'
201        elif lcd == '0PEn':
202            lcd = 'Open'
203        # Byte 8
204        indicator3 = self.indicator_map_3(data[7])
205        # Byte 9
206        # TODO: This is a checksum + 0x57
207
208        # And finally print out the interpretation.
209        return "%.6f %s %s %s %s %s" % (timestamp, mode, indicator3, lcd,
210            indicator2, indicator1)
211
212if __name__ == "__main__":
213    if len(sys.argv) < 2:
214        sys.stderr.write("Syntax error: serial device name required.\n")
215        sys.exit(1)
216    serialport = open(sys.argv[1],"r")
217
218    multimeter = RadioShackDigitalMultimeter22812(serialport)
219    while True:
220        # Read the data and grab a timestamp for the reading
221        print multimeter.readline()
222
223# vim:foldmethod=indent foldcolumn=8
224# vim:shiftwidth=4 ts=4 softtabstop=4 expandtab
Note: See TracBrowser for help on using the repository browser.