From b155a58374c4bbb923a06b3fe98ee02a161efa9f Mon Sep 17 00:00:00 2001 From: Yeongdo Park Date: Sun, 29 Nov 2020 10:07:37 -0800 Subject: [PATCH] 36-cell (3-slave) voltage monitoring/logging version --- .idea/misc.xml | 2 +- .idea/wxGladeTest.iml | 2 +- BmsApp.py | 127 ++++++++++++++++++++-------------- VoltageView.py | 115 +++++++++++++++++++++++++++++++ VoltageView.wxg | 155 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 ++ 6 files changed, 354 insertions(+), 52 deletions(-) create mode 100644 VoltageView.py create mode 100644 VoltageView.wxg create mode 100644 requirements.txt diff --git a/.idea/misc.xml b/.idea/misc.xml index bef9027..cf67d7b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/.idea/wxGladeTest.iml b/.idea/wxGladeTest.iml index 74d515a..a7875f7 100644 --- a/.idea/wxGladeTest.iml +++ b/.idea/wxGladeTest.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/BmsApp.py b/BmsApp.py index 0fa29fe..8c9bd32 100644 --- a/BmsApp.py +++ b/BmsApp.py @@ -1,6 +1,10 @@ #!/usr/bin/env python # -*- coding: UTF-8 -*- +import datetime +import csv +import win32api + import wx import wx.propgrid @@ -13,13 +17,22 @@ from enum import Enum import can import cantools -from BmsView import BmsMonitorFrame +from VoltageView import BmsMonitorFrame + +startTime = datetime.datetime.now() +winUptime_ms = win32api.GetTickCount() +winUptime_dt = datetime.timedelta(milliseconds=winUptime_ms) +bootTime = startTime-winUptime_dt db = cantools.database.load_file('res/EBUS_Host.dbc') -nModule = 22 +nModule = 3 nCell = 12 +completeKey = "isModuleDataComplete" +timestampKey = "MessageTimestamp" +lastUpdateKey = timestampKey + ".old" +lastMsgKey = "LastMessageId" def parse_can_id(arbitration_id): mprefix = (arbitration_id & 0xfffff000) >> 12 @@ -48,7 +61,7 @@ def find_category(imsg, isignal): if 0x00 <= msg_type <= 0x06: return BMS_MSG_CAT.BMS_PROFILE - elif 0x07 <= msg_type <= 0x0B: + elif 0x07 <= msg_type <= 0x0C: return BMS_MSG_CAT.MODULE_DATA elif msg_type == 0x10: if isignal < 4: @@ -84,7 +97,7 @@ class BmsListener (can.Listener): self.parent = parent def on_message_received(self, msg): - print("{:#X}".format(msg.arbitration_id)) + # print("{:#X}".format(msg.arbitration_id)) self.parent.process_message(msg) @@ -98,8 +111,31 @@ class Controller: pub.subscribe (self.view.update_battery_module_grid, "bms.module.update") + pub.subscribe(self.start_write, "view.start") + pub.subscribe(self.stop_write, "view.stop") + pub.subscribe (self.write_battery_module, "bms.module.update") + pub.subscribe (self.model.setter, "view.id_selector") + def start_write(self): + filename = self.view.text_ctrl_1.GetValue() + self.output_files = [open(filename+"-M{}.csv".format(i+1), 'w') for i in range(nModule)] + self.csv_writers = [csv.writer(output_file, delimiter=',') for output_file in self.output_files] + for csv_writer in self.csv_writers: + csv_writer.writerow(["time"] + ["CV{:02d}".format(i) for i in range(nCell)]) + + def stop_write(self): + for output_file in self.output_files: + output_file.close() + + def write_battery_module(self, state, i_module): + module_dict = state[i_module] + if module_dict[lastMsgKey] & 0xff == 0x0c: + keyString = 'BatS_Cell{}_V' + voltages = [module_dict.get(keyString.format(x), -1) for x in range(nCell)] + dt = str(bootTime+datetime.timedelta(seconds=module_dict.get(timestampKey, 0.0))) + self.csv_writers[i_module].writerow([dt]+voltages) + class PmgrowBms: def __init__(self, add_printer=True): @@ -108,7 +144,7 @@ class PmgrowBms: self.notifier = None self.listeners = [can.Printer()] if add_printer else [] self.listeners.append(BmsListener(self)) - self.master_id = 0x02 + self.master_id = 0x09 def setter (self, id): print ("updating master id") @@ -128,17 +164,28 @@ class PmgrowBms: self.notifier = None self.bus = None - def __update_bm_state(self, dmsg): - imodule = dmsg['BatS_Cell_ModuleNo'] + def __update_bm_state(self, dmsg, msg): + i_module = dmsg['BatS_Cell_ModuleNo'] + self.battery_module_state[i_module][lastUpdateKey] = self.battery_module_state[i_module].get(timestampKey,0.0) + self.battery_module_state[i_module][timestampKey] = msg.timestamp + self.battery_module_state[i_module][lastMsgKey] = msg.arbitration_id + + if self.battery_module_state[i_module][timestampKey] - self.battery_module_state[i_module][lastUpdateKey] > 0.1: + self.battery_module_state[i_module][completeKey] = 4 + else: + self.battery_module_state[i_module][completeKey] = self.battery_module_state[i_module][completeKey] - 1 + for k in dmsg: - self.battery_module_state[imodule][k] = dmsg[k] + self.battery_module_state[i_module][k] = dmsg[k] + + return i_module def process_message(self, msg): msg_prefix, master_id, msg_type = parse_can_id(msg.arbitration_id) full_id = (msg_prefix, master_id, msg_type) db_msg = db.get_message_by_frame_id(msg.arbitration_id) - print(master_id, self.master_id) + # print(master_id, self.master_id) if master_id == self.master_id: if find_category(full_id, 0) == BMS_MSG_CAT.MODULE_DATA: try: @@ -146,8 +193,8 @@ class PmgrowBms: except KeyError as key: print(key, " does not exist") else: - self.__update_bm_state(decoded) - pub.sendMessage("bms.module.update", state=self.battery_module_state) + i_module = self.__update_bm_state(decoded, msg) + pub.sendMessage("bms.module.update", state=self.battery_module_state, i_module=i_module) elif find_category(full_id, 0) == BMS_MSG_CAT.BMS_PROFILE: pass @@ -184,36 +231,6 @@ class BmsMonitorView(BmsMonitorFrame): def __init__(self, *args, **kwds): BmsMonitorFrame.__init__(self, *args, **kwds) - self.property_grid_page_info = self.property_grid_1.AddPage("Info") - - for c in pgrid_category: - cat_name = c.name - cat = wx.propgrid.PropertyCategory(label=cat_name) - self.property_grid_page_info.Append(cat) - - # bms_model = wx.propgrid.StringProperty(label="Model", name="bms_model", value="B3DWER32EW") - # self.property_grid_page_info.AppendIn("packdata", bms_model) - - for msg in db.messages: - prefix, id, type = parse_can_id(msg.frame_id) - id_tuple = prefix, id, type - - if id == 0x02: - - if find_category(id_tuple, 0) == BMS_MSG_CAT.BMS_PROFILE: - prop = wx.propgrid.StringProperty(label=msg.name) - self.property_grid_page_info.AppendIn(BMS_MSG_CAT.BMS_PROFILE.name, prop) - - for isig, signal in enumerate(msg.signals): - plabel = str_hex(type) + signal.name - prop = wx.propgrid.StringProperty(label=plabel) - prop.SetHelpString(signal.comment) - - cat = find_category(id_tuple, isig) - - if cat in pgrid_category: - self.property_grid_page_info.AppendIn(cat.name, prop) - def GetModuleVoltage(self, module_dict): keyString = 'BatS_Cell{}_V' return [module_dict.get(keyString.format(x), -1) for x in range(nCell)] @@ -222,18 +239,28 @@ class BmsMonitorView(BmsMonitorFrame): keyString = 'BatS_Cell{}_BalState' return [module_dict.get(keyString.format(x), -1) for x in range(nCell)] - def update_battery_module_grid(self, state): - i_module = self.spin_ctrl_1.GetValue() + def update_battery_module_grid(self, state, i_module): + # grids = [self.grid_1, self.grid_2, self.grid_3] + ts_ctrls = [self.timestamp_1, self.timestamp_2, self.timestamp_3, ] + grid = self.grid_1 + ts_ctrl = ts_ctrls[i_module] module_dict = state[i_module] - voltages = self.GetModuleVoltage(module_dict) - bal_states = self.GetModuleBalState(module_dict) - for i in range(nCell): - self.grid_1.SetCellValue(i, 0, str(voltages[i])) - self.grid_1.SetCellValue(i, 2, str(bal_states[i])) - self.grid_1.ForceRefresh() + if module_dict[lastMsgKey] & 0xff == 0x0c: + voltages = self.GetModuleVoltage(module_dict) + bal_states = self.GetModuleBalState(module_dict) + dt = datetime.datetime.fromtimestamp(module_dict.get(timestampKey, 0.0)) + msg_id = module_dict.get(lastMsgKey, 0) + ts_ctrl.SetValue(str(bootTime+datetime.timedelta(seconds=dt.timestamp()))) + for i in range(nCell): + grid.SetCellValue(i, i_module, str(voltages[i])) + print("refreshing module ", i_module, "latest update: ", bootTime+datetime.timedelta(seconds=dt.timestamp()), + dt.timestamp(), "{:x}".format(msg_id)) + grid.ForceRefresh() def OnIdChoice(self, event): print ("sending update request") + # event.Get + # wx.Event.GetEventObject() pub.sendMessage("view.id_selector", id=int(self.choice_1.GetStringSelection(), 16)) event.Skip() @@ -253,7 +280,7 @@ class BmsMonitorView(BmsMonitorFrame): class BmsApp(wx.App): def OnInit(self): - self.bms = PmgrowBms() + self.bms = PmgrowBms(add_printer=True) self.frame = BmsMonitorView(None, wx.ID_ANY, "") self.bms_controller = Controller(self.bms, self.frame) self.SetTopWindow(self.frame) diff --git a/VoltageView.py b/VoltageView.py new file mode 100644 index 0000000..c720314 --- /dev/null +++ b/VoltageView.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# generated by wxGlade 0.9.6 on Sun Nov 29 07:00:24 2020 +# + +import wx +import wx.grid + +# begin wxGlade: dependencies +import gettext +# end wxGlade + +# begin wxGlade: extracode +# end wxGlade + + +class BmsMonitorFrame(wx.Frame): + def __init__(self, *args, **kwds): + # begin wxGlade: BmsMonitorFrame.__init__ + kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.SYSTEM_MENU + wx.Frame.__init__(self, *args, **kwds) + self.SetSize((338, 393)) + self.button_1 = wx.ToggleButton(self, wx.ID_ANY, _("Start")) + self.text_ctrl_1 = wx.TextCtrl(self, wx.ID_ANY, _("prefix")) + self.timestamp_1 = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_READONLY) + self.timestamp_2 = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_READONLY) + self.timestamp_3 = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_READONLY) + self.grid_1 = wx.grid.Grid(self, wx.ID_ANY, size=(1, 1)) + + self.__set_properties() + self.__do_layout() + + self.Bind(wx.EVT_TOGGLEBUTTON, self.OnToggle, self.button_1) + # end wxGlade + + def __set_properties(self): + # begin wxGlade: BmsMonitorFrame.__set_properties + self.SetTitle(_("BMS Monitor")) + _icon = wx.NullIcon + _icon.CopyFromBitmap(wx.Bitmap("res\\repurpose-energy-ci-icon.ico", wx.BITMAP_TYPE_ANY)) + self.SetIcon(_icon) + self.grid_1.CreateGrid(12, 3) + self.grid_1.EnableEditing(0) + self.grid_1.EnableDragColSize(0) + self.grid_1.EnableDragRowSize(0) + self.grid_1.EnableDragGridSize(0) + self.grid_1.SetColLabelValue(0, _("Slave 1 (V)")) + self.grid_1.SetColLabelValue(1, _("Slave 2 (V)")) + self.grid_1.SetColLabelValue(2, _("Slave 3 (V)")) + self.grid_1.SetRowLabelValue(0, _("Cell 1")) + self.grid_1.SetRowLabelValue(1, _("Cell 2")) + self.grid_1.SetRowLabelValue(2, _("Cell 3")) + self.grid_1.SetRowLabelValue(3, _("Cell 4")) + self.grid_1.SetRowLabelValue(4, _("Cell 5")) + self.grid_1.SetRowLabelValue(5, _("Cell 6")) + self.grid_1.SetRowSize(5, 18) + self.grid_1.SetRowLabelValue(6, _("Cell 7")) + self.grid_1.SetRowLabelValue(7, _("Cell 8")) + self.grid_1.SetRowLabelValue(8, _("Cell 9")) + self.grid_1.SetRowLabelValue(9, _("Cell 10")) + self.grid_1.SetRowLabelValue(10, _("Cell 11")) + self.grid_1.SetRowLabelValue(11, _("Cell 12")) + # end wxGlade + + def __do_layout(self): + # begin wxGlade: BmsMonitorFrame.__do_layout + sizer_1 = wx.BoxSizer(wx.VERTICAL) + sizer_4 = wx.BoxSizer(wx.HORIZONTAL) + sizer_3 = wx.BoxSizer(wx.HORIZONTAL) + sizer_2 = wx.BoxSizer(wx.HORIZONTAL) + sizer_6 = wx.BoxSizer(wx.HORIZONTAL) + sizer_6.Add(self.button_1, 0, 0, 0) + sizer_6.Add((20, 20), 0, 0, 0) + label_4 = wx.StaticText(self, wx.ID_ANY, _("Output: ")) + sizer_6.Add(label_4, 0, 0, 0) + sizer_6.Add(self.text_ctrl_1, 0, 0, 0) + sizer_1.Add(sizer_6, 0, wx.EXPAND, 0) + label_1 = wx.StaticText(self, wx.ID_ANY, _("Slave 1 Update ")) + sizer_2.Add(label_1, 0, 0, 0) + sizer_2.Add(self.timestamp_1, 1, 0, 0) + sizer_1.Add(sizer_2, 0, wx.EXPAND, 0) + label_2 = wx.StaticText(self, wx.ID_ANY, _("Slave 2 Update ")) + sizer_3.Add(label_2, 0, 0, 0) + sizer_3.Add(self.timestamp_2, 1, wx.EXPAND, 0) + sizer_1.Add(sizer_3, 0, wx.EXPAND, 0) + label_3 = wx.StaticText(self, wx.ID_ANY, _("Slave 3 Update ")) + sizer_4.Add(label_3, 0, 0, 0) + sizer_4.Add(self.timestamp_3, 2, wx.EXPAND, 0) + sizer_1.Add(sizer_4, 0, wx.EXPAND, 0) + sizer_1.Add(self.grid_1, 1, wx.EXPAND, 0) + self.SetSizer(sizer_1) + self.Layout() + # end wxGlade + + def OnToggle(self, event): # wxGlade: BmsMonitorFrame. + print("Event handler 'OnToggle' not implemented!") + event.Skip() + +# end of class BmsMonitorFrame + +class BmsApp(wx.App): + def OnInit(self): + self.frame = BmsMonitorFrame(None, wx.ID_ANY, "") + self.SetTopWindow(self.frame) + self.frame.Show() + return True + +# end of class BmsApp + +if __name__ == "__main__": + gettext.install("app") # replace with the appropriate catalog name + + app = BmsApp(0) + app.MainLoop() diff --git a/VoltageView.wxg b/VoltageView.wxg new file mode 100644 index 0000000..af3292c --- /dev/null +++ b/VoltageView.wxg @@ -0,0 +1,155 @@ + + + + + + 338, 393 + BMS Monitor + + res\repurpose-energy-ci-icon.ico + + wxVERTICAL + + + 0 + wxEXPAND + + wxHORIZONTAL + + + 0 + + + OnToggle + + + + + + + 0 + + 20 + 20 + + + + + 0 + + + + + + + 0 + + prefix + + + + + + + 0 + wxEXPAND + + wxHORIZONTAL + + + 0 + + + + + + + 0 + + + + + + + + + 0 + wxEXPAND + + wxHORIZONTAL + + + 0 + + + + + + + 0 + wxEXPAND + + + + + + + + + 0 + wxEXPAND + + wxHORIZONTAL + + + 0 + + + + + + + 0 + wxEXPAND + + + + + + + + + 0 + wxEXPAND + + 1 + + Slave 1 (V) + Slave 2 (V) + Slave 3 (V) + + + Cell 1 + Cell 2 + Cell 3 + Cell 4 + Cell 5 + Cell 6 + Cell 7 + Cell 8 + Cell 9 + Cell 10 + Cell 11 + Cell 12 + + 0 + 1 + 0 + 0 + 0 + wxGrid.wxGridSelectCells + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..24f86bd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +wxPython~=4.1.0 +python-can~=3.3.3 +cantools~=35.3.0 +pywin32~=300 +Pypubsub~=4.0.3 \ No newline at end of file