Mercurial > ~darius > hgwebdir.cgi > giant
comparison giant.py @ 0:1f3c12ba927d default tip
Rework code for USB interface
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Sun, 19 Nov 2017 18:10:23 +1030 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:1f3c12ba927d |
---|---|
1 #!/usr/bin/env python | |
2 | |
3 # pip install --user pyusb pycrc | |
4 import crc16 | |
5 import exceptions | |
6 import os | |
7 import usb.core, usb.util, usb.control # https://github.com/pyusb/pyusb | |
8 | |
9 # USB details | |
10 # Borrowed from http://allican.be/blog/2017/01/28/reverse-engineering-cypress-serial-usb.html | |
11 vendorId = 0x0665 | |
12 productId = 0x5161 | |
13 interface = 0 | |
14 | |
15 # Have to add one to these CRCs values before sending | |
16 badvals = [0x28, 0x0d, 0x0a] | |
17 | |
18 class Timeout(exceptions.BaseException): | |
19 pass | |
20 | |
21 class FramingError(exceptions.BaseException): | |
22 pass | |
23 | |
24 class CRCError(exceptions.BaseException): | |
25 pass | |
26 | |
27 class GiantIPS(object): | |
28 PIWS = ['Reserved', 'InverterFault', 'BusOver', 'BusUnder', 'BusSoftFail', 'LineFail', 'OPVShort', 'InverterVoltsLow', | |
29 'InverterVoltsHigh', 'OverTemp', 'FanLocked', 'BattVoltsHigh', 'BattLowAlarm', 'Reserved(Overcharge)', | |
30 'BatterySHutdown', 'Reserved(BattDerate)', 'Overload', 'EEPROM', 'InverterOverCurrent', 'SelfTest', | |
31 'OPDCVoltsOver', 'BattOpen', 'CurrentSenseFail', 'BatteryShort', 'PowerLimit', 'PVVoltsHigh1', | |
32 'MPPTOverload', 'MPPTOverloadWarn', 'BattTooLowChrg', 'PVVoltsHigh2', 'MPPTOverload2', 'MPPTOverloadWarn2', | |
33 'BattTooLowChrg2', 'PVVoltsHigh3', 'MPPTOverload3', 'MPPTOverloadWarn3', 'BattTooLowChrg3'] | |
34 def __init__(self): | |
35 dev = usb.core.find(idVendor = vendorId, idProduct = productId) | |
36 if dev.is_kernel_driver_active(interface): | |
37 dev.detach_kernel_driver(interface) | |
38 dev.set_interface_altsetting(0, 0) | |
39 self.dev = dev | |
40 | |
41 def compose_msg(self, data): | |
42 crc = crc16.crc16xmodem(data) | |
43 crclow = crc & 0xff | |
44 crchigh = crc >> 8 | |
45 if crclow in badvals: | |
46 crclow += 1 | |
47 if crchigh in badvals: | |
48 crchigh += 1 | |
49 data = data + chr(crchigh) + chr(crclow) + chr(0x0d) | |
50 while len(data) < 8: | |
51 data = data + b'\0' | |
52 return data | |
53 | |
54 def tx_msg(self, data): | |
55 self.dev.ctrl_transfer(0x21, 0x9, 0x200, 0, self.compose_msg(data)) | |
56 | |
57 def rx_msg(self): | |
58 res = '' | |
59 tries = 200 | |
60 while tries > 0 and '\r' not in res: | |
61 try: | |
62 d = self.dev.read(0x81, 8, 10) | |
63 res += ''.join([chr(i) for i in d if i != 0x00]) | |
64 except usb.core.USBError as e: | |
65 if e.errno == 110: # timeout | |
66 tries -= 1 | |
67 pass | |
68 else: | |
69 raise | |
70 | |
71 | |
72 if tries == 0: | |
73 raise Timeout() | |
74 if res[0] != '(' or res[-1] != '\r': | |
75 raise FramingError() | |
76 crc = crc16.crc16xmodem(res[0:-3]) | |
77 crclow = crc & 0xff | |
78 crchigh = crc >> 8 | |
79 if ord(res[-3]) != crchigh or ord(res[-2]) != crclow: | |
80 #raise CRCError() | |
81 print('CRC error') | |
82 | |
83 return res[1:-3] | |
84 | |
85 def cmd(self, cmd): | |
86 self.tx_msg(cmd) | |
87 return self.rx_msg() | |
88 | |
89 def getStatus(self): | |
90 d = self.cmd('QPIGS').split() | |
91 status = {} | |
92 if d[16][0] == '1': | |
93 status['SBUPrio'] = True | |
94 else: | |
95 status['SBUPrio'] = False | |
96 if d[16][1] == '1': | |
97 status['ConfigChg'] = True | |
98 else: | |
99 status['ConfigChg'] = False | |
100 if d[16][2] == '1': | |
101 status['BattVoltSteady'] = True | |
102 else: | |
103 status['BattVoltSteady'] = False | |
104 if d[16][3] == '1': | |
105 status['Charging'] = True | |
106 else: | |
107 status['Charging'] = False | |
108 if d[16][4] == '1': | |
109 status['SCC1Charging'] = True | |
110 else: | |
111 status['SCC1Charging'] = False | |
112 if d[16][5:] == '110': | |
113 status['ChargeType'] = 'SCC1' | |
114 elif d[16][5:] == '101': | |
115 status['ChargeType'] = 'AC' | |
116 else: | |
117 status['ChargeType'] = 'Both' | |
118 if d[20][0] == '1': | |
119 status['FloatCharge'] = True | |
120 else: | |
121 status['FloatCharge'] = False | |
122 if d[20][1] == '1': | |
123 status['Switch'] = True | |
124 else: | |
125 status['Switch'] = False | |
126 return { | |
127 'GridVolts' : float(d[0]), | |
128 'GridFreq' : float(d[1]), | |
129 'ACVolts' : float(d[2]), | |
130 'ACFreq' : float(d[3]), | |
131 'ACAppPower' : float(d[4]), | |
132 'ACActPower' : float(d[5]), | |
133 'LoadPct' : float(d[6]), | |
134 'BusVolts' : float(d[7]), | |
135 'BattVolts' : float(d[8]), | |
136 'BattChrCurr' : float(d[9]), | |
137 'BattCap' : float(d[10]), | |
138 'HSTemp' : float(d[11]), | |
139 'PVCurr1' : float(d[12]), | |
140 'PVVolt1' : float(d[13]), | |
141 'SCC1Volt' : float(d[14]), | |
142 'BattDisCurr' : float(d[15]), | |
143 'Status' : status, | |
144 'BattVoltOfs' : float(d[17]) / 0.01, # 10mV | |
145 'PVChrgPow1' : float(d[19]), | |
146 } | |
147 | |
148 def getAlarms(self): | |
149 d = self.cmd('QPIWS') | |
150 res = {} | |
151 for i in range(min(len(d), len(self.PIWS))): | |
152 if d[i] == '1': | |
153 res[self.PIWS[i]] = True | |
154 else: | |
155 res[self.PIWS[i]] = False | |
156 return res | |
157 | |
158 def main(): | |
159 ips = GiantIPS() | |
160 print(ips.cmd('QPI')) | |
161 | |
162 if __name__ == '__main__': | |
163 main() |