diff --git a/Readme.md b/Readme.md index 8e0710b..bf74728 100644 --- a/Readme.md +++ b/Readme.md @@ -27,47 +27,93 @@ deactivate ``` ## Usage: -At the moment you have to use the python shell. I script for nicer commands is on the way. - -### listen to a single packet -``` python -from thingset.cansocket import CANsocket -sock = CANsocket('vcan0') # or other interface -frame = sock.receive() -print(frame.data) # raw data -print(frame.cbor) # cbor decoded data -print(frame.priority) -print(frame.dataobjectID) -print(frame.source) + +### Initialization + +```python +from thingset.thingset_can import ThingSet_CAN + +# Define some addresses +own_id = 0x01 +bms_id = 0x0A +mppt_id = 0x14 + +# Create ThingSet client object with CAN interface and source ID +ts = ThingSet_CAN(if_name='can0', own_id=own_id) +# Subscribe to publication messages of specific devices +ts.subscribe(bms_id) +ts.subscribe(mppt_id) +# Start reception process +rx_thread.start() ``` -### listen to packets in a loop: -``` python -from thingset.cansocket import CANsocket -sock = CANsocket('vcan0') # or other interface -while(True) - frame = sock.receive() +### Callback Mechanism + +It is possible to register a callback function that is called on every received publication frame to process the frame directly after reception. + +```python +def pub_callback(src:int, id:int, data): + '''Callback function for received Publication Frames''' + print(f'Message from ID {src} -> {id}: {data}') + +ts.setPubCallback(pub_callback) +``` + +### Request API + +* Return data is dependent on request type + * can be: + * status code (string) + * bool + * int + * float + * string + * array (Python List) + * map (Python Dictionary) + +#### get + +```python +# Retrieve all data from a path +meas_id = ts.get(bms_id, 0x02))) +meas_str = ts.get(bms_id, 'meas'))) + +# Retrieve single data items +deviceID = ts.get(bms_id, 0x1D))) +can_enable = ts.get(bms_id, 0xF6))) +bat_V = ts.get(bms_id, 0x71))) + +# Retrieve array data items +cell_V = ts.get(bms_id, 0x80))) ``` -### parse can trace -Assuming the trace file ist called "trace123.csv": -``` python -from thingset.parser import playback -playback('trace123.csv") +#### fetch + +```python +# fetch BatNom_Ah from CONF Path +batNom_Ah = ts.fetch(bms_id, 0x06, [0x31]) + +# fetch multiple items with an array +response = ts.fetch(bms_id, 0x06, [0x31, 0x41]) ``` -## send dummy data via cansend to test: -``` shell -cansend vcan0 014#8000FA3FF33333 -cansend vcan0 014#8000820C16 -cansend vcan0 014#8000C48221196AB3 -cansend vcan0 014#8080C48221196AB3 +#### post + +```python +# call print-registers function +response = ts.post(bms_id, 0xEC, []) + +# call print-register function with argument +response = ts.post(bms_id, 0xEA, [0x4B]))) + +# Athenticate +response = ts.post(bms_id, 0xEE, ["maker456"]) +response = ts.post(bms_id, 0xEE, ["expert123"]) ``` -The output should look like this: -``` shell -Message from ID 20 -> 0: 1.899999976158142 -Message from ID 20 -> 0: [12, 22] -Message from ID 20 -> 0: 273.15 -Error receiving package from ID 20. Error code: 0x80 +#### patch + +```python +# iPatch (bool) - can.enable = False +response = ts.patch(bms_id, 0xF5, {0xF6: False}) ``` diff --git a/test.py b/test.py index 04ebbf7..a5170a1 100755 --- a/test.py +++ b/test.py @@ -1,10 +1,99 @@ +# Tested with BMS-8S50-IC +# HW v0.1.1 +# FW v21.0-57-g6c0fb5f-dirty -from thingset.cansocket import CANsocket -sock = CANsocket('can0') # or other interface +import threading +import time +from thingset.thingset_can import ThingSet_CAN -while(True): - frame = sock.receive() - if isinstance(frame.cbor, float): - print("device: 0x%x data id: 0x%x value: %.2f" % (frame.source, frame.dataobjectID, frame.cbor)) - else: - print("device:", hex(frame.source), " data id:", hex(frame.dataobjectID), " value:", frame.cbor) +own_id = 0x01 +bms_id = 0x0A +mppt_id = 0x14 + + +def pub_callback(src: int, id: int, data): + '''Callback function for received Publication Frames''' + print(f'Message from ID {src} -> {id}: {data}') + + +ts = ThingSet_CAN(if_name='can0', own_id=own_id) +ts.subscribe(bms_id) +ts.subscribe(mppt_id) +ts.setPubCallback(pub_callback) +ts.start() + +print("Python ThingSet Client for CAN (Binary Mode)") +print("--------------------------------------------") + +# WORKING ===================================================== + +# GET +print(" > Get - INFO Path: " + str(ts.get(bms_id, 0x01))) +print(" > Get - INFO Path: " + str(ts.get(bms_id, 'info'))) +print(" > Get - MEAS Path: " + str(ts.get(bms_id, 0x02))) +print(" > Get - MEAS Path: " + str(ts.get(bms_id, 'meas'))) +print(" > Get - INPUT Path: " + str(ts.get(bms_id, 0x05))) +print(" > Get - CONF Path: " + str(ts.get(bms_id, 0x06))) +print(" > Get - can.Enable: " + str(ts.get(bms_id, 0xF6))) +print(" > Get - meas.Bat_V: " + str(ts.get(bms_id, 0x71))) +print(" > Get - meas.Cell_V: " + str(ts.get(bms_id, 0x80))) +print(" > Get - DeviceID: " + str(ts.get(bms_id, 0x1D))) + +# PATCH +print(" > iPatch (bool) - Discharge disable: " + str(ts.patch(bms_id, 0x05, {0x61: False}))) +print(" > iPatch (bool) - Discharge enable: " + str(ts.patch(bms_id, 0x05, {0x61: True}))) +print(" > iPatch (string) - Password = abcd: " + str(ts.patch(bms_id, 0xEE, {0xEF: 'abcd'}))) +print(" iPatch (int) - PcbDisSC_us = 200: " + str(ts.patch(bms_id, 0x06, {0x41: 200}))) +print(" iPatch (bool) - .pub.can.enable = False: " + str(ts.patch(bms_id, 0xF5, {0xF6: False}))) +print(" iPatch (bool) - .pub.can.enable = True: " + str(ts.patch(bms_id, 0xF5, {0xF6: True}))) + +# FETCH +print(" > Fetch - Request BatNom_Ah from CONF Path: " + str(ts.fetch(bms_id, 0x06, [0x31]))) +print(" > Fetch - Request Array: " + str(ts.fetch(bms_id, 0x06, [0x31, 0x41]))) + +# POST - Execute Function +print(" > Post - print-registers: " + str(ts.post(bms_id, 0xEC, []))) +print(" > Post - print-register: " + str(ts.post(bms_id, 0xEA, [0x4B]))) +print(" > Post - auth: " + str(ts.post(bms_id, 0xEE, ["maker456"]))) +print(" > Post - auth: " + str(ts.post(bms_id, 0xEE, ["expert123"]))) + +# ISSUES ======================================================= + +# print(" > Fetch - MEAS item IDs: " + str(ts.fetch(bms_id, 0x02, 0xF7))) +# Returs: Error - Response too large (0xE1) + +# print(" > Post - Append : " + str(ts.post(bms_id, 0x100, 0x74))) + +# print(" > Get - .pub Path: " + str(ts.get(bms_id, 0x100))) +# returns: 1EDA010A 06 85 A2 18 F1 18 F5 +# cbor2.loads(b'\xa2\x18\xf1\x18\xf5') -> premature end of stream (expected to read 1 bytes, got 0 instead) + +# print(" > Get - serial: " + str(ts.get(bms_id, 0xF3))) +# Returns: 1EDA010A 01 85 +# print(" > Get - can: " + str(ts.get(bms_id, 0xF7))) +# Returns: 1EDA010A 01 85 + +# print(" > Get - serial: " + str(ts.get(bms_id, 0xF1))) +# Returns: 1EDA010A 07 85 A2 18 F3 18 F2 F4 +# cbor2.loads(b'\xa2\x18\xf3\x18\xf2\xf4') -> premature end of stream (expected to read 1 bytes, got 0 instead) +# print(" > Get - can: " + str(ts.get(bms_id, 0xF5))) +# Returns: 1EDA010A 07 85 A2 18 F7 18 F6 F5 +# cbor2.loads(b'\xa2\x18\xf7\x18\xf6\xf5') -> premature end of stream (expected to read 1 bytes, got 0 instead) + +# Fetch Names returns values not strings (diff from spec) +# print(" > Fetch - Names: " + str(ts.fetch(bms_id, 0x17, [0x40, 0x41]))) +# returns: 1EDA010A 10 09 85 82 FA 00 00 00 +# 1EDA010A 21 BE 18 C8 + +# ============================================================== + +print("Exit with Ctrl+C ") + +try: + while True: + time.sleep(1) +except KeyboardInterrupt: + print('interrupted!') + +print("Latest BMS Pub. Data: " + str(ts.data[bms_id])) +print("Latest MPPT Pub. Data: " + str(ts.data[mppt_id])) diff --git a/thingset/cansocket.py b/thingset/cansocket.py index 39f3495..ab4c3c7 100644 --- a/thingset/cansocket.py +++ b/thingset/cansocket.py @@ -1,25 +1,202 @@ import socket import struct -from thingset.packet import TSPacket, SingleFrame +import time +from cbor2 import loads +from thingset.packet import TSPacket, SingleFrame, RequestFrame + class CANsocket(object): FMT = ' None: + if addr not in range(0, 256): + raise ValueError( + "Address must be integer between 0 and 255 (got: {})".format(addr)) + if not addr in self.sub_addresses: + self.sub_addresses.append(addr) - def receive(self): + def receive(self) -> tuple: + '''Receive function with ThingSet/ISO-TP Frame handling + Returns a tuple with 4 entries: + 1: Is Publication Frame: bool + 2: Status or Data Object ID: int + 3: Data: Deserialized Object (bool, int, float, list, dict) + 4: Publication Frame Source ID + ''' + ret = None packet = self.s.recv(64) can_id, length, data = struct.unpack(self.FMT, packet) can_id &= socket.CAN_EFF_MASK - frame = SingleFrame(data=data) + if (can_id & TSPacket.TS_FRAME_FLAG): + frame = SingleFrame(data=data) frame.parseIdentifier(can_id) + if frame.source in self.sub_addresses: + ret = (True, frame.dataobjectID, frame.cbor, frame.source) + else: + frame = RequestFrame(data=data) + frame.parseIdentifier(can_id) + if frame.destination == self.address: + if frame.type == RequestFrame.FRAME_TYPE_SINGLE: + status = self.status_code[data[1]] + if status == 'Content': + ret = (False, status, loads(frame.data[2:]), None) + else: + ret = (False, status, None, None) + if frame.type == RequestFrame.FRAME_TYPE_FIRST: + self.packetsize = frame.framesize + self.databuffer.clear() + self.databuffer.extend(frame.data[2:]) + self.last_index = 0 + fc_frame = RequestFrame( + src=self.address, dst=frame.source, data=b'\x30\x00\x00') + self.send(fc_frame) + if frame.type == RequestFrame.FRAME_TYPE_CONSEC: + expected_index = (self.last_index + 1) % 16 + if frame.index == expected_index: + self.last_index = expected_index + self.databuffer.extend(data[1:]) + if len(self.databuffer) >= self.packetsize: + status = self.status_code[self.databuffer[0]] + ret = (False, status, + (loads(self.databuffer[1:])), None) + else: + print("Received Consecutive Frame with invalid Index.") + ret = (False, None, None, None) + + if frame.type == RequestFrame.FRAME_TYPE_FLOWC: + self.flow_control = (frame.fcflag, frame.blocksize, frame.delay) - return(frame) + return ret - def send(self, message): + def send(self, message: RequestFrame) -> bool: + '''Send function with basic ThingSet/ISO-TP handling + Returns True on Success + ''' can_id = message.identifier | socket.CAN_EFF_FLAG - can_packet = struct.pack(self.FMT, can_id, len(message.data), message.data) - self.s.send(can_packet) + + if message.type == RequestFrame.FRAME_TYPE_FLOWC: + can_packet = struct.pack( + self.FMT, can_id, len(message.data), message.data) + self.s.send(can_packet) + else: + if len(message.data) <= 7: + # data fits in single frame + frame_data = bytearray() + frame_data.append(len(message.data)) + frame_data.extend(message.data) + can_packet = struct.pack( + self.FMT, can_id, len(frame_data), frame_data) + self.s.send(can_packet) + else: + # First Frame + data = bytearray(message.data) + index = 0 + block = 0 + length = len(data) + len_bytes = length.to_bytes(2, byteorder='little') + frame_data = bytearray() + frame_data.append(0x10 | len_bytes[1]) + frame_data.append(len_bytes[0]) + frame_data.extend(data[:6]) + del data[:6] + can_packet = struct.pack(self.FMT, can_id, 8, frame_data) + self.s.send(can_packet) + if self.waitFC(): + # Consecutive Frames + flag, blocksize, delay_s = self.evalFCFlags() + if flag == 'Abort': + print("Abort by Flow Control Flag from Device") + return False + while len(data) != 0: + if len(data) >= 7: + frame_data = bytearray() + index += 1 % 16 + frame_data.append(0x20 | index) + frame_data.extend(data[:7]) + del data[:7] + self.s.send(struct.pack( + self.FMT, can_id, len(frame_data), frame_data)) + block += 1 + if block == blocksize: + block = 0 + if self.waitFC(): + flag, blocksize, delay_s = self.evalFCFlags() + if flag == 'Abort': + print("Abort by Flow Control Flag from Device") + return False + else: + print("Flow Control Timeout while sending") + return False + else: + time.sleep(delay_s) + else: + frame_data = bytearray() + index += 1 % 16 + frame_data.append(0x20 | index) + frame_data.extend(data) + self.s.send(struct.pack( + self.FMT, can_id, len(frame_data), frame_data)) + del data[:] + else: + print("Flow Control Timeout before sending") + return False + return True + + def waitFC(self, timeout: float = 0.5): + '''Wait for Flow Control Frame and return False on Timeout and True on Success''' + ret = bool(False) + timeout = time.perf_counter() + timeout + while time.perf_counter() <= timeout: + if self.flow_control: + ret = True + break + time.sleep(0.001) + return ret + + def evalFCFlags(self): + fcflag, blocksize, delay_code = self.flow_control + self.flow_control = None + if delay_code > 0 and delay_code <= 127: + delay_s = float(delay_code) / 1000.0 + elif delay_code >= 0xF1 and delay_code <= 0xF9: + delay_s = float(delay_code & 0xF) / 10000.0 + else: + delay_s = 0.0 + if fcflag == 'Wait': + time.sleep(1.0) + return (fcflag, blocksize, delay_s) diff --git a/thingset/packet.py b/thingset/packet.py index 2e96a91..0245ac0 100644 --- a/thingset/packet.py +++ b/thingset/packet.py @@ -1,6 +1,7 @@ from socket import CAN_EFF_FLAG from cbor2 import loads + class TSPacket(object): TS_FRAME_FLAG = (1 << 24) @@ -14,8 +15,9 @@ def source(self): @source.setter def source(self, source): - if source not in range(0,256): - raise ValueError("Source ID must be integer between 0 and 255 (got: {})".format(source)) + if source not in range(0, 256): + raise ValueError( + "Source ID must be integer between 0 and 255 (got: {})".format(source)) self._source = source @property @@ -25,7 +27,8 @@ def timestamp(self): @timestamp.setter def timestamp(self, timestamp): if not isinstance(timestamp, float): - raise TypeError("Timestamp must be float (got: {})".format(type(timestamp))) + raise TypeError( + "Timestamp must be float (got: {})".format(type(timestamp))) self._timestamp = timestamp @@ -46,8 +49,9 @@ def dataobjectID(self): @dataobjectID.setter def dataobjectID(self, dataobjectID): - if not dataobjectID in range(0,65537): - raise ValueError("Data object ID must be integer between 0 and 65536 (got: {}).".format(dataobjectID)) + if not dataobjectID in range(0, 65537): + raise ValueError( + "Data object ID must be integer between 0 and 65536 (got: {}).".format(dataobjectID)) self._dataobjectID = dataobjectID @@ -65,9 +69,11 @@ def __init__(self, data=None, dataobjectID=0, priority=6, source=0, timestamp=0. def parseIdentifier(self, identifier): if not isinstance(identifier, int): - raise ValueError("Identifier must be integer, not {}.".format(identifier)) + raise ValueError( + "Identifier must be integer, not {}.".format(identifier)) if identifier >= (1 << 30): - raise ValueError("Identifier too big. Cannot contain more than 29 bits") + raise ValueError( + "Identifier too big. Cannot contain more than 29 bits") if not (identifier & TSPacket.TS_FRAME_FLAG): raise ValueError("Not a publication message.") self.priority = identifier >> 26 @@ -97,10 +103,115 @@ def data(self, data): if data is None: self._data = bytes() elif not isinstance(data, bytes): - raise TypeError("Wrong data type. Must be bytes, not {}".format(type(data))) + raise TypeError( + "Wrong data type. Must be bytes, not {}".format(type(data))) self._data = data self._cbor = loads(self._data) @property def cbor(self): return self._cbor + + +class ServiceFrame(TSPacket): + def __init__(self, priority=7, destination=0, source=0): + super().__init__() + self._messageType = False + self.priority = priority + self.source = source + self.destination = destination + + @property + def messageType(self): + return "Service message" + + @property + def priority(self): + return self._priority + + @priority.setter + def priority(self, priority): + if priority not in range(0, 8): + raise ValueError( + "Priority must be integer between 0 and 7 (got: {})".format(priority)) + self._priority = priority + + @property + def destination(self): + return self._destination + + @destination.setter + def destination(self, destination): + if destination not in range(0, 256): + raise ValueError( + "Destination ID must be integer between 0 and 255 (got: {})".format(destination)) + self._destination = destination + + +class RequestFrame(ServiceFrame): + SINGLE_ID_MASK = (0b10 << 24) + + FRAME_TYPE_SINGLE = 0x0 + FRAME_TYPE_FIRST = 0x1 + FRAME_TYPE_CONSEC = 0x2 + FRAME_TYPE_FLOWC = 0x3 + + def __init__(self, priority=7, src=0, dst=0, data=bytes()): + super().__init__() + self.priority = priority + self.source = src + self.destination = dst + self.type = (data[0] & 0xf0) >> 4 + self.data = data + + if self.type == self.FRAME_TYPE_SINGLE: + self.framesize = data[0] & 0xf + if self.type == self.FRAME_TYPE_FIRST: + self.framesize = ((data[0] & 0xF) << 8) | data[1] + if self.type == self.FRAME_TYPE_CONSEC: + self.index = data[0] & 0xf + if self.type == self.FRAME_TYPE_FLOWC: + self.fcflag = data[0] & 0xf + self.blocksize = data[1] + self.delay = data[2] + + def parseIdentifier(self, identifier): + if not isinstance(identifier, int): + raise ValueError( + "Identifier must be integer, not {}.".format(identifier)) + if identifier >= (1 << 30): + raise ValueError( + "Identifier too big. Cannot contain more than 29 bits") + if (identifier & TSPacket.TS_FRAME_FLAG): + raise ValueError("Not a request/response message.") + self.priority = identifier >> 26 + self.destination = (identifier & 0xffff) >> 8 + self.source = identifier & 0xff + + @property + def identifier(self): + id_prio = self._priority << 26 + id_fixed = 0xDA << 16 + id_dst = self._destination << 8 + return id_prio | self.SINGLE_ID_MASK | id_fixed | id_dst | self.source + + @property + def type(self): + return self._type + + @type.setter + def type(self, type): + self._type = type + + @property + def data(self): + return self._data + + @data.setter + def data(self, data): + if data is None: + self._data = bytes() + elif not isinstance(data, bytes): + raise TypeError( + "Wrong data type. Must be bytes, not {}".format(type(data))) + self._data = data diff --git a/thingset/thingset_can.py b/thingset/thingset_can.py new file mode 100644 index 0000000..4f55c94 --- /dev/null +++ b/thingset/thingset_can.py @@ -0,0 +1,162 @@ +import threading +import time +from thingset.cansocket import CANsocket +from thingset.packet import RequestFrame +from cbor2 import dumps + + +class ThingSet_CAN(threading.Thread): + + __response = None + __own_id = int() + __pub_callback = None + + data = dict() + + def __init__(self, if_name: str, own_id: int = 0x01): + super().__init__() + if own_id not in range(0, 256): + raise ValueError( + "Address must be integer between 0 and 255 (got: {})".format(own_id)) + self.__own_id = own_id + self.__sock = CANsocket(if_name, self.__own_id) # or other interface + self.setDaemon(True) + + def setPubCallback(self, callback): + if hasattr(callback, '__call__'): + self.__pub_callback = callback + + def subscribe(self, addr: int): + self.data[addr] = {} + self.__sock.subscribe(addr) + + @staticmethod + def translate(data:dict, id_name_map:dict): + '''Translates data with id as keys to data with strings as keys''' + str_dict = None + try: + str_dict = dict((id_name_map[key], value) for (key, value) in data.items()) + except KeyError as e: + print(f'Cannot translate ID! Maybe missing in map. {KeyError}') + return str_dict + + def get(self, addr: int, id: int): + '''Retrieve all data from a path''' + ret = None + if addr not in range(0, 256): + raise ValueError( + "Address must be integer between 0 and 255 (got: {})".format(addr)) + data = bytearray(b'\x01') + data.extend(dumps(id)) + frame = RequestFrame(src=self.__own_id, dst=addr, data=bytes(data)) + self.__sock.send(frame) + if self.__waitResponse(0.5): + ret = self.__getResponse() + else: + print("GET Timeout!") + return ret + + def post(self, addr: int, id: int, data): + '''Append data to an object or execute a function''' + ret = None + if addr not in range(0, 256): + raise ValueError( + "Address must be integer between 0 and 255 (got: {})".format(addr)) + frame_data = bytearray(b'\x02') + frame_data.extend(dumps(id)) + frame_data.extend(dumps(data)) + frame = RequestFrame(src=self.__own_id, dst=addr, + data=bytes(frame_data)) + self.__sock.send(frame) + if self.__waitResponse(0.5): + ret = self.__getResponse() + else: + print("POST Timeout!") + return ret + + def delete(self, addr: int, data): + '''Delete data from an object''' + ret = None + if addr not in range(0, 256): + raise ValueError( + "Address must be integer between 0 and 255 (got: {})".format(addr)) + frame_data = bytearray(b'\x04') + frame_data.extend(dumps(data)) + frame = RequestFrame(src=self.__own_id, dst=addr, + data=bytes(frame_data)) + self.__sock.send(frame) + if self.__waitResponse(0.5): + ret = self.__getResponse() + else: + print("DELETE Timeout!") + return ret + + def fetch(self, addr: int, path_id: int, array: list): + '''Retrieve a subset of data from a path''' + ret = None + if addr not in range(0, 256): + raise ValueError( + "Address must be integer between 0 and 255 (got: {})".format(addr)) + data = bytearray(b'\x05') + data.extend(dumps(path_id)) + data.extend(dumps(array)) + frame = RequestFrame(src=self.__own_id, dst=addr, data=bytes(data)) + self.__sock.send(frame) + if self.__waitResponse(1.5): + ret = self.__getResponse() + else: + print("FETCH Timeout!") + return ret + + def patch(self, addr: int, obj_id: int, map: dict): + '''Update (overwrite) data of a path''' + ret = None + if addr not in range(0, 256): + raise ValueError( + "Address must be integer between 0 and 255 (got: {})".format(addr)) + data = bytearray(b'\x07') + data.extend(dumps(obj_id)) + data.extend(dumps(map)) + self.__sock.send(RequestFrame( + src=self.__own_id, dst=addr, data=bytes(data))) + if self.__waitResponse(): + ret = self.__getResponse() + else: + print("PATCH Timeout!") + + return ret + + def __waitResponse(self, timeout: float = 1.5): + '''Wait for response and return False on Timeout and True on Success''' + ret = bool(False) + timeout = time.perf_counter() + timeout + while time.perf_counter() <= timeout: + if self.__response != None: + ret = True + break + time.sleep(0.001) + return ret + + def __getResponse(self): + '''Returns Response Code/Response Data''' + status, data = self.__response + self.__response = None + if data == None: + ret = status + else: + ret = data + return ret + + def run(self): + '''Reception Thread Function''' + while True: + rx = self.__sock.receive() + if rx != None: + pub_frame, status, data, src_id = rx + if pub_frame == True: + id = status + self.data[src_id].update({id: data}) + if self.__pub_callback != None: + self.__pub_callback(src_id, id, data) + else: + self.__response = (status, data) diff --git a/thingset_log.py b/thingset_log.py index 3b6229b..a4ccd05 100755 --- a/thingset_log.py +++ b/thingset_log.py @@ -1,10 +1,8 @@ - import json import time from datetime import datetime -from thingset.cansocket import CANsocket -sock = CANsocket('can0') # or other interface +from thingset.thingset_can import ThingSet_CAN mppt_id = 0x14 bms_id = 0xa @@ -32,7 +30,7 @@ 0x71: 'Bat_V', 0x72: 'Bat_A', 0x73: 'Bat_degC', - 0x74: 'IC_degC', + 0x76: 'MOSFETs_degC', 0x7C: 'SOC_pct', 0x7E: 'ErrorFlags', 0x7F: 'BmsState', @@ -42,15 +40,12 @@ 0x9D: 'BalancingStatus', } -mppt_data = {} -mppt_updated = False -mppt_file = open("data/%s_mppt.csv" % datetime.now().strftime("%Y%m%d_%H%M%S"), "a") +mppt_file = open("data/%s_mppt.csv" % + datetime.now().strftime("%Y%m%d_%H%M%S"), "a") -bms_data = {} -bms_updated = False -bms_file = open("data/%s_bms.csv" % datetime.now().strftime("%Y%m%d_%H%M%S"), "a") +bms_file = open("data/%s_bms.csv" % + datetime.now().strftime("%Y%m%d_%H%M%S"), "a") -last_update = int(time.time()) def csv_header(file, id_map): names = ['Timestamp_s'] @@ -58,6 +53,7 @@ def csv_header(file, id_map): names.append(id_map[key]) file.write(','.join(names) + '\n') + def csv_data(file, id_map, timestamp, data): values = [str(timestamp)] for key in id_map: @@ -68,55 +64,48 @@ def csv_data(file, id_map, timestamp, data): file.write(','.join(values) + '\n') file.flush() + +ts = ThingSet_CAN(if_name='can0') +ts.subscribe(bms_id) +ts.subscribe(mppt_id) +ts.start() + csv_header(mppt_file, mppt_map) csv_header(bms_file, bms_map) +last_update = int(time.time()) + try: while True: - frame = sock.receive() - - data_name = str(frame.dataobjectID) - if frame.source == mppt_id and frame.dataobjectID in mppt_map: - data_name = mppt_map[frame.dataobjectID] - mppt_data[data_name] = frame.cbor - mppt_updated = True - elif frame.source == bms_id and frame.dataobjectID in bms_map: - data_name = bms_map[frame.dataobjectID] - bms_data[data_name] = frame.cbor - bms_updated = True + time.sleep(1) now = int(time.time()) - # store data in CSV file - if now > last_update: - try: - print("BMS: Bat %.2fV %.2fA, Cells %.2fV < %.2fV < %.2fV, Err %d " % ( \ - bms_data['Bat_V'], bms_data['Bat_A'], bms_data['CellMin_V'], \ - bms_data['CellAvg_V'], bms_data['CellMax_V'], bms_data['ErrorFlags']), end='' \ - ) - except: - pass - - try: - print("MPPT: Bat %.2fV %.2fA, Solar %.2fV, Load %.2fA, Err %d" % ( \ - mppt_data['Bat_V'], mppt_data['Bat_A'], mppt_data['Solar_V'], \ - mppt_data['Load_A'], mppt_data['ErrorFlags']) \ - ) - except: - print("") - #pass - - if mppt_updated: - #print(json.dumps(mppt_data)) - csv_data(mppt_file, mppt_map, now, mppt_data) - mppt_data = {} - mppt_updated = False - if bms_updated: - #print(json.dumps(bms_data)) - csv_data(bms_file, bms_map, now, bms_data) - bms_data = {} - bms_updated = False - last_update = now + try: + bms_data = ThingSet_CAN.translate(ts.data[bms_id], bms_map) + csv_data(bms_file, bms_map, now, bms_data) + #print(json.dumps(bms_data)) + print("BMS: Bat %.2fV %.2fA, Cells %.2fV < %.2fV < %.2fV, Err %d" % ( + bms_data['Bat_V'], bms_data['Bat_A'], bms_data['CellMin_V'], + bms_data['CellAvg_V'], bms_data['CellMax_V'], bms_data['ErrorFlags']), end='' + ) + except KeyError as exc: + #print(exc) + pass + + try: + mppt_data = ThingSet_CAN.translate(ts.data[mppt_id], mppt_map) + csv_data(mppt_file, mppt_map, now, mppt_data) + # print(json.dumps(mppt_data)) + print("MPPT: Bat %.2fV %.2fA, Solar %.2fV, Load %.2fA, Err %d" % ( + mppt_data['Bat_V'], mppt_data['Bat_A'], mppt_data['Solar_V'], + mppt_data['Load_A'], mppt_data['ErrorFlags']) + ) + except KeyError as exc: + # print(exc) + print("") + + except KeyboardInterrupt: mppt_file.close()