changeset 0:1de08e57774f

Initial comit of code to talk to EnerDrive ePro battery monitor
author Daniel O'Connor <darius@dons.net.au>
date Mon, 20 Jul 2015 00:30:01 +0930
parents
children 10bf13dd032f
files .hgignore epro.py sample.dat
diffstat 3 files changed, 262 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Mon Jul 20 00:30:01 2015 +0930
@@ -0,0 +1,2 @@
+.*\.pyc
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/epro.py	Mon Jul 20 00:30:01 2015 +0930
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+
+# View facing ePro from the back
+#   +---+
+# +-|   |-|
+# |       |
+# |       |
+# +-------+
+#  1 ... 6
+#
+# RJ12 plug (Jaycar cable colours)
+# 1 white   temp out
+# 2 black   temp in
+# 3 red     GND
+# 4 green   TX TTL (5V)
+# 5 yellow  RX TTL (5V)
+# 6 blue    VCC (12V)
+#
+# 2400bps 8E1
+# s = serial.Serial('/dev/cu.usbserial-AM01Z7TZ', 2400, parity='E')
+
+class Packet(object):
+    MSGTYPE = -1
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        self.dstadr = dstadr
+        self.srcadr = srcadr
+        self.devid = devid
+        self.msgtype = msgtype
+        self.data = data
+
+    def __repr__(self):
+        hdr = "Src: 0x%02x Dst: 0x%02x DevID: 0x%02x MsgType: 0x%02x" % (
+            self.dstadr, self.srcadr, self.devid, self.msgtype)
+        # MSGTYPE is overridden for subclasses which also have their own repr so don't dumb data here
+        if self.MSGTYPE == -1:
+            hdr += " Data:"
+            for d in self.data:
+                hdr += " 0x%02x" % (d)
+        else:
+            hdr += " Name: " + self.MSGNAME
+
+        return hdr
+
+class MainVoltage(Packet):
+    """Main Battery Voltage"""
+    MSGTYPE = 0x60
+    MSGNAME = "Main Voltage"
+    LEN = 3
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        super(MainVoltage, self).__init__(dstadr, srcadr, devid, msgtype, data)
+        self.volts = (data[2] & 0x7f | (data[1] & 0x7f) << 7 | (data[0] & 0x03) << 14) / 100.0
+
+    def __repr__(self):
+        s = super(MainVoltage, self).__repr__()
+        s += ": %.2f V" % (self.volts)
+        return s
+
+class BatteryCurrent(Packet):
+    """Main Battery Current"""
+    MSGTYPE = 0x61
+    MSGNAME = "Battery Current"
+    LEN = 3
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        super(BatteryCurrent, self).__init__(dstadr, srcadr, devid, msgtype, data)
+        self.amps = (data[2] & 0x7f | (data[1] & 0x7f) << 7 | (data[0] & 0x3f) << 14) / 100.0
+        if data[2] & 0x40:
+            self.amps *= -1
+
+    def __repr__(self):
+        s = super(BatteryCurrent, self).__repr__()
+        s += ": %.2f A" % (self.amps)
+        return s
+
+class AmpHours(Packet):
+    """Number of amp hours removed from the battery"""
+    MSGTYPE = 0x62
+    MSGNAME = "Amp Hours"
+    LEN = 3
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        super(AmpHours, self).__init__(dstadr, srcadr, devid, msgtype, data)
+        self.amphrs = (data[2] & 0x7f | (data[1] & 0x7f) << 7 | (data[0] & 0x3f) << 14) / 100.0
+        if data[2] & 0x40:
+            self.amphrs *= -1
+
+    def __repr__(self):
+        s = super(AmpHours, self).__repr__()
+        s += ": %.2f Ah" % (self.amphrs)
+        return s
+
+class StateOfCharge(Packet):
+    """State of battery charge"""
+    MSGTYPE = 0x64
+    MSGNAME = "State Of Charge"
+    LEN = 3
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        super(StateOfCharge, self).__init__(dstadr, srcadr, devid, msgtype, data)
+        self.soc = (data[2] & 0x7f | (data[1] & 0x7f) << 7 | (data[0] & 0x03) << 14) / 10.0
+
+    def __repr__(self):
+        s = super(StateOfCharge, self).__repr__()
+        s += ": %.2f%%" % (self.soc)
+        return s
+
+class TimeRemaining(Packet):
+    """Time remaining until battery needs charging"""
+    MSGTYPE = 0x65
+    MSGNAME = "Time remaining"
+    LEN = 3
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        super(TimeRemaining, self).__init__(dstadr, srcadr, devid, msgtype, data)
+        self.time = (data[2] & 0x7f | (data[1] & 0x7f) << 7 | (data[0] & 0x3f) << 14)
+        if data[2] & 0x40:
+            self.time *= -1
+
+    def __repr__(self):
+        s = super(TimeRemaining, self).__repr__()
+        s += ": %.2f min" % (self.time)
+        return s
+
+class BatteryTemperature(Packet):
+    """Battery temperature (degrees Celcius)"""
+    MSGTYPE = 0x66
+    MSGNAME = "Battery Temperature"
+    LEN = 3
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        super(BatteryTemperature, self).__init__(dstadr, srcadr, devid, msgtype, data)
+        self.time = (data[2] & 0x7f | (data[1] & 0x7f) << 7 | (data[0] & 0x3f) << 14) / 10.0
+        if data[2] & 0x40:
+            self.time *= -1
+
+    def __repr__(self):
+        s = super(BatteryTemperature, self).__repr__()
+        s += ": %.2f degC" % (self.time)
+        return s
+
+class MonitorStatus(Packet):
+    """Monitor status"""
+    MSGTYPE = 0x67
+    MSGNAME = "Monitor Status"
+    LEN = 3
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        super(MonitorStatus, self).__init__(dstadr, srcadr, devid, msgtype, data)
+        self.autosyncvolt = data[0] & 0x10
+        self.autosyncamp = data[0] & 0x08
+        self.autosyncchrg = data[0] & 0x04
+        self.e501compat = data[0] & 0x02
+        self.alarmtst = data[0] & 0x01
+        self.backlight = data[1] & 0x40
+        self.disptst = data[1] & 0x20
+        self.tempsense = data[1] & 0x10 # Seems to be inverted from data sheet
+        self.auxhv = data[1] & 0x08
+        self.auxlv = data[1] & 0x04
+        self.lock = data[1] & 0x02
+        self.mainhv = data[1] & 0x01
+        self.mainlv = data[2] & 0x40
+        self.lowbatalarm = data[2] & 0x20
+        self.batflat = data[2] & 0x10
+        self.batfull = data[2] & 0x08
+        self.charged = data[2] & 0x04
+        self.nosync = data[2] & 0x02
+        self.monreset = data[2] & 0x01
+
+    def __repr__(self):
+        s = super(MonitorStatus, self).__repr__()
+        stats = ( ( "ASV", self.autosyncvolt ), ( "ASA", self.autosyncamp ), ( "ASC" , self.autosyncchrg ),
+          ( "E501", self.e501compat ), ( "Alarm test", self.alarmtst ), ( "Light", self.backlight ),
+          ( "Temperature Sensor", self.tempsense ), ( "Aux HV", self.auxhv ), ( "Aux LV", self.auxlv ),
+          ( "Lock", self.lock ), ( "Main HV", self.mainhv ), ( "Main LV", self.mainlv ),
+          ( "Low Battery", self.lowbatalarm ), ( "Battery Flat", self.batfull ),
+          ( "Battery Full", self.batfull ), ( "Battery Charged", self.charged ),
+          ( "No Sync", self.nosync ), ( "Monitor Reset", self.monreset ) )
+        for (short, var) in stats:
+            if var:
+                s += " %s : True" % (short)
+            else:
+                s += " %s : False" % (short)
+
+        return s
+
+class AuxVoltage(Packet):
+    """Aux Battery Voltage"""
+    MSGTYPE = 0x68
+    MSGNAME = "Aux Voltage"
+    LEN = 3
+
+    def __init__(self, dstadr, srcadr, devid, msgtype, data):
+        super(AuxVoltage, self).__init__(dstadr, srcadr, devid, msgtype, data)
+        self.volts = (data[2] & 0x7f | (data[1] & 0x7f) << 7 | (data[0] & 0x03) << 14) / 100.0
+
+    def __repr__(self):
+        s = super(AuxVoltage, self).__repr__()
+        s += ": %.2f V" % (self.volts)
+        return s
+
+class Processor(object):
+    PKT_TYPES = { MainVoltage.MSGTYPE : MainVoltage, BatteryCurrent.MSGTYPE : BatteryCurrent,
+                  AmpHours.MSGTYPE : AmpHours, StateOfCharge.MSGTYPE : StateOfCharge,
+                  TimeRemaining.MSGTYPE : TimeRemaining, BatteryTemperature.MSGTYPE : BatteryTemperature,
+                  MonitorStatus.MSGTYPE : MonitorStatus, AuxVoltage.MSGTYPE : AuxVoltage }
+
+    def __init__(self):
+        self.state = 0
+        self.packets = []
+
+    def process(self, dat):
+        for d in dat:
+            d = ord(d)
+            if d == 0xff and self.state != 4:
+                print "Packet corruption"
+                continue
+
+            if self.state == 0:
+                # Waiting for destination address (MSB set but not 0xff as that is EOM)
+                if d == 0xff or d & 0x80 == 0:
+                    print "Skipping byte"
+                    continue
+                self.dstadr = d & 0x7f
+                self.data = []
+                self.state += 1
+            elif self.state == 1:
+                # Source address
+                self.srcadr = d
+                self.state += 1
+            elif self.state == 2:
+                # Device ID
+                self.devid = d
+                self.state += 1
+            elif self.state == 3:
+                # Message type
+                self.msgtype = d
+                self.state += 1
+            elif self.state == 4:
+                # Data
+                if d != 0xff:
+                    self.data.append(d)
+                    continue
+                self.state = 0
+                if self.msgtype in Processor.PKT_TYPES:
+                    t = self.PKT_TYPES[self.msgtype]
+                    if len(self.data) != t.LEN:
+                        print "Packet length incorrect, expected %d got %d" % (t.LEN, len(self.data))
+                        continue
+
+                    p = self.PKT_TYPES[self.msgtype](self.dstadr, self.srcadr, self.devid, self.msgtype, self.data)
+                else:
+                    p = Packet(self.dstadr, self.srcadr, self.devid, self.msgtype, self.data)
+
+                print p
+                self.packets.append(p)
+
+if __name__ == '__main__':
+    main()
Binary file sample.dat has changed