Skip to content

Commit 4b42402

Browse files
committed
Adds unit tests; fixes things found by unit tests
1 parent 6982927 commit 4b42402

File tree

3 files changed

+127
-15
lines changed

3 files changed

+127
-15
lines changed

python/query_demo.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
t_ser = TracerSerial(tracer, "")
1414
if not fake:
1515
ser = serial.Serial('/dev/ttyAMA0', 9600, timeout = 1)
16-
ser.write(bytearray(t_ser.to_bytes(query)))
16+
ser.write(t_ser.to_bytes(query))
1717
data = bytearray(ser.read(200))
1818
else:
1919
data = fake
2020

2121
print "Read %d bytes" % len(data)
2222
print ", ".join(map(lambda a: "%0X" % (a), data))
2323
result = t_ser.from_bytes(data)
24+
print result
2425

2526
print "PV voltage %s" % result.pv_voltage
2627
print "Battery voltage %s" % result.batt_voltage

python/test.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from tracer import Result, QueryResult, Command, QueryCommand, ManualCommand, TracerSerial, Tracer
2+
from unittest2 import TestCase
3+
import unittest2
4+
from mock import Mock
5+
6+
fixture_float_bytes=bytearray(b'\xD2\x04')
7+
fixture_data=bytearray(b'\x41\x42\x43')
8+
query_result = bytearray([0xD2, 0x4, 0xD3, 0x4, 0x0, 0x0, 0xE, 0x0, 0x53, 0x4, 0xA5, 0x5, 0x1, 0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x0, 0x33, 0x0A, 0x0, 0x0, 0x99, 0x5B, 0x7F])
9+
full_query_result = bytearray([0xEB, 0x90, 0xEB, 0x90, 0xEB, 0x90, 0x0, 0xA0, 0x18]) + query_result
10+
class TestResult(TestCase):
11+
def test_to_float(self):
12+
r=Result("")
13+
self.assertEqual(12.34, r.to_float(fixture_float_bytes))
14+
15+
def assertQueryResult(self, qr):
16+
self.assertEqual(12.34, qr.batt_voltage)
17+
self.assertEqual(12.35, qr.pv_voltage)
18+
self.assertEqual(0.14, qr.load_amps)
19+
self.assertEqual(11.07, qr.batt_overdischarge_voltage)
20+
self.assertEqual(14.45, qr.batt_full_voltage)
21+
self.assertEqual(True, qr.load_on)
22+
self.assertEqual(False, qr.load_overload)
23+
self.assertEqual(False, qr.load_short)
24+
self.assertEqual(False, qr.batt_overload)
25+
self.assertEqual(False, qr.batt_overdischarge)
26+
self.assertEqual(False, qr.batt_full)
27+
self.assertEqual(False, qr.batt_charging)
28+
self.assertEqual(21, qr.batt_temp)
29+
self.assertEqual(0.1, qr.charge_current)
30+
31+
class TestQueryResult(TestCase):
32+
def test_decode(self):
33+
qr = QueryResult(query_result)
34+
assertQueryResult(self, qr)
35+
36+
class TestTracerSerial(TestCase):
37+
def setUp(self):
38+
tracer = Tracer(16)
39+
tracer.get_command_bytes=Mock(return_value=fixture_data)
40+
tracer.add_crc = Mock(return_value=fixture_data)
41+
tracer.verify_crc = Mock(return_value=True)
42+
tracer.get_result = Mock(return_value=Command(0x12, fixture_data))
43+
self.ts = TracerSerial(tracer, None)
44+
45+
def test_to_bytes(self):
46+
command = Command(0x12)
47+
command.decode_result = Mock()
48+
result = self.ts.to_bytes(command)
49+
self.assertEqual(bytearray(b'\xAA\x55\xAA\x55\xAA\x55\xEB\x90\xEB\x90\xEB\x90' + fixture_data), result)
50+
51+
def test_from_bytes(self):
52+
result = self.ts.from_bytes(bytearray(b'\xEB\x90\xEB\x90\xEB\x90\x00\x12\x03'+fixture_data + '\x00\x00\x7A'))
53+
self.assertEqual(fixture_data, result.data)
54+
55+
def test_receive_result(self):
56+
class FakePort(object):
57+
def __init__(self, data):
58+
self.data = data
59+
read_idx = 0
60+
def read(self, count=1):
61+
result = self.data[self.read_idx:self.read_idx+count]
62+
self.read_idx += count
63+
return result
64+
self.ts.port = FakePort(full_query_result)
65+
self.ts.from_bytes = Mock(return_result=QueryResult(query_result))
66+
67+
# make the actual call
68+
result = self.ts.receive_result()
69+
70+
self.assertNotEqual(None, result)
71+
self.ts.from_bytes.assert_called_with(full_query_result)
72+
73+
class TestTracer(TestCase):
74+
def setUp(self):
75+
self.t = Tracer(16)
76+
77+
def test_get_command_bytes(self):
78+
result = self.t.get_command_bytes(Command(0x12, fixture_data))
79+
self.assertEqual(bytearray(b'\x10\x12\x03' + fixture_data), result)
80+
81+
def test_get_result(self):
82+
result = self.t.get_result(bytearray(b'\x00\xA0\x18') + query_result)
83+
self.assertEqual(QueryResult, type(result))
84+
85+
if __name__ == '__main__':
86+
unittest2.main()

python/tracer/__init__.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
class Result(object):
1313
"""A command result from the controller."""
14+
props=[]
1415
def __init__(self, data):
1516
self.data = data
1617
self.decode(data)
@@ -23,9 +24,12 @@ def to_float(self, two_bytes):
2324
"""Convert a list of two bytes into a floating point value."""
2425
# convert two bytes to a float value
2526
return ((two_bytes[1] << 8) | two_bytes[0]) / 100.0
27+
def __str__(self):
28+
return "%s{%s}" % (self.__class__.__name__, ", ".join(map(lambda a: "%s: %s" % (a, getattr(self, a)), self.props)))
2629

2730
class QueryResult(Result):
2831
"""The result of a query command."""
32+
props=['batt_voltage', 'pv_voltage', 'load_amps', 'batt_overdischarge_voltage', 'batt_full_voltage', 'load_on', 'load_overload', 'load_short', 'batt_overload', 'batt_overdischarge', 'batt_full', 'batt_charging', 'batt_temp', 'charge_current']
2933
def decode(self, data):
3034
"""Decodes the query result, storing results as fields"""
3135
if len(data) < 23:
@@ -49,7 +53,7 @@ def decode(self, data):
4953

5054
class Command(object):
5155
"""A command sent to the controller"""
52-
def __init__(self, code, data= []):
56+
def __init__(self, code, data=bytearray()):
5357
self.code = code
5458
self.data = data
5559
def decode_result(self, data):
@@ -74,8 +78,8 @@ def __init__(self, state):
7478

7579
class TracerSerial(object):
7680
"""A serial interface to the Tracer"""
77-
sync_header = [0xEB, 0x90] * 3
78-
comm_init = [0xAA, 0x55] * 3 + sync_header
81+
sync_header = bytearray([0xEB, 0x90] * 3)
82+
comm_init = bytearray([0xAA, 0x55] * 3) + sync_header
7983

8084
def __init__(self, tracer, port):
8185
"""Create a new Tracer interface on the given serial port
@@ -87,23 +91,45 @@ def __init__(self, tracer, port):
8791

8892
def to_bytes(self, command):
8993
"""Converts the command into the bytes that should be sent"""
90-
cmd_data = self.tracer.get_command_bytes(command) + [0x00, 0x00, 0x7F]
94+
cmd_data = self.tracer.get_command_bytes(command) + bytearray(b'\x00\x00\x7F')
9195
crc_data = self.tracer.add_crc(cmd_data)
9296
to_send = self.comm_init + crc_data
9397

9498
return to_send
9599

96100
def from_bytes(self, data):
97101
"""Given bytes from the serial port, returns the appropriate command result"""
98-
if list(data[0:6]) != self.sync_header:
102+
if data[0:6] != self.sync_header:
99103
raise Exception("Invalid sync header")
100104
if len(data) != data[8] + 12:
101-
raise Exception("Invalid length")
102-
print list(data[6:])
105+
raise Exception("Invalid length. Expecting %d, got %d" % (data[8] + 12, len(data)))
103106
if not self.tracer.verify_crc(data[6:]):
104-
print "invalid crc"
107+
print "invalid crc"
105108
#raise Exception("Invalid CRC")
106-
return self.tracer.get_command(data[6:])
109+
return self.tracer.get_result(data[6:])
110+
111+
def send_command(self, command):
112+
to_send = self.to_bytes(command)
113+
if len(to_send) != self.port.write(to_send):
114+
raise IOError("Error sending command: did not send all bytes")
115+
116+
def receive_result(self):
117+
buff = bytearray()
118+
read_idx = 0
119+
120+
b = self.port.read(1)
121+
to_read = 200
122+
123+
while b >= 0 and read_idx < (to_read + 12):
124+
buff += b
125+
if read_idx < len(self.sync_header) and b[0] != self.sync_header[read_idx]:
126+
raise IOError("Error receiving result: invalid sync header")
127+
# the location of the read length
128+
elif read_idx == 8:
129+
to_read = b[0]
130+
read_idx += 1
131+
b = self.port.read(1)
132+
return self.from_bytes(buff)
107133

108134
class Tracer(object):
109135
"""An implementation of the Tracer MT-5 communication protocol"""
@@ -116,17 +142,17 @@ def __init__(self, controller_id):
116142
def get_command_bytes(self, command):
117143
"""Given a command, gets its byte representation
118144
119-
This excludes the CRC and trailer."""
120-
data = []
145+
This excludes the header, CRC, and trailer."""
146+
data = bytearray()
121147
data.append(self.controller_id)
122148
data.append(command.code)
123149
data.append(len(command.data))
124150
data += command.data
125151

126152
return data
127153

128-
def get_command(self, data):
129-
if data[1] == 0xA0:
154+
def get_result(self, data):
155+
if data[1] == QueryCommand().code:
130156
return QueryResult(data[3:])
131157

132158
def verify_crc(self, data):
@@ -137,7 +163,6 @@ def verify_crc(self, data):
137163

138164
def add_crc(self, data):
139165
"""Returns a copy of the data with the CRC added"""
140-
data = list(data)
141166
if len(data) < 6:
142167
raise Exception("data are too short")
143168
# the input CRC bytes must be zeroed

0 commit comments

Comments
 (0)