/* Copyright (C) 2020 - 2024, Thornwave Labs Inc * Written by Razvan Turiac <razvan.turiac@thornwave.com> * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated * documentation files (the “Software”), to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * Attribution shall be given to Thornwave Labs Inc. and shall be made visible to the final user. * * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include <gui_main.h> #include <wx/mstream.h> #include <wx/bitmap.h> #include <wx/dcbuffer.h> #include <wx/graphics.h> #if defined (__GNUC__) #include <arpa/inet.h> #include <resources/powermon_icon.xpm> #endif #if defined(_MSC_VER) #include <winsock.h> #endif #include <string.h> #include <gui_string_dialog.h> #include <model.h> GuiMain::GuiMain(): wxFrame(0, wxID_ANY, wxT("PowerMon Manager"), wxDefaultPosition, wxDefaultSize, wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxSYSTEM_MENU | wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX | wxCLIP_CHILDREN) { #if defined(__WXMSW__) SetIcon(wxIcon(wxT("APP_ICON"))); #elif defined (__GNUC__) SetIcon(wxIcon(wxICON(powermon_icon))); #endif Model::getInstance().mMainWindow = this; mMenuBar = new wxMenuBar; mDeviceMenu = new wxMenu; mDeviceMenu->Append(ID_MENU_ADD_DEVICE, wxT("Add New Device")); mDeviceMenu->Append(ID_MENU_DIRECT_CONNECT, wxT("WiFi Direct Connect")); mDeviceMenu->SetHelpString(ID_MENU_ADD_DEVICE, wxT("Add New Remote Device")); mDeviceMenu->SetHelpString(ID_MENU_DIRECT_CONNECT, wxT("Direct Connection to PowerMon-W Device")); mMenuBar->Append(mDeviceMenu, wxT("&Device")); wxMenu* menu = new wxMenu; menu->Append(wxID_ABOUT, wxT("&About")); mMenuBar->Append(menu, wxT("&Help")); SetMenuBar(mMenuBar); //main window sizer & panel wxPanel* panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); panel->SetSizer(main_sizer); mDeviceListNotebook = new wxNotebook(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, wxT("")); main_sizer->Add(mDeviceListNotebook, 1, wxALL | wxEXPAND, FromDIP(10)); mLocalDeviceListCtrl = new wxListCtrl(mDeviceListNotebook, ID_LISTBOX_LOCAL_DEVICE_LIST, wxDefaultPosition, wxDefaultSize, wxLC_SINGLE_SEL | wxLC_REPORT | wxLC_VRULES); mDeviceListNotebook->AddPage(mLocalDeviceListCtrl, wxT("Local Devices")); mCloudDeviceListCtrl = new wxListCtrl(mDeviceListNotebook, ID_LISTBOX_CLOUD_DEVICE_LIST, wxDefaultPosition, FromDIP(wxSize(800, 500)), wxLC_SINGLE_SEL | wxLC_REPORT | wxLC_VRULES); mDeviceListNotebook->AddPage(mCloudDeviceListCtrl, wxT("Cloud Devices")); mDeviceConnectButton = new wxButton(panel, ID_BUTTON_DEVICE_CONNECT, wxT("Connect"), wxDefaultPosition, FromDIP(wxSize(150, 40))); mDeviceConnectButton->Enable(false); main_sizer->Add(mDeviceConnectButton, 0, wxALL, FromDIP(10)); mLocalDeviceListCtrl->AppendColumn(wxT("Name"), wxLIST_FORMAT_LEFT, FromDIP(120)); mLocalDeviceListCtrl->AppendColumn(wxT("Model"), wxLIST_FORMAT_LEFT, FromDIP(120)); mLocalDeviceListCtrl->AppendColumn(wxT("Serial"), wxLIST_FORMAT_LEFT, FromDIP(150)); mLocalDeviceListCtrl->AppendColumn(wxT("FW"), wxLIST_FORMAT_RIGHT, FromDIP(50)); mLocalDeviceListCtrl->AppendColumn(wxT("V1"), wxLIST_FORMAT_RIGHT, FromDIP(75)); mLocalDeviceListCtrl->AppendColumn(wxT("V2"), wxLIST_FORMAT_RIGHT, FromDIP(75)); mLocalDeviceListCtrl->AppendColumn(wxT("I"), wxLIST_FORMAT_RIGHT, FromDIP(90)); mLocalDeviceListCtrl->AppendColumn(wxT("P"), wxLIST_FORMAT_RIGHT, FromDIP(80)); mLocalDeviceListCtrl->AppendColumn(wxT("Wh"), wxLIST_FORMAT_RIGHT, FromDIP(80)); mLocalDeviceListCtrl->AppendColumn(wxT("Ah"), wxLIST_FORMAT_RIGHT, FromDIP(80)); mLocalDeviceListCtrl->AppendColumn(wxT("Temp"), wxLIST_FORMAT_RIGHT, FromDIP(60)); mLocalDeviceListCtrl->AppendColumn(wxT("SoC"), wxLIST_FORMAT_RIGHT, FromDIP(60)); mLocalDeviceListCtrl->AppendColumn(wxT("Runtime"), wxLIST_FORMAT_RIGHT, FromDIP(100)); mLocalDeviceListCtrl->AppendColumn(wxT("PS"), wxLIST_FORMAT_RIGHT, FromDIP(50)); mLocalDeviceListCtrl->AppendColumn(wxT("Address"), wxLIST_FORMAT_RIGHT, FromDIP(150)); mLocalDeviceListCtrl->AppendColumn(wxT("RSSI"), wxLIST_FORMAT_RIGHT, FromDIP(70)); mCloudDeviceListCtrl->AppendColumn(wxT("Name"), wxLIST_FORMAT_LEFT, FromDIP(400)); mCloudDeviceListCtrl->AppendColumn(wxT("Model"), wxLIST_FORMAT_LEFT, FromDIP(120)); mCloudDeviceListCtrl->AppendColumn(wxT("Serial"), wxLIST_FORMAT_LEFT, FromDIP(150)); main_sizer->SetSizeHints(this); Centre(); UpdateCloudDeviceList(); mDeviceListNotebook->SetSelection(0); mPowermonScanner = PowermonScanner::createInstance(); mPowermonScanner->setCallback([this](const PowermonScanner::Advertisment &adv) { wxQueueEvent(this, new GuiEvent([this, adv](const GuiEvent &event) mutable { bool found = false; uint32_t index = 0; for(auto it = mLocalDeviceList.begin(); it != mLocalDeviceList.end(); it++, index++) { if (it->adv.serial == adv.serial) { it->adv = adv; it->timestamp = (int64_t)wxGetUTCTimeMillis().GetValue(); found = true; break; } } if (found == false) { DeviceListItem item; item.timestamp = wxGetUTCTimeMillis().GetValue(); item.adv = adv; mLocalDeviceList.push_back(item); index = mLocalDeviceList.size(); } UpdateLocalDeviceList(index, adv); })); }); mPowermonScanner->startWifiScan(); mPowermonScanner->startBleScan(); mScrubTimer = new wxTimer(this, ID_SCRUB_TIMER); mScrubTimer->Start(1000); SetSize(FromDIP(wxSize(800, 480))); } GuiMain::~GuiMain() { Model::getInstance().mMainWindow = nullptr; mPowermonScanner->stopWifiScan(); mPowermonScanner->stopBleScan(); delete mPowermonScanner; delete mScrubTimer; } void GuiMain::UpdateLocalDeviceList(int32_t index, const PowermonScanner::Advertisment &adv) { wxString str; if (index >= mLocalDeviceListCtrl->GetItemCount()) { wxListItem item; index = mLocalDeviceListCtrl->InsertItem(index, wxString(adv.name.c_str(), wxConvUTF8)); } else { mLocalDeviceListCtrl->SetItem(index, 0, wxString(adv.name.c_str(), wxConvUTF8)); } mLocalDeviceListCtrl->SetItem(index, 1, Powermon::getHardwareString(adv.hardware_revision_bcd)); str.Printf(wxT("%016" PRIX64), adv.serial); mLocalDeviceListCtrl->SetItem(index, 2, str); str.Printf(wxT("%X.%02X"), adv.firmware_version_bcd >> 8, adv.firmware_version_bcd & 0xFF); mLocalDeviceListCtrl->SetItem(index, 3, str); str.Printf(wxT("%.3f V"), adv.voltage1); mLocalDeviceListCtrl->SetItem(index, 4, str); if (!isnan(adv.voltage2)) str.Printf(wxT("%.3f V"), adv.voltage2); else str.clear(); mLocalDeviceListCtrl->SetItem(index, 5, str); str.Printf(wxT("%.3f A"), adv.current); mLocalDeviceListCtrl->SetItem(index, 6, str); if (fabs(adv.power) >= 1000) str.Printf(wxT("%.3f kW"), adv.power / 1000.0); else str.Printf(wxT("%.2f W"), adv.power); mLocalDeviceListCtrl->SetItem(index, 7, str); if (fabs(adv.power_meter) >= 1000) str.Printf(wxT("%.3f kWh"), adv.power_meter / 1000.0); else str.Printf(wxT("%.2f Wh"), adv.power_meter); mLocalDeviceListCtrl->SetItem(index, 8, str); if (fabs(adv.coulomb_meter) >= 1000) str.Printf(wxT("%.3f kAh"), adv.coulomb_meter / 1000.0); else str.Printf(wxT("%.2f Ah"), adv.coulomb_meter); mLocalDeviceListCtrl->SetItem(index, 9, str); str.Printf(wxT("%s%.0f \u00B0C"), adv.isExternalTemperature() ? "e " : "", adv.temperature); mLocalDeviceListCtrl->SetItem(index, 10, str); if (adv.soc > 100) str = wxT("---"); else str.Printf(wxT("%u %%"), adv.soc); mLocalDeviceListCtrl->SetItem(index, 11, str); if (adv.runtime > FG_RUNTIME_MAX) str = wxT("---"); else if (adv.runtime == 0) str = wxT("under 1m"); else if (adv.runtime > 60 * 24 * 45) str = wxT("over 45d"); else { uint32_t minutes = adv.runtime; uint32_t days = minutes / (60 * 24); minutes -= days * 60 * 24; uint32_t hours = minutes / 60; minutes -= hours * 60; if (days) str.Printf(wxT("%ud %uh"), days, hours); else if (hours) str.Printf(wxT("%uh %um"), hours, minutes); else str.Printf(wxT("%um"), minutes); } mLocalDeviceListCtrl->SetItem(index, 12, str); mLocalDeviceListCtrl->SetItem(index, 13, wxString(Powermon::getPowerStatusString(adv.power_status).c_str(), wxConvUTF8)); std::string strr = Powermon::getIpAddressString(adv.address); if (Powermon::hasWifi(adv.hardware_revision_bcd)) str = Powermon::getIpAddressString(adv.address); else str = Powermon::getMacAddressString(adv.address); mLocalDeviceListCtrl->SetItem(index, 14, str); str.Printf(wxT("%ddBm"), adv.rssi); mLocalDeviceListCtrl->SetItem(index, 15, str); mLocalDeviceListCtrl->RefreshItem(index); } void GuiMain::UpdateCloudDeviceList(void) { mCloudDeviceListCtrl->DeleteAllItems(); for(auto &id: Model::getInstance().cloudDeviceList) { size_t index = mCloudDeviceListCtrl->GetItemCount(); index = mCloudDeviceListCtrl->InsertItem(index, wxString(id.name.c_str(), wxConvUTF8)); mCloudDeviceListCtrl->SetItem(index, 1, Powermon::getHardwareString(id.hardware_revision_bcd)); wxString str; str.Printf(wxT("%" PRIX64), id.serial); mCloudDeviceListCtrl->SetItem(index, 2, str); } mDeviceListNotebook->SetSelection(1); } void GuiMain::OnListSelection(wxListEvent &event) { int32_t index = -1; if (mDeviceListNotebook->GetSelection() == 0) index = mLocalDeviceListCtrl->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); else if (mDeviceListNotebook->GetSelection() == 1) index = mCloudDeviceListCtrl->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); mDeviceConnectButton->Enable(index >= 0); } void GuiMain::OnListActivation(wxListEvent &event) { startConnection(); } void GuiMain::OnButtonConnectClicked(wxCommandEvent &event) { startConnection(); } void GuiMain::OnScrubDeviceListTimerEvent(wxTimerEvent &event) { uint32_t index = 0; for(auto it = mLocalDeviceList.begin(); it != mLocalDeviceList.end();) { if ((wxGetUTCTimeMillis() - it->timestamp) >= 10000) { it = mLocalDeviceList.erase(it); mLocalDeviceListCtrl->DeleteItem(index); } else { ++it; index++; } } } void GuiMain::startConnection(void) { if (mDeviceListNotebook->GetSelection() == 0) { const int32_t index = mLocalDeviceListCtrl->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); if (index >= 0) { Powermon::DeviceIdentifier did; const DeviceListItem &device = mLocalDeviceList[index]; did.name = device.adv.name; did.serial = device.adv.serial; did.hardware_revision_bcd = device.adv.hardware_revision_bcd; did.address = device.adv.address; PowermonScanner* scanner = Powermon::hasWifi(device.adv.hardware_revision_bcd) ? nullptr : mPowermonScanner; GuiDevice* device_window = new GuiDevice(this, did, scanner); mDevices.push_back(device_window); device_window->SetFocus(); device_window->Show(); } } else if (mDeviceListNotebook->GetSelection() == 1) { const int32_t index = mCloudDeviceListCtrl->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); if (index >= 0) { const auto cloudDeviceList = Model::getInstance().cloudDeviceList; const Powermon::DeviceIdentifier &did = cloudDeviceList[index]; GuiDevice* device_window = new GuiDevice(this, did, nullptr); mDevices.push_back(device_window); device_window->SetFocus(); device_window->Show(); } } } void GuiMain::OnMenuDeviceAdd(wxCommandEvent &event) { GuiEnterString dialog(this, wxID_ANY, wxT("Enter PowerMon-W device link"), false, false); dialog.setMaxStringLength(255); dialog.setStringLabel(wxT("Device link")); if (dialog.ShowModal() != wxID_OK) return; char link[256]; memcpy(link, dialog.getString().mb_str(), dialog.getString().size()); link[dialog.getString().size()] = 0; Powermon::DeviceIdentifier id; if (id.fromURL(link)) Model::getInstance().addCloudDevice(id); else wxMessageDialog msg(this, wxT("Invalid PowerMon-W device link"), wxT("Error"), wxOK | wxSTAY_ON_TOP | wxCENTRE | wxICON_ERROR); } void GuiMain::OnMenuDeviceDirectConnect(wxCommandEvent &event) { wxMessageDialog msg(this, wxT("Please power up your PowerMon-W battery monitor. \n" "If you previously disabled the AP mode of your device, power cycle it.\n" "Use your computer WiFi settings to connect to the device.\b" "Your device will show up looking like: PMON_12A456B8.\n" "The password is: powermon"), wxT("Instructions"), wxCANCEL | wxOK | wxSTAY_ON_TOP | wxCENTRE); Powermon::DeviceIdentifier did; did.address = inet_addr("192.168.100.1"); did.hardware_revision_bcd = 0x40; GuiDevice* device_window = new GuiDevice(this, did, mPowermonScanner); device_window->SetFocus(); device_window->Show(); mDevices.push_back(device_window); } void GuiMain::OnMenuHelpAbout(wxCommandEvent &event) { wxString message; message.Printf(wxT("%s v%X.%02X. (C) Thornwave Labs Inc.\n\n"), g_version_tag, g_version_bcd >> 8, g_version_bcd & 0xFF); wxMessageDialog msg(this, message, wxT("About"), wxOK | wxSTAY_ON_TOP | wxCENTRE); msg.ShowModal(); } void GuiMain::OnClose(wxCloseEvent &event) { Destroy(); } void GuiMain::OnGuiEvent(GuiEvent &event) { event.process(); } BEGIN_EVENT_TABLE(GuiMain, wxFrame) EVT_CLOSE(GuiMain::OnClose) GUI_EVT(GuiMain::OnGuiEvent) EVT_LIST_ITEM_ACTIVATED(ID_LISTBOX_LOCAL_DEVICE_LIST, GuiMain::OnListActivation) EVT_LIST_ITEM_ACTIVATED(ID_LISTBOX_CLOUD_DEVICE_LIST, GuiMain::OnListActivation) EVT_LIST_ITEM_SELECTED(ID_LISTBOX_LOCAL_DEVICE_LIST, GuiMain::OnListSelection) EVT_LIST_ITEM_SELECTED(ID_LISTBOX_CLOUD_DEVICE_LIST, GuiMain::OnListSelection) EVT_LIST_ITEM_DESELECTED(ID_LISTBOX_LOCAL_DEVICE_LIST, GuiMain::OnListSelection) EVT_LIST_ITEM_DESELECTED(ID_LISTBOX_CLOUD_DEVICE_LIST, GuiMain::OnListSelection) EVT_BUTTON(ID_BUTTON_DEVICE_CONNECT, GuiMain::OnButtonConnectClicked) EVT_TIMER(ID_SCRUB_TIMER, GuiMain::OnScrubDeviceListTimerEvent) EVT_MENU(ID_MENU_ADD_DEVICE, GuiMain::OnMenuDeviceAdd) EVT_MENU(ID_MENU_DIRECT_CONNECT, GuiMain::OnMenuDeviceDirectConnect) EVT_MENU(wxID_ABOUT, GuiMain::OnMenuHelpAbout) END_EVENT_TABLE()